flowra 0.0.2.dev5__tar.gz → 0.0.4__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.
- {flowra-0.0.2.dev5 → flowra-0.0.4}/.gitignore +3 -1
- {flowra-0.0.2.dev5 → flowra-0.0.4}/CHANGELOG.md +19 -1
- {flowra-0.0.2.dev5 → flowra-0.0.4}/CLAUDE.md +33 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/PKG-INFO +1 -1
- {flowra-0.0.2.dev5 → flowra-0.0.4}/context7.json +6 -6
- {flowra-0.0.2.dev5 → flowra-0.0.4}/docs/architecture.md +2 -1
- {flowra-0.0.2.dev5 → flowra-0.0.4}/docs/lib.md +85 -131
- {flowra-0.0.2.dev5 → flowra-0.0.4}/docs/llm.md +5 -2
- flowra-0.0.4/docs/research/hooks_redesign.md +273 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/docs/research/strands_comparison.md +4 -4
- flowra-0.0.4/docs/research/voice_stt.md +105 -0
- flowra-0.0.4/docs/todo.md +126 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/examples/console_chat.py +9 -14
- {flowra-0.0.2.dev5 → flowra-0.0.4}/examples/llm_logging.py +5 -4
- {flowra-0.0.2.dev5 → flowra-0.0.4}/examples/tui_chat.py +48 -44
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/agent/__init__.py +5 -0
- flowra-0.0.4/flowra/agent/hooks.py +113 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/lib/chat/__init__.py +2 -4
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/lib/chat/agent.py +8 -11
- flowra-0.0.4/flowra/lib/chat/hook_events.py +23 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/lib/chat/spec.py +1 -1
- flowra-0.0.4/flowra/lib/tool_loop/__init__.py +71 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/lib/tool_loop/agent.py +50 -56
- flowra-0.0.4/flowra/lib/tool_loop/hook_events.py +178 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/lib/tool_loop/spec.py +1 -1
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/providers/anthropic_vertex.py +8 -4
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/schema_validation.py +8 -5
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/version.py +1 -1
- flowra-0.0.4/tests/agent/test_hooks.py +167 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/lib/test_chat_agent.py +45 -34
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/lib/test_tool_loop_agent.py +127 -125
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/providers/test_anthropic_e2e.py +2 -2
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/providers/test_anthropic_vertex.py +61 -1
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/test_schema_validation.py +25 -23
- flowra-0.0.2.dev5/docs/todo.md +0 -22
- flowra-0.0.2.dev5/flowra/lib/chat/hook_executor.py +0 -21
- flowra-0.0.2.dev5/flowra/lib/chat/hooks.py +0 -43
- flowra-0.0.2.dev5/flowra/lib/tool_loop/__init__.py +0 -107
- flowra-0.0.2.dev5/flowra/lib/tool_loop/hook_executor.py +0 -221
- flowra-0.0.2.dev5/flowra/lib/tool_loop/hooks.py +0 -479
- {flowra-0.0.2.dev5 → flowra-0.0.4}/.claude/commands/update-pricing.md +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/.env.example +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/.github/workflows/master.yml +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/.github/workflows/publish.yml +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/.github/workflows/pull_request.yml +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/.github/workflows/pull_request_e2e.yml +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/.python-version +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/LICENSE +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/Makefile +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/README.md +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/docs/agent.md +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/docs/review_plan.md +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/docs/review_prompts/step1_structure.md +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/docs/review_prompts/step2_code_style.md +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/docs/review_prompts/step3_documentation.md +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/docs/review_prompts/step4_doc_readability.md +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/docs/review_prompts/step5_doc_audit.md +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/docs/review_prompts/step6_tests.md +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/docs/runtime.md +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/docs/tools.md +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/examples/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/examples/app_agent.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/examples/llm_routing.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/examples/menu_agent.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/examples/menu_agent_class.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/examples/model_registry.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/examples/system_prompt.txt +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/examples/tools/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/examples/tools/calculator.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/examples/tools/random_numbers.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/examples/tools/switch_model.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/agent/agent.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/agent/agent_def.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/agent/agent_registry.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/agent/agent_store.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/agent/compile.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/agent/interrupt_helpers.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/agent/interrupt_token.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/agent/service_locator.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/agent/step_decorator.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/agent/stored_values.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/lib/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/lib/chat/config.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/lib/config_value.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/lib/llm_config.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/lib/tool_loop/_tool_call_agent.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/lib/tool_loop/cache.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/lib/tool_loop/config.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/lib/tool_loop/context.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/_base.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/blocks.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/messages.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/pricing/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/pricing/anthropic.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/pricing/google.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/pricing/openai.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/provider.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/providers/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/providers/google_vertex.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/providers/openai.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/request.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/response.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/schema_formatting.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/stream.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/llm/tools.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/py.typed +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/runtime/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/runtime/_sealed_scope.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/runtime/engine.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/runtime/execution.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/runtime/interrupt.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/runtime/runtime.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/runtime/runtime_scope.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/runtime/serialization.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/runtime/storage/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/runtime/storage/file.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/runtime/storage/in_memory.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/runtime/storage/session_storage.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/tools/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/tools/local_tool.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/tools/mcp_connection.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/tools/tool_group.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/tools/tool_registry.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/flowra/tools/types.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/pyproject.toml +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/agent/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/agent/test_agent.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/agent/test_agent_def.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/agent/test_agent_registry.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/agent/test_compile.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/agent/test_step_ref.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/agent/test_values.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/agent/test_with_interrupt.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/lib/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/lib/test_config_value.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/lib/test_tool_call_agent.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/lib/tool_loop/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/lib/tool_loop/test_cache.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/pricing/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/pricing/test_anthropic.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/pricing/test_google.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/pricing/test_openai.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/providers/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/providers/test_google_vertex.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/providers/test_google_vertex_e2e.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/providers/test_openai_e2e.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/providers/test_openai_provider.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/test_metadata.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/test_response.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/test_schema_formatting.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/llm/test_stream.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/runtime/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/runtime/storage/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/runtime/storage/test_file.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/runtime/storage/test_in_memory.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/runtime/test_engine.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/runtime/test_interrupt.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/runtime/test_persistence.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/runtime/test_runtime.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/runtime/test_scope.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/runtime/test_serialization.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/tools/__init__.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/tools/test_local_tool.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/tools/test_mcp_connection.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/tools/test_tool_group.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/tests/tools/test_tool_registry.py +0 -0
- {flowra-0.0.2.dev5 → flowra-0.0.4}/uv.lock +0 -0
|
@@ -5,7 +5,25 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.0.4] - 2026-03-09
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **JSON schema response cleanup**: `AnthropicVertexProvider` now returns a single `TextBlock`
|
|
12
|
+
with clean JSON when `json_schema` is set — markdown fences and thinking blocks are stripped.
|
|
13
|
+
Previously, validated JSON was cleaned internally but the original (fenced) text was returned.
|
|
14
|
+
- `validate_json_schema` now returns `(error, cleaned_text)` tuple so callers get the
|
|
15
|
+
fence-stripped text.
|
|
16
|
+
|
|
17
|
+
## [0.0.3] - 2026-03-08
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **Hook system redesign**: replaced `ToolLoopHooks`/`ChatHooks` (24 Protocol classes, 12 executor
|
|
21
|
+
functions, 5 result dataclasses) with generic `HookRegistry` + `Event[T]` pattern. Supports
|
|
22
|
+
multiple handlers per event, `HookProvider` for distributable hook sets, and `propagate_to()`
|
|
23
|
+
for parent/child isolation. Hook events are plain mutable dataclasses with result fields.
|
|
24
|
+
- `ToolLoopSpec.meta` / `ChatSpec.meta` renamed to `metadata`.
|
|
25
|
+
|
|
26
|
+
## [0.0.2] - 2026-03-08
|
|
9
27
|
|
|
10
28
|
### Added
|
|
11
29
|
- **Streaming**: `LLMProvider.stream()` method returns `AsyncIterator[StreamEvent]` with `TextDelta`, `ThinkingDelta`, and `ContentComplete` events. All three built-in providers implement real-time streaming. Default fallback calls `call()` and yields `ContentComplete`.
|
|
@@ -58,6 +58,39 @@ Review prompts live in `docs/review_prompts/`:
|
|
|
58
58
|
|
|
59
59
|
Test directory structure mirrors `flowra/`. E2E tests use `_e2e` suffix (e.g., `test_anthropic_e2e.py`). Environment variables loaded from `.env` via Makefile.
|
|
60
60
|
|
|
61
|
+
## Releases
|
|
62
|
+
|
|
63
|
+
CI workflow: `.github/workflows/publish.yml` (manual trigger via `workflow_dispatch`).
|
|
64
|
+
|
|
65
|
+
### Pre-release
|
|
66
|
+
|
|
67
|
+
No preparation needed. Tell the user to trigger the `publish` workflow with `type: pre-release` in GitHub Actions. Do NOT trigger it yourself. CI appends `.dev{run_number}` to the current version automatically.
|
|
68
|
+
|
|
69
|
+
### Release
|
|
70
|
+
|
|
71
|
+
**Preparation (manual):**
|
|
72
|
+
|
|
73
|
+
1. Check that `flowra/version.py` has the correct version (CI bumps it automatically after each release, so it should already be set)
|
|
74
|
+
2. Rename the `[Unreleased]` section in `CHANGELOG.md` to `[X.Y.Z] - YYYY-MM-DD` matching the version in `version.py`
|
|
75
|
+
3. Verify documentation is up to date with code changes (especially `docs/llm.md`, `context7.json`)
|
|
76
|
+
4. Run `make check` — lint + all tests must pass
|
|
77
|
+
5. Commit, push to `master`
|
|
78
|
+
|
|
79
|
+
**Publishing:**
|
|
80
|
+
|
|
81
|
+
Tell the user to trigger the `publish` workflow manually in GitHub Actions. Do NOT trigger it yourself.
|
|
82
|
+
|
|
83
|
+
CI (`type: release`) will:
|
|
84
|
+
- Validate that the branch is `master`
|
|
85
|
+
- Validate that `CHANGELOG.md` has a section matching the version
|
|
86
|
+
- Run lint + tests
|
|
87
|
+
- Build and publish to PyPI
|
|
88
|
+
- Create git tag `v{version}` and GitHub Release
|
|
89
|
+
- Bump version for next cycle (patch increment) and add `[Unreleased]` section
|
|
90
|
+
- Push the version bump commit to `master`
|
|
91
|
+
|
|
92
|
+
**Do NOT** create git tags manually — CI handles this.
|
|
93
|
+
|
|
61
94
|
## Maintenance
|
|
62
95
|
|
|
63
96
|
- **`context7.json`** — project description for [Context7](https://context7.com). Must be updated when adding new features, changing public APIs, or modifying architecture. Keep rules in sync with actual capabilities.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowra
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: Flowra — flow infrastructure for building stateful LLM agents
|
|
5
5
|
Project-URL: Repository, https://github.com/anna-money/flowra
|
|
6
6
|
Project-URL: Changelog, https://github.com/anna-money/flowra/blob/master/CHANGELOG.md
|
|
@@ -62,15 +62,15 @@
|
|
|
62
62
|
"ChatAgent usage: runtime.run(agent=ChatAgent, step=ChatAgent.process_message, spec=ChatSpec(user_message=text))",
|
|
63
63
|
"ChatSpec(user_message: str) is the input, ChatResult(response: str | None, usage: Usage | None) is the output",
|
|
64
64
|
"ChatConfig configures ChatAgent: ChatConfig(llm_config=LLMConfig(model='...'), system_messages=[SystemMessage(...)])",
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"Transient message pattern: mark messages with metadata={'transient': True}, then filter in
|
|
65
|
+
"HookRegistry from flowra.agent provides generic Event[T] + handler pattern for lifecycle hooks. Register handlers via hooks.on(EventType, handler). Supports multiple handlers, sync/async transparency, HookProvider composition, and propagate_to() for isolation",
|
|
66
|
+
"SaveTurnMessagesEvent filters turn messages before saving to session history — mutate event.data.messages in place to control what gets persisted. Useful for excluding transient messages injected by hooks",
|
|
67
|
+
"Transient message pattern: mark messages with metadata={'transient': True}, then filter in SaveTurnMessagesEvent handler",
|
|
68
68
|
|
|
69
69
|
"PRE-BUILT AGENTS — ToolLoopAgent: single-turn LLM tool loop with hooks and caching",
|
|
70
70
|
"ToolLoopAgent sends messages to LLM, executes tool calls, feeds results back, repeats until done",
|
|
71
71
|
"ToolLoopConfig configures ToolLoopAgent: ToolLoopConfig(llm_config=LLMConfig(model='...'), cache_config=CacheConfig(...))",
|
|
72
|
-
"
|
|
73
|
-
"When
|
|
72
|
+
"ToolLoopAgent hook events: UserMessageEvent, MessageAcceptedEvent, StartIterationEvent, BeforeLLMCallEvent, TextDeltaEvent, ThinkingDeltaEvent, AfterLLMCallEvent, TextReasoningEvent, ThinkingEvent, ResultMessageEvent, BeforeToolCallEvent, AfterToolCallEvent",
|
|
73
|
+
"When TextDeltaEvent or ThinkingDeltaEvent handlers are registered, ToolLoopAgent automatically uses provider.stream() instead of provider.call()",
|
|
74
74
|
|
|
75
75
|
"CACHING: CacheConfig(system_prompt, tools, messages) controls prompt caching strategies",
|
|
76
76
|
"Predefined configs: CACHE_ALL, CACHE_SESSION, CACHE_MANUAL, NO_CACHE",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"ConfigValue[T] wraps static or dynamic (callable) config values: ConfigValue[str] | ConfigValue[Callable[[], str]]",
|
|
81
81
|
|
|
82
82
|
"QUICK START: Create provider -> create ToolRegistry -> create Config -> create AgentRuntime -> runtime.run()",
|
|
83
|
-
"Import ChatAgent: from flowra.lib.chat import ChatAgent, ChatConfig,
|
|
83
|
+
"Import ChatAgent: from flowra.lib.chat import ChatAgent, ChatConfig, ChatResult, ChatSpec, SaveTurnMessagesEvent",
|
|
84
84
|
"Import LLMConfig: from flowra.lib import LLMConfig",
|
|
85
85
|
"Import LLM types: from flowra.llm import LLMProvider, SystemMessage, TextBlock, Usage, TextDelta, ThinkingDelta, ContentComplete",
|
|
86
86
|
"Import runtime: from flowra.runtime import AgentRuntime, FileSessionStorage",
|
|
@@ -56,7 +56,8 @@ Pre-built agents for common patterns:
|
|
|
56
56
|
- **`ToolLoopAgent`** — single-turn LLM tool loop. Sends messages to the LLM,
|
|
57
57
|
executes tool calls, feeds results back, repeats until done.
|
|
58
58
|
|
|
59
|
-
Also provides
|
|
59
|
+
Also provides a generic `HookRegistry` + `Event[T]` hook system, prompt caching strategies,
|
|
60
|
+
and dynamic config. → [docs/lib.md](lib.md)
|
|
60
61
|
|
|
61
62
|
## Data flow
|
|
62
63
|
|
|
@@ -11,14 +11,12 @@ flowra/lib/
|
|
|
11
11
|
├── chat/
|
|
12
12
|
│ ├── agent.py # ChatAgent — multi-turn chat with session history
|
|
13
13
|
│ ├── config.py # ChatConfig — chat-level configuration
|
|
14
|
-
│ ├──
|
|
15
|
-
│ ├── hook_executor.py # Internal: async/sync hook dispatch
|
|
14
|
+
│ ├── hook_events.py # SaveTurnMessagesEvent
|
|
16
15
|
│ └── spec.py # ChatSpec, ChatResult
|
|
17
16
|
└── tool_loop/
|
|
18
17
|
├── agent.py # ToolLoopAgent — single-turn LLM tool loop
|
|
19
18
|
├── config.py # ToolLoopConfig
|
|
20
|
-
├──
|
|
21
|
-
├── hook_executor.py # Internal: async/sync hook dispatch
|
|
19
|
+
├── hook_events.py # 12 event dataclasses for hook system
|
|
22
20
|
├── spec.py # ToolLoopSpec, ToolLoopResult, ToolLoopStatus
|
|
23
21
|
├── context.py # ToolLoopAgentContext — tool-accessible loop context
|
|
24
22
|
├── cache.py # CacheConfig, cache strategies and presets
|
|
@@ -114,10 +112,10 @@ Input for `ChatAgent.process_message`.
|
|
|
114
112
|
| Field | Type | Default |
|
|
115
113
|
|----------------|--------------------------|---------|
|
|
116
114
|
| `user_message` | `str` | — |
|
|
117
|
-
| `
|
|
115
|
+
| `metadata` | `dict[str, Any] \| None` | `None` |
|
|
118
116
|
|
|
119
|
-
`
|
|
120
|
-
|
|
117
|
+
`metadata` is passed through to `ToolLoopSpec` and delivered to `MessageAcceptedEvent`
|
|
118
|
+
— useful for tracking message IDs in interrupt scenarios.
|
|
121
119
|
|
|
122
120
|
### `ChatResult`
|
|
123
121
|
|
|
@@ -151,31 +149,32 @@ sync callable, or async callable (see [ConfigValue](#configvalue) below).
|
|
|
151
149
|
instead of `session_messages`. The chat agent builds `session_messages` automatically
|
|
152
150
|
by prepending `system_messages` to the accumulated conversation history.
|
|
153
151
|
|
|
154
|
-
###
|
|
152
|
+
### Chat hooks
|
|
155
153
|
|
|
156
|
-
Chat-level
|
|
154
|
+
Chat-level hook events, registered on the same `HookRegistry` instance as tool loop hooks.
|
|
157
155
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
**`SaveTurnMessagesEvent`** — emitted before turn messages are saved into session history.
|
|
157
|
+
Handlers mutate `messages` in place to control what gets persisted. Useful for excluding
|
|
158
|
+
transient messages (e.g. dynamic context injected by hooks) from long-term session history.
|
|
161
159
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
from long-term session history.
|
|
160
|
+
| Field | Type | Default |
|
|
161
|
+
|---------------------|---------------------------|---------|
|
|
162
|
+
| `messages` | `list[Message]` | — |
|
|
166
163
|
|
|
167
164
|
```python
|
|
168
|
-
from flowra.
|
|
165
|
+
from flowra.agent import HookRegistry
|
|
166
|
+
from flowra.lib.chat import ChatAgent, ChatConfig, SaveTurnMessagesEvent
|
|
169
167
|
from flowra.llm import Message
|
|
170
168
|
|
|
171
|
-
def filter_transient(
|
|
172
|
-
|
|
169
|
+
def filter_transient(event):
|
|
170
|
+
event.data.messages = [m for m in event.data.messages if not m.metadata.get("transient")]
|
|
173
171
|
|
|
174
|
-
hooks =
|
|
172
|
+
hooks = HookRegistry()
|
|
173
|
+
hooks.on(SaveTurnMessagesEvent, filter_transient)
|
|
175
174
|
|
|
176
175
|
runtime = AgentRuntime(
|
|
177
176
|
agents={"chat": ChatAgent},
|
|
178
|
-
services={..., ChatConfig: config,
|
|
177
|
+
services={..., ChatConfig: config, HookRegistry: hooks},
|
|
179
178
|
)
|
|
180
179
|
```
|
|
181
180
|
|
|
@@ -217,7 +216,7 @@ Input for `ToolLoopAgent.start`.
|
|
|
217
216
|
| Field | Type | Default |
|
|
218
217
|
|----------------|--------------------------|---------|
|
|
219
218
|
| `user_message` | `str` | — |
|
|
220
|
-
| `
|
|
219
|
+
| `metadata` | `dict[str, Any] \| None` | `None` |
|
|
221
220
|
|
|
222
221
|
### `ToolLoopResult`
|
|
223
222
|
|
|
@@ -278,99 +277,78 @@ the agent tells the LLM to try a different approach.
|
|
|
278
277
|
|
|
279
278
|
## Hooks
|
|
280
279
|
|
|
281
|
-
|
|
282
|
-
|
|
280
|
+
Lifecycle hooks use a generic `HookRegistry` + `Event[T]` pattern from `flowra.agent`.
|
|
281
|
+
Register handlers for typed event dataclasses. Supports multiple handlers per event,
|
|
282
|
+
sync/async transparency, and composition via `HookProvider`.
|
|
283
283
|
|
|
284
284
|
```python
|
|
285
|
-
from flowra.
|
|
285
|
+
from flowra.agent import HookRegistry
|
|
286
|
+
from flowra.lib.tool_loop import BeforeLLMCallEvent, AfterLLMCallEvent
|
|
286
287
|
|
|
287
|
-
hooks =
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
)
|
|
288
|
+
hooks = HookRegistry()
|
|
289
|
+
hooks.on(BeforeLLMCallEvent, lambda e: print(f"Calling LLM with {len(e.data.request.messages)} messages"))
|
|
290
|
+
hooks.on(AfterLLMCallEvent, lambda e: print(f"LLM returned {e.data.response.stop_reason}"))
|
|
291
291
|
|
|
292
292
|
runtime = AgentRuntime(
|
|
293
293
|
agents={"chat": ChatAgent},
|
|
294
|
-
services={...,
|
|
294
|
+
services={..., HookRegistry: hooks},
|
|
295
295
|
)
|
|
296
296
|
```
|
|
297
297
|
|
|
298
|
-
### `
|
|
299
|
-
|
|
300
|
-
|
|
|
301
|
-
|
|
302
|
-
| `
|
|
303
|
-
| `
|
|
304
|
-
| `
|
|
305
|
-
| `
|
|
306
|
-
| `
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
298
|
+
### `HookRegistry`
|
|
299
|
+
|
|
300
|
+
| Method | Description |
|
|
301
|
+
|---------------------------------------------|--------------------------------------------------|
|
|
302
|
+
| `on(event_type, handler, *, prepend=False)` | Register a handler for an event type |
|
|
303
|
+
| `has_handlers(event_type) -> bool` | Check if any handlers are registered |
|
|
304
|
+
| `add(provider: HookProvider)` | Register all hooks from a HookProvider |
|
|
305
|
+
| `async emit(data: T) -> T` | Wrap data in `Event`, call handlers, return data |
|
|
306
|
+
| `propagate_to(target, *, only, exclude)` | Forward events to another registry |
|
|
307
|
+
|
|
308
|
+
Handlers receive `Event[T]` where `T` is the event dataclass. `Event` has two fields:
|
|
309
|
+
`data: T` (the event payload) and `context: HookContext | None` (metadata stamped by emit).
|
|
310
|
+
`HookContext` carries `agent_type: type[AbstractAgent] | None` and `agent_path: str`.
|
|
311
|
+
Handlers can mutate `event.data` fields to communicate results back.
|
|
312
|
+
|
|
313
|
+
### Hook events
|
|
314
|
+
|
|
315
|
+
| Event | Mutable fields | Description |
|
|
316
|
+
|------------------------|--------------------------------------------------|---------------------------------------------------------------------------|
|
|
317
|
+
| `UserMessageEvent` | `messages: list[UserMessage]` | Once at loop start. Mutate to replace or augment the initial messages. |
|
|
318
|
+
| `MessageAcceptedEvent` | — | After messages accepted (incl. on resume). Has `messages: list[Message]`. |
|
|
319
|
+
| `StartIterationEvent` | `additional_messages: list[UserMessage]` | At start of each iteration. Append to inject context before LLM call. |
|
|
320
|
+
| `BeforeLLMCallEvent` | — | Before each LLM request. Has `request`, `context`. |
|
|
321
|
+
| `TextDeltaEvent` | — | During streaming. Has `text` (str), `context`. |
|
|
322
|
+
| `ThinkingDeltaEvent` | — | During streaming. Has `text` (str), `context`. |
|
|
323
|
+
| `AfterLLMCallEvent` | — | After each LLM response. Has `request`, `response`, `context`. |
|
|
324
|
+
| `TextReasoningEvent` | — | For each `TextBlock` in response. Has `text` (TextBlock), `context`. |
|
|
325
|
+
| `ThinkingEvent` | — | For each `ThinkingBlock` in response. Has `thinking`, `context`. |
|
|
326
|
+
| `BeforeToolCallEvent` | `tool_use: ToolUseBlock` | Before tool execution. Replace to modify tool parameters. |
|
|
327
|
+
| `AfterToolCallEvent` | `result: ToolResultBlock`, `additional_messages` | After tool execution. Replace result or inject messages. |
|
|
328
|
+
| `ResultMessageEvent` | `continue_messages: list[UserMessage]` | On END_TURN. Set to continue loop instead of finishing. |
|
|
314
329
|
|
|
315
330
|
### Hook lifecycle
|
|
316
331
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
1. **`on_user_message`** — once at loop start, before the first iteration. Receives the
|
|
320
|
-
`UserMessage` built from the spec. Return `UserMessageResult(messages=[...])` to
|
|
321
|
-
replace the initial messages (include the original in the list to keep it).
|
|
322
|
-
|
|
323
|
-
2. **`on_message_accepted`** — once after the user message is persisted via `flush()`.
|
|
324
|
-
Receives `meta` from the spec. Useful for tracking accepted message IDs.
|
|
325
|
-
|
|
326
|
-
3. **`on_start_iteration`** — at the start of each iteration. Receives 1-indexed
|
|
327
|
-
iteration number and `ToolLoopAgentContext`. Return `StartIterationResult` with
|
|
328
|
-
`additional_messages` to inject context before the LLM call.
|
|
329
|
-
|
|
330
|
-
4. **`on_before_llm_call`** — before each LLM request. Receives `LLMRequest` and context.
|
|
331
|
-
Observational only (no return value).
|
|
332
|
+
Events fire in this order during a single tool loop iteration:
|
|
332
333
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
6. **`
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
7. **`on_text_reasoning`** — for each `TextBlock` in the assistant response. Fires
|
|
344
|
-
regardless of stop reason — useful for observing text output even when tool calls
|
|
345
|
-
are also present.
|
|
346
|
-
|
|
347
|
-
8. **`on_thinking`** — for each `ThinkingBlock` in the assistant response. Fires
|
|
348
|
-
for models with thinking/reasoning enabled (e.g. extended thinking).
|
|
334
|
+
1. **`UserMessageEvent`** — once at loop start, before the first iteration.
|
|
335
|
+
2. **`MessageAcceptedEvent`** — once after messages are accepted (including on resume).
|
|
336
|
+
3. **`StartIterationEvent`** — at the start of each iteration.
|
|
337
|
+
4. **`BeforeLLMCallEvent`** — before each LLM request.
|
|
338
|
+
5. **`TextDeltaEvent`** / **`ThinkingDeltaEvent`** — when handlers are registered for either,
|
|
339
|
+
the agent uses `provider.stream()` instead of `provider.call()`. These fire **during**
|
|
340
|
+
the LLM call, before `AfterLLMCallEvent`.
|
|
341
|
+
6. **`AfterLLMCallEvent`** — after each LLM response.
|
|
342
|
+
7. **`TextReasoningEvent`** — for each `TextBlock` in the assistant response.
|
|
343
|
+
8. **`ThinkingEvent`** — for each `ThinkingBlock` in the assistant response.
|
|
349
344
|
|
|
350
345
|
Then the flow branches based on stop reason:
|
|
351
346
|
|
|
352
347
|
- **If `TOOL_USE`:**
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
with `amended_tool_use` to modify tool parameters.
|
|
356
|
-
|
|
357
|
-
10. **`on_after_tool_call`** — after each tool execution. Return `AfterToolCallResult`
|
|
358
|
-
with `amended_result` and/or `additional_messages`.
|
|
359
|
-
|
|
348
|
+
9. **`BeforeToolCallEvent`** — before each tool execution.
|
|
349
|
+
10. **`AfterToolCallEvent`** — after each tool execution.
|
|
360
350
|
- **If `END_TURN`:**
|
|
361
|
-
|
|
362
|
-
11. **`on_result_message`** — return `ResultMessageResult` with `continue_messages`
|
|
363
|
-
to force the loop to continue instead of finishing.
|
|
364
|
-
|
|
365
|
-
### Hook result types
|
|
366
|
-
|
|
367
|
-
| Type | Fields |
|
|
368
|
-
|--------------------------|-------------------------------------------------------------------------------------|
|
|
369
|
-
| `UserMessageResult` | `messages: list[UserMessage]` |
|
|
370
|
-
| `StartIterationResult` | `additional_messages: list[UserMessage]` (default `[]`) |
|
|
371
|
-
| `BeforeToolCallResult` | `amended_tool_use: ToolUseBlock \| None` (default `None`) |
|
|
372
|
-
| `AfterToolCallResult` | `amended_result: ToolResultBlock \| None`, `additional_messages: list[UserMessage]` |
|
|
373
|
-
| `ResultMessageResult` | `continue_messages: list[UserMessage]` (default `[]`) |
|
|
351
|
+
11. **`ResultMessageEvent`** — set `continue_messages` to force the loop to continue.
|
|
374
352
|
|
|
375
353
|
## Prompt caching
|
|
376
354
|
|
|
@@ -515,23 +493,6 @@ value = await resolve_config_value(config.max_iterations) # always returns T
|
|
|
515
493
|
## Type aliases
|
|
516
494
|
|
|
517
495
|
```python
|
|
518
|
-
# Chat hook protocols (each has sync and async variant)
|
|
519
|
-
OnSaveTurnMessages / OnSaveTurnMessagesAsync
|
|
520
|
-
|
|
521
|
-
# Tool loop hook protocols (each has sync and async variant)
|
|
522
|
-
OnUserMessage / OnUserMessageAsync
|
|
523
|
-
OnMessageAccepted / OnMessageAcceptedAsync
|
|
524
|
-
OnStartIteration / OnStartIterationAsync
|
|
525
|
-
OnBeforeLLMCall / OnBeforeLLMCallAsync
|
|
526
|
-
OnAfterLLMCall / OnAfterLLMCallAsync
|
|
527
|
-
OnTextDelta / OnTextDeltaAsync
|
|
528
|
-
OnThinkingDelta / OnThinkingDeltaAsync
|
|
529
|
-
OnTextReasoning / OnTextReasoningAsync
|
|
530
|
-
OnThinking / OnThinkingAsync
|
|
531
|
-
OnResultMessage / OnResultMessageAsync
|
|
532
|
-
OnBeforeToolCall / OnBeforeToolCallAsync
|
|
533
|
-
OnAfterToolCall / OnAfterToolCallAsync
|
|
534
|
-
|
|
535
496
|
# Cache strategy protocols
|
|
536
497
|
SystemPromptCacheStrategy # (messages: list[SystemMessage]) -> list[SystemMessage]
|
|
537
498
|
ToolsCacheStrategy # (tools: list[Tool]) -> list[Tool]
|
|
@@ -554,23 +515,16 @@ ChatAgent
|
|
|
554
515
|
|
|
555
516
|
### Tool loop flow
|
|
556
517
|
|
|
557
|
-
1. `start` — accepts user message,
|
|
558
|
-
`turn_messages`, flushes,
|
|
559
|
-
2. `call_llm` — checks interrupt/finish/max_iterations,
|
|
560
|
-
builds `LLMRequest`,
|
|
561
|
-
`
|
|
562
|
-
`
|
|
563
|
-
- `END_TURN` →
|
|
518
|
+
1. `start` — accepts user message, emits `UserMessageEvent`, appends to
|
|
519
|
+
`turn_messages`, flushes, emits `MessageAcceptedEvent`, then gotos `call_llm`.
|
|
520
|
+
2. `call_llm` — checks interrupt/finish/max_iterations, emits `StartIterationEvent`,
|
|
521
|
+
builds `LLMRequest`, emits `BeforeLLMCallEvent`, calls LLM (streaming deltas via
|
|
522
|
+
`TextDeltaEvent`/`ThinkingDeltaEvent` if handlers registered), emits `AfterLLMCallEvent`,
|
|
523
|
+
`TextReasoningEvent`, and `ThinkingEvent`, then:
|
|
524
|
+
- `END_TURN` → emits `ResultMessageEvent`, returns `ToolLoopResult` (or continues
|
|
564
525
|
if `continue_messages` is non-empty)
|
|
565
|
-
- `TOOL_USE` →
|
|
526
|
+
- `TOOL_USE` → emits `BeforeToolCallEvent` for each tool, spawns `ToolCallAgent`
|
|
566
527
|
children in parallel
|
|
567
|
-
3. `collect_results` — gathers results from `ToolCallAgent` children,
|
|
568
|
-
`
|
|
528
|
+
3. `collect_results` — gathers results from `ToolCallAgent` children, emits
|
|
529
|
+
`AfterToolCallEvent` for each, applies `max_consecutive_errors` logic,
|
|
569
530
|
appends tool results to `turn_messages`, gotos `call_llm`.
|
|
570
|
-
|
|
571
|
-
### Hook executor
|
|
572
|
-
|
|
573
|
-
`hook_executor.py` provides `execute_*_hook()` functions that handle the sync/async
|
|
574
|
-
dispatch transparently — each function calls the hook, checks `inspect.isawaitable()`,
|
|
575
|
-
and awaits if needed. This allows hooks to be plain functions or async functions
|
|
576
|
-
interchangeably.
|
|
@@ -525,8 +525,11 @@ print(format_schema_for_llm(schema))
|
|
|
525
525
|
#### Schema validation (`schema_validation.py`)
|
|
526
526
|
|
|
527
527
|
`validate_json_schema(text, schema)` strips markdown code fences, parses JSON, and
|
|
528
|
-
validates against the schema. Returns `
|
|
529
|
-
on
|
|
528
|
+
validates against the schema. Returns `(error, cleaned_text)` tuple — `error` is `None`
|
|
529
|
+
on success (and `cleaned_text` contains the fence-stripped JSON), or an error description
|
|
530
|
+
when validation fails. Used internally by `AnthropicVertexProvider` for structured output
|
|
531
|
+
retries. When validation succeeds, the provider returns a single `TextBlock` with the
|
|
532
|
+
cleaned JSON (no markdown fences, no thinking blocks).
|
|
530
533
|
|
|
531
534
|
Internally, `strip_markdown_code_block(text)` removes surrounding markdown code fences
|
|
532
535
|
(`` ```...``` ``) before parsing. This is an implementation detail, not part of the public API.
|