agent-flywheel 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.
- agent_flywheel-0.1.0/.gitignore +58 -0
- agent_flywheel-0.1.0/LICENSE +21 -0
- agent_flywheel-0.1.0/PKG-INFO +429 -0
- agent_flywheel-0.1.0/README.md +365 -0
- agent_flywheel-0.1.0/examples/goose/README.md +78 -0
- agent_flywheel-0.1.0/examples/goose/demo_capture.py +90 -0
- agent_flywheel-0.1.0/examples/goose/serve_goose.py +321 -0
- agent_flywheel-0.1.0/examples/prime_agent/README.md +208 -0
- agent_flywheel-0.1.0/examples/prime_agent/route.ts +91 -0
- agent_flywheel-0.1.0/examples/prime_agent/serve_prime.py +551 -0
- agent_flywheel-0.1.0/pyproject.toml +146 -0
- agent_flywheel-0.1.0/schema/flywheel-1.json +432 -0
- agent_flywheel-0.1.0/scripts/cross_language_abi_check.sh +191 -0
- agent_flywheel-0.1.0/src/agent_flywheel/__init__.py +1067 -0
- agent_flywheel-0.1.0/src/agent_flywheel/_http.py +176 -0
- agent_flywheel-0.1.0/src/agent_flywheel/adapters/__init__.py +49 -0
- agent_flywheel-0.1.0/src/agent_flywheel/adapters/adk.py +268 -0
- agent_flywheel-0.1.0/src/agent_flywheel/adapters/langchain.py +288 -0
- agent_flywheel-0.1.0/src/agent_flywheel/adapters/prime_agent.py +1232 -0
- agent_flywheel-0.1.0/src/agent_flywheel/binding.py +360 -0
- agent_flywheel-0.1.0/src/agent_flywheel/child.py +63 -0
- agent_flywheel-0.1.0/src/agent_flywheel/cli.py +228 -0
- agent_flywheel-0.1.0/src/agent_flywheel/contract.py +411 -0
- agent_flywheel-0.1.0/src/agent_flywheel/endpoint.py +225 -0
- agent_flywheel-0.1.0/src/agent_flywheel/entrypoint.py +146 -0
- agent_flywheel-0.1.0/src/agent_flywheel/errors.py +87 -0
- agent_flywheel-0.1.0/src/agent_flywheel/event_stream.py +281 -0
- agent_flywheel-0.1.0/src/agent_flywheel/evidence.py +140 -0
- agent_flywheel-0.1.0/src/agent_flywheel/execution_identity.py +217 -0
- agent_flywheel-0.1.0/src/agent_flywheel/export.py +344 -0
- agent_flywheel-0.1.0/src/agent_flywheel/host_description.py +870 -0
- agent_flywheel-0.1.0/src/agent_flywheel/hosts/__init__.py +53 -0
- agent_flywheel-0.1.0/src/agent_flywheel/hosts/goose/plugin.json +7 -0
- agent_flywheel-0.1.0/src/agent_flywheel/hosts/scripts/percepteye_record.py +494 -0
- agent_flywheel-0.1.0/src/agent_flywheel/hosts/scripts/percepteye_report.py +192 -0
- agent_flywheel-0.1.0/src/agent_flywheel/install_hooks.py +301 -0
- agent_flywheel-0.1.0/src/agent_flywheel/introspection.py +619 -0
- agent_flywheel-0.1.0/src/agent_flywheel/isolation.py +227 -0
- agent_flywheel-0.1.0/src/agent_flywheel/outcomes.py +831 -0
- agent_flywheel-0.1.0/src/agent_flywheel/policy.py +490 -0
- agent_flywheel-0.1.0/src/agent_flywheel/production.py +1589 -0
- agent_flywheel-0.1.0/src/agent_flywheel/prompt.py +178 -0
- agent_flywheel-0.1.0/src/agent_flywheel/runner.py +1623 -0
- agent_flywheel-0.1.0/src/agent_flywheel/tracing.py +279 -0
- agent_flywheel-0.1.0/src/agent_flywheel/transport.py +328 -0
- agent_flywheel-0.1.0/tests/__init__.py +0 -0
- agent_flywheel-0.1.0/tests/conftest.py +26 -0
- agent_flywheel-0.1.0/tests/fixture_agent.py +178 -0
- agent_flywheel-0.1.0/tests/fixtures_agent.py +40 -0
- agent_flywheel-0.1.0/tests/node_sdk.py +97 -0
- agent_flywheel-0.1.0/tests/test_adk_adapter.py +325 -0
- agent_flywheel-0.1.0/tests/test_apply_current_policy.py +135 -0
- agent_flywheel-0.1.0/tests/test_architecture_svg_matches_its_renderer.py +53 -0
- agent_flywheel-0.1.0/tests/test_artifact_inline_bodies.py +221 -0
- agent_flywheel-0.1.0/tests/test_attach_consent_gate.py +179 -0
- agent_flywheel-0.1.0/tests/test_attach_production_capture.py +334 -0
- agent_flywheel-0.1.0/tests/test_binding.py +196 -0
- agent_flywheel-0.1.0/tests/test_capture_cost.py +285 -0
- agent_flywheel-0.1.0/tests/test_capture_verdict_parity.py +194 -0
- agent_flywheel-0.1.0/tests/test_cli_says_what_is_missing.py +135 -0
- agent_flywheel-0.1.0/tests/test_command_entrypoint.py +137 -0
- agent_flywheel-0.1.0/tests/test_completion_gate.py +132 -0
- agent_flywheel-0.1.0/tests/test_contract.py +221 -0
- agent_flywheel-0.1.0/tests/test_current_prompt.py +100 -0
- agent_flywheel-0.1.0/tests/test_documented_counts_are_true.py +130 -0
- agent_flywheel-0.1.0/tests/test_endpoint_follows_mode.py +284 -0
- agent_flywheel-0.1.0/tests/test_entity_identity_evidence.py +353 -0
- agent_flywheel-0.1.0/tests/test_entrypoint_and_outcomes.py +148 -0
- agent_flywheel-0.1.0/tests/test_event_stream_tool_calls.py +586 -0
- agent_flywheel-0.1.0/tests/test_excluded_rollouts_keep_their_evidence.py +240 -0
- agent_flywheel-0.1.0/tests/test_execution_identity.py +305 -0
- agent_flywheel-0.1.0/tests/test_export_matches_the_upload_path.py +157 -0
- agent_flywheel-0.1.0/tests/test_export_records.py +226 -0
- agent_flywheel-0.1.0/tests/test_goose_integration.py +370 -0
- agent_flywheel-0.1.0/tests/test_host_description.py +508 -0
- agent_flywheel-0.1.0/tests/test_host_description_wire.py +313 -0
- agent_flywheel-0.1.0/tests/test_install_hooks.py +298 -0
- agent_flywheel-0.1.0/tests/test_introspection.py +213 -0
- agent_flywheel-0.1.0/tests/test_isolation.py +206 -0
- agent_flywheel-0.1.0/tests/test_langchain_adapter.py +266 -0
- agent_flywheel-0.1.0/tests/test_langgraph_introspection.py +317 -0
- agent_flywheel-0.1.0/tests/test_lease_and_concurrency.py +624 -0
- agent_flywheel-0.1.0/tests/test_missing_control_plane_is_named.py +144 -0
- agent_flywheel-0.1.0/tests/test_mode_switch.py +308 -0
- agent_flywheel-0.1.0/tests/test_node_figures_match.py +55 -0
- agent_flywheel-0.1.0/tests/test_one_control_plane_spelling.py +75 -0
- agent_flywheel-0.1.0/tests/test_oss_boundary.py +305 -0
- agent_flywheel-0.1.0/tests/test_otel_egress.py +546 -0
- agent_flywheel-0.1.0/tests/test_packaging.py +128 -0
- agent_flywheel-0.1.0/tests/test_policy_delivery.py +135 -0
- agent_flywheel-0.1.0/tests/test_policy_source.py +458 -0
- agent_flywheel-0.1.0/tests/test_prime_agent_example.py +402 -0
- agent_flywheel-0.1.0/tests/test_prime_harness_snapshot.py +695 -0
- agent_flywheel-0.1.0/tests/test_production_turn_upload.py +785 -0
- agent_flywheel-0.1.0/tests/test_production_upload_producer.py +703 -0
- agent_flywheel-0.1.0/tests/test_prompt_decision.py +148 -0
- agent_flywheel-0.1.0/tests/test_prompt_decision_parity.py +212 -0
- agent_flywheel-0.1.0/tests/test_readme_renders_on_github.py +124 -0
- agent_flywheel-0.1.0/tests/test_report_turns_wire.py +353 -0
- agent_flywheel-0.1.0/tests/test_rollout_worker_identity_transport.py +99 -0
- agent_flywheel-0.1.0/tests/test_serve_registration.py +196 -0
- agent_flywheel-0.1.0/tests/test_served_model_is_servable.py +151 -0
- agent_flywheel-0.1.0/tests/test_subprocess_roundtrip.py +171 -0
- agent_flywheel-0.1.0/tests/test_substrate_unavailable.py +72 -0
- agent_flywheel-0.1.0/tests/test_task_argv_and_endpoint_env.py +127 -0
- agent_flywheel-0.1.0/tests/test_tool_call_id.py +219 -0
- agent_flywheel-0.1.0/tests/test_tool_calls_belong_to_their_own_turn.py +173 -0
- agent_flywheel-0.1.0/tests/test_tool_read_only_trait.py +80 -0
- agent_flywheel-0.1.0/tests/test_trajectory_is_crash_tolerant.py +134 -0
- agent_flywheel-0.1.0/tests/test_turn_text_capture.py +159 -0
- agent_flywheel-0.1.0/tests/test_zero_instrumentation.py +231 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.venv/
|
|
4
|
+
venv/
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
.mypy_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
htmlcov/
|
|
13
|
+
.percepteye-artifacts/
|
|
14
|
+
# ...and the misspellings the harness has actually written. All three appeared
|
|
15
|
+
# in one working tree: the artifacts root comes from a runtime path that is
|
|
16
|
+
# spelled differently in different places, so ignoring only the correct one
|
|
17
|
+
# left two directories of rollout output untracked-but-visible, which is how
|
|
18
|
+
# they end up in a `git add -A`. Directories, not file globs — a whole tree of
|
|
19
|
+
# `rl_<id>/` solve scripts is unambiguously agent output, so the reasoning
|
|
20
|
+
# below about not swallowing a future requirements.txt does not apply to them.
|
|
21
|
+
.perceptye-artifacts/
|
|
22
|
+
.percept-eye-artifacts/
|
|
23
|
+
_percepteye-artifacts/
|
|
24
|
+
.DS_Store
|
|
25
|
+
|
|
26
|
+
# Files an agent wrote into the repo root.
|
|
27
|
+
#
|
|
28
|
+
# A CLI agent's cwd IS its collected-artifacts directory, so running an example
|
|
29
|
+
# or a test from the repo root leaves the agent's output files here. Twenty of
|
|
30
|
+
# them were committed once by a `git add -A` -- 426KB of `xxxx` padding at the
|
|
31
|
+
# root of the SDK we ship.
|
|
32
|
+
#
|
|
33
|
+
# Enumerated, not globbed: `/*.txt` would silently swallow a future
|
|
34
|
+
# requirements.txt and `/*.sh` a future bootstrap script, and a gitignore that
|
|
35
|
+
# quietly drops a file someone wanted is a worse bug than the one it prevents.
|
|
36
|
+
# The durable fix is to run examples from a scratch directory rather than to
|
|
37
|
+
# extend this list forever.
|
|
38
|
+
/a.txt
|
|
39
|
+
/b.bin
|
|
40
|
+
/big.txt
|
|
41
|
+
/blob.bin
|
|
42
|
+
/hi.txt
|
|
43
|
+
/kept.txt
|
|
44
|
+
/answer.txt
|
|
45
|
+
/triage.sh
|
|
46
|
+
/f[0-9][0-9].txt
|
|
47
|
+
/avg_response_time.sh
|
|
48
|
+
/extract_ips.sh
|
|
49
|
+
/last_trace.sh
|
|
50
|
+
/merge_logs.sh
|
|
51
|
+
.worktrees/
|
|
52
|
+
|
|
53
|
+
# Cross-language ABI check scratch output (CI writes this, never committed)
|
|
54
|
+
abi/
|
|
55
|
+
|
|
56
|
+
# The built documentation site. Generated from website/docs by mkdocs;
|
|
57
|
+
# the source is tracked, the output is not.
|
|
58
|
+
website/site/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PerceptEye
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agent-flywheel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Self-improving agents through continual learning: in-process agentic RL that turns your own agent's work into training data. Observed tool outcomes, outbound-only, one runtime dependency.
|
|
5
|
+
Project-URL: Homepage, https://github.com/percepteye-ai/agent-flywheel
|
|
6
|
+
Project-URL: Documentation, https://github.com/percepteye-ai/agent-flywheel/tree/main/website/docs
|
|
7
|
+
Project-URL: Source, https://github.com/percepteye-ai/agent-flywheel
|
|
8
|
+
Project-URL: Issues, https://github.com/percepteye-ai/agent-flywheel/issues
|
|
9
|
+
Author: PerceptEye
|
|
10
|
+
Maintainer: PerceptEye
|
|
11
|
+
License: MIT License
|
|
12
|
+
|
|
13
|
+
Copyright (c) 2026 PerceptEye
|
|
14
|
+
|
|
15
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
16
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
17
|
+
in the Software without restriction, including without limitation the rights
|
|
18
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
19
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
20
|
+
furnished to do so, subject to the following conditions:
|
|
21
|
+
|
|
22
|
+
The above copyright notice and this permission notice shall be included in all
|
|
23
|
+
copies or substantial portions of the Software.
|
|
24
|
+
|
|
25
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
26
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
27
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
28
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
29
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
30
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
31
|
+
SOFTWARE.
|
|
32
|
+
License-File: LICENSE
|
|
33
|
+
Keywords: agent-evals,agent-rl,agentic-ai,agents,evaluation,flywheel,google-adk,langchain,langgraph,llm,llmops,observability,opentelemetry,reinforcement-fine-tuning,reinforcement-learning,rl-environments,rlvr,rollout,self-hosted
|
|
34
|
+
Classifier: Development Status :: 3 - Alpha
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
42
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
43
|
+
Classifier: Typing :: Typed
|
|
44
|
+
Requires-Python: >=3.10
|
|
45
|
+
Requires-Dist: httpx>=0.27
|
|
46
|
+
Provides-Extra: adapters-test
|
|
47
|
+
Requires-Dist: google-adk>=2.2; extra == 'adapters-test'
|
|
48
|
+
Requires-Dist: langchain-core>=0.3; extra == 'adapters-test'
|
|
49
|
+
Requires-Dist: langgraph>=1.0; extra == 'adapters-test'
|
|
50
|
+
Provides-Extra: dev
|
|
51
|
+
Requires-Dist: hatchling>=1.24; extra == 'dev'
|
|
52
|
+
Requires-Dist: jsonschema>=4.22; extra == 'dev'
|
|
53
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
54
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
55
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
56
|
+
Requires-Dist: ruff==0.16.2; extra == 'dev'
|
|
57
|
+
Provides-Extra: docs
|
|
58
|
+
Requires-Dist: mkdocs-material>=9.7; extra == 'docs'
|
|
59
|
+
Provides-Extra: otel
|
|
60
|
+
Requires-Dist: opentelemetry-api>=1.20; extra == 'otel'
|
|
61
|
+
Provides-Extra: otel-test
|
|
62
|
+
Requires-Dist: opentelemetry-sdk>=1.20; extra == 'otel-test'
|
|
63
|
+
Description-Content-Type: text/markdown
|
|
64
|
+
|
|
65
|
+
<div align="center">
|
|
66
|
+
|
|
67
|
+
<!-- IMAGE PATHS ARE RELATIVE ON PURPOSE, and it is a one-way door while this
|
|
68
|
+
repo is private. GitHub resolves a relative src against the repo you are
|
|
69
|
+
already authenticated to, so these render. An absolute
|
|
70
|
+
https://raw.githubusercontent.com/... src does NOT: raw.githubusercontent
|
|
71
|
+
is fetched anonymously and does not carry your github.com session, so a
|
|
72
|
+
PRIVATE repo answers 404 and every image on this page breaks -- for the
|
|
73
|
+
owner too, looking at their own README. Verified 2026-09-06: all four
|
|
74
|
+
raw.githubusercontent URLs a previous edit introduced returned 404, as did
|
|
75
|
+
the repo page itself.
|
|
76
|
+
|
|
77
|
+
This has now regressed TWICE, both times reasoning from PyPI: README.md is
|
|
78
|
+
copied into PKG-INFO and relative paths do not resolve there. That is true
|
|
79
|
+
and it is not the trade-off we are making. GitHub is the target surface for
|
|
80
|
+
these figures while the repo is private. PyPI cannot resolve them, so its
|
|
81
|
+
page shows their alt text instead -- and an absolute URL would not fix
|
|
82
|
+
that, because it 404s for PyPI's readers exactly as it does for GitHub's.
|
|
83
|
+
docs/assets/ is also deliberately excluded from both the wheel and the
|
|
84
|
+
sdist (tests/test_packaging.py builds the real artifacts and asserts it).
|
|
85
|
+
|
|
86
|
+
ON PUBLISH, and only once the repo is PUBLIC, absolute raw.githubusercontent
|
|
87
|
+
URLs become correct and are then required for PyPI rendering. Public first;
|
|
88
|
+
flipping these while the repo is private just breaks GitHub again.
|
|
89
|
+
tests/test_readme_renders_on_github.py enforces the relative form and that
|
|
90
|
+
every relative target exists. -->
|
|
91
|
+
|
|
92
|
+
# <img src="docs/assets/flywheel-spinner.gif" height="44" alt="" align="middle"> Agent Flywheel
|
|
93
|
+
|
|
94
|
+
Built and Maintained by **[PerceptEye](https://percepteye.ai)**
|
|
95
|
+
|
|
96
|
+
### **Your agent gets better at its own job. You don't run any of the training.**
|
|
97
|
+
|
|
98
|
+
Ship one line. Your agent keeps running in your process, behind your firewall, on your credentials — **unchanged** — and the model it calls keeps improving.
|
|
99
|
+
|
|
100
|
+
<!-- BADGES — `ci` was a live shields.io badge and rendered as a red error
|
|
101
|
+
image, because shields.io queries GitHub anonymously and this repo is
|
|
102
|
+
PRIVATE, so GitHub's API 404s for an anonymous caller whatever
|
|
103
|
+
.github/workflows/ci.yml — which does exist, and is pushed — actually did.
|
|
104
|
+
Verified against the live service 2026-09-06.
|
|
105
|
+
|
|
106
|
+
ONCE THE REPO IS PUBLIC restore it verbatim. Do not hand-write a
|
|
107
|
+
"passing" into a static badge to fill the gap — that asserts a state
|
|
108
|
+
nothing checks.
|
|
109
|
+
[](https://github.com/percepteye-ai/agent-flywheel/actions)
|
|
110
|
+
`pypi` is live and asks PyPI for the published version of
|
|
111
|
+
`agent-flywheel`. PyPI answers anonymous callers, so unlike `ci` it only
|
|
112
|
+
reads "not found" until the first release is out. This README is also
|
|
113
|
+
that release's PyPI page, so it must not say "not published" anywhere.
|
|
114
|
+
`ci` gets no static stand-in: the hardcoded `tests` badge below already
|
|
115
|
+
carries what a reader wanted from it, and a README cannot assert its own
|
|
116
|
+
pipeline state. That badge used to be hardcoded with nothing checking it,
|
|
117
|
+
and it drifted: it read 695 while the suite was 749, and the Development
|
|
118
|
+
section below said 695 while the docs site said 509. It is CHECKED now.
|
|
119
|
+
tests/test_documented_counts_are_true.py re-derives the suite size and
|
|
120
|
+
requires every published copy to match it, here and on the docs site, so
|
|
121
|
+
editing one copy alone turns the suite red. Re-derive by hand with:
|
|
122
|
+
.venv/bin/python -m pytest tests/ --collect-only -q -p no:warnings \
|
|
123
|
+
| awk -F': ' '/^tests\/.*: [0-9]+$/{n+=$2} END{print n}' # -> 982 -->
|
|
124
|
+
[](https://pypi.org/project/agent-flywheel/)
|
|
125
|
+
[](pyproject.toml)
|
|
126
|
+
<br/>
|
|
127
|
+
[](pyproject.toml)
|
|
128
|
+
[](#-zero-instrumentation-concretely)
|
|
129
|
+
[](#-development)
|
|
130
|
+
[](LICENSE)
|
|
131
|
+
|
|
132
|
+
<img src="docs/assets/flywheel-overview.gif" width="1040"
|
|
133
|
+
alt="Your agent — prompt, tools and model — sits inside your infrastructure. fw.serve() wraps it from the outside with zero lines changed inside. Rollouts and observed outcomes go out; a trained policy comes back. The model row flips from your-model to a policy trained on this agent's own work, while the prompt and tools rows stay untouched.">
|
|
134
|
+
|
|
135
|
+
**[Start in one call](#-start-in-one-call)** · [Harnesses](#-harnesses-supported) · [How the loop closes](#-how-the-loop-closes) · [Training vs production](#-two-modes-one-image) · [Production capture](#-production-observability) · [Docs](website/docs/)
|
|
136
|
+
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## <img src="docs/assets/flywheel-spinner.gif" height="26" alt="" align="middle"> A continual-learning loop around the agent you already have
|
|
142
|
+
|
|
143
|
+
Your agent already does real work. **This turns that work into training data, and delivers what it learns back as an endpoint your agent already calls.**
|
|
144
|
+
|
|
145
|
+
That is the whole product. Not a dashboard you read, not a benchmark you run — a loop that closes on its own and leaves your agent measurably better than the one you deployed.
|
|
146
|
+
|
|
147
|
+
| you get | what it actually means |
|
|
148
|
+
|:--|:--|
|
|
149
|
+
| 📈 **An agent that improves in place** | a policy trained on *this* agent's own trajectories — its prompt, its tools, its failure modes — not a generic model |
|
|
150
|
+
| 🤖 **Post-training you never operate** | task generation, grading, reinforcement learning, promotion gates: run by the control plane, on our GPUs |
|
|
151
|
+
| 🧠 **A prompt that improves too** | training co-optimizes the system prompt and ships the approved one back beside the model |
|
|
152
|
+
| 🔭 **Honest evidence** | every tool call is recorded as `ok`, `failed`, or `unknown` — the third is legal, expected, and never rounded up to `ok`, so a verifier abstains instead of learning from a lie |
|
|
153
|
+
| 🔒 **Nothing moves** | no container, no cluster, no GPU, no inbound port; every connection is dialled out from your side |
|
|
154
|
+
|
|
155
|
+
**And you can watch every lap.** The control plane is the part you don't build — not a part you can't see. Sign in and the tasks it generated, how each rollout scored, and what got promoted are all there to inspect.
|
|
156
|
+
|
|
157
|
+
## ⚡ Start in one call
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pip install agent-flywheel # one runtime dependency: httpx
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
import agent_flywheel as fw
|
|
165
|
+
|
|
166
|
+
fw.serve("my_package.agent:build", agent_id="my-coding-agent", input_shape="text")
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**That is the integration.** You pass an importable `"module:attr"` factory so each rollout can rebuild your agent in a fresh child process. `serve()` claims rollouts, runs them, reports outcomes, and blocks until you stop it.
|
|
170
|
+
|
|
171
|
+
It needs two things from the environment — there is no baked-in address:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
export PERCEPTEYE_CONTROL_PLANE_URL=https://launch.percepteye.ai/api/flywheel/v1
|
|
175
|
+
export PERCEPTEYE_API_KEY=pefw_... # issued by whoever operates your control plane
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Either one missing and `serve()` raises before the first rollout, naming it.
|
|
179
|
+
|
|
180
|
+
Everything that can be misconfigured — entrypoint, credential, contract version, isolation mode — is checked **before the first rollout** and raises there, never mid-run.
|
|
181
|
+
|
|
182
|
+
<!-- SIGNUP ROUTE STILL NEEDED — owner action. There is no self-service signup,
|
|
183
|
+
no key-request form and no "request access" endpoint anywhere in either SDK
|
|
184
|
+
repo; grep for one before assuming otherwise. support@percepteye.ai below is
|
|
185
|
+
the only non-security address either repo contains (the sibling Node README
|
|
186
|
+
is its single occurrence — it is uncorroborated by any code, config, DNS or
|
|
187
|
+
docs page here), and it is used because the Node README already gives it and
|
|
188
|
+
both SDKs must answer this question identically. Replace this paragraph with
|
|
189
|
+
the real route once it exists, in BOTH READMEs. Do not invent one, and do
|
|
190
|
+
not link a host that has not been resolved: docs.percepteye.ai was linked
|
|
191
|
+
here and does not resolve (checked 2026-09-06). -->
|
|
192
|
+
|
|
193
|
+
**Where a `pefw_` key comes from:** it is issued by whoever operates your PerceptEye control plane — ask them, or write to **support@percepteye.ai**. There is no self-service page to sign up on, and this README deliberately does not point you at one.
|
|
194
|
+
|
|
195
|
+
> [!TIP]
|
|
196
|
+
> Rollouts are real runs on your real credentials. Bound the first one with `max_rollouts=8` and point `artifacts_dir=` somewhere you don't mind it writing. `max_rollouts` is also the only clean exit: there is no signal handler, and Ctrl-C drains in-flight rollouts first.
|
|
197
|
+
|
|
198
|
+
### Your agent is a binary? Pass its command line.
|
|
199
|
+
|
|
200
|
+
Any CLI agent is an argv list — no protocol to implement, no change to the binary:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
fw.serve(["my-agent", "run"], agent_id="my-cli-agent", input_shape="text")
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The task arrives on **stdin** and as `PERCEPTEYE_TURN_INPUT`; ordinary **stdout** is the final text. Structured JSONL is interpreted only when you pass an explicit `command_output_adapter`, so core never guesses a customer's event vocabulary. Mark an argv position with `fw.TASK` if your CLI takes the task as an argument.
|
|
207
|
+
|
|
208
|
+
## 🧩 Harnesses supported
|
|
209
|
+
|
|
210
|
+
| harness | surface | integration |
|
|
211
|
+
|:--|:--|:--|
|
|
212
|
+
| **LangChain** · **LangGraph** · **Google ADK** | 🐍 Python | adapter auto-binds — **zero code in your agent** |
|
|
213
|
+
| **any CLI agent** | ⌘ CLI | `fw.serve(["my-agent", "run"], ...)` — nothing added to the binary |
|
|
214
|
+
| **any structured CLI** | ⌘ CLI | provide a `CommandOutputAdapter`; the generic `StreamFormat` parser can reconcile tool lifecycles |
|
|
215
|
+
| **prime-agent** *(optional example)* | ⌘ CLI | argv + explicit `PrimeAgentOutputAdapter` — **[worked example →](examples/prime_agent/)** |
|
|
216
|
+
|
|
217
|
+
Adapters are on by default (`adapters="auto"`) and bind every built-in whose framework is installed. No adapter imports its framework until you bind it, so they add **zero dependencies**. Shipping your own is one entry-point factory in the `agent_flywheel.adapters` group.
|
|
218
|
+
|
|
219
|
+
The command adapter is explicit and optional. Capture from it survives clean exits, non-zero exits and timeout kills — a rollout that died is exactly the one you need the tool calls from. With no adapter, structured stdout remains ordinary stdout and makes no telemetry or model-call-count claim.
|
|
220
|
+
|
|
221
|
+
*Running OpenClaw or DeepSeek Harness?* Those are the Node plugin → [`percepteye-ai/agent-flywheel-node`](https://github.com/percepteye-ai/agent-flywheel-node).
|
|
222
|
+
|
|
223
|
+
## 🔁 How the loop closes
|
|
224
|
+
|
|
225
|
+
<p align="center">
|
|
226
|
+
<img src="docs/assets/flywheel-loop.gif" width="980"
|
|
227
|
+
alt="The SDK integrating into a customer's agent and talking to the control plane. On the left, inside your infrastructure: pip install, the harness, your agent unchanged, and its trajectory. A dashed trust boundary runs down the middle. On the right, the control plane generates tasks from your tool surface, scores every rollout, trains the policy, and hands a candidate back as an endpoint you can call.">
|
|
228
|
+
</p>
|
|
229
|
+
|
|
230
|
+
**Register once, then loop forever.** Every lane crosses the trust boundary **outbound, opened from your side**.
|
|
231
|
+
|
|
232
|
+
| beat | what happens | who does the work |
|
|
233
|
+
|:--|:--|:--|
|
|
234
|
+
| **⓪ Onboard** | the SDK describes your agent from inside its own process and registers it — once, before the first rollout | automatic; `introspect=False` opts out |
|
|
235
|
+
| **① Claim** | your process long-polls for a rollout; leases heartbeat, crashed workers redeliver | you dial out |
|
|
236
|
+
| **② Run** | fresh child process, own environment, single-use credential, own trajectory | your agent, unchanged |
|
|
237
|
+
| **③ Record** | every tool call lands as `ok` / `failed` / `unknown`; artifacts collected on every path | adapters, from the outside |
|
|
238
|
+
| **④ Score & train** | tasks generated from your real tool surface, graded against compiled checks, then trained | **PerceptEye — you build none of it** |
|
|
239
|
+
| **⑤ Improve** | your production agent picks up the promoted policy; the next lap runs on the better agent | one call at startup |
|
|
240
|
+
|
|
241
|
+
That last beat is what makes it a flywheel rather than a pipeline: **the better agent generates the better evidence.**
|
|
242
|
+
|
|
243
|
+
Beat ⓪ is why you never write a task list. Tasks come from **the tool surface you actually have** — read after templating and conditional registration, so it is the prompt your agent really assembled, the tools it really registered, the sub-agents it really built. A CLI agent with no object to read is warmed up once, well under a second, and described from that.
|
|
244
|
+
|
|
245
|
+
## 🎚 Two modes, one image
|
|
246
|
+
|
|
247
|
+
One variable decides whether a deployment does training work. The same container does either, with no code edit.
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
PERCEPTEYE_AGENT_MODE=training # the default when unset
|
|
251
|
+
PERCEPTEYE_AGENT_MODE=production
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
| | `training` | `production` |
|
|
255
|
+
|:--|:--|:--|
|
|
256
|
+
| **pick it for** | a machine you're happy to lend to rollouts | the deployment serving your users |
|
|
257
|
+
| `fw.serve(...)` | claims and runs rollouts | returns `0` immediately, **before opening any connection** |
|
|
258
|
+
| needs a URL and key | yes | no |
|
|
259
|
+
|
|
260
|
+
An unrecognised value **raises** rather than guessing: a typo that silently picked `training` would keep spending your credentials with nothing printed. (`train`, `rollout`, `fine-tuning` also mean contribute; `prod` and `serving` mean don't. `mode=` in code beats the variable, and the printed line says when that happened.)
|
|
261
|
+
|
|
262
|
+
> [!IMPORTANT]
|
|
263
|
+
> The switch gates **`serve()`** — the call that does training work. It does not gate `attach()` — that *is* the production surface. Gating both would mean the correct setting for a production agent was `training`.
|
|
264
|
+
|
|
265
|
+
**Contribute nothing, still get the trained model.** No `serve()`, no `attach()`, no registration:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
kw = fw.apply_current_policy(agent_id="my-agent")
|
|
269
|
+
if kw: # {} means keep your current config
|
|
270
|
+
client = OpenAI(base_url=kw["base_url"], api_key=kw["api_key"])
|
|
271
|
+
model = kw["model"]
|
|
272
|
+
|
|
273
|
+
served = fw.current_prompt(agent_id="my-agent") # the approved prompt
|
|
274
|
+
decision = fw.prompt_decision(served) # may I apply it?
|
|
275
|
+
if decision.apply:
|
|
276
|
+
system_prompt = decision.text
|
|
277
|
+
else:
|
|
278
|
+
log.info("keeping the current prompt: %s", decision.reason)
|
|
279
|
+
|
|
280
|
+
src = fw.policy_source(agent_id="my-agent", on_change=reconfigure)
|
|
281
|
+
snap = src.current() # per turn; between refreshes it's a cache read
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Nothing polls in the background.** The refresh runs inside your call — no timer, no poller — so a promotion *or a rollback* reaches production with no redeploy. After its first success `policy_source()` keeps serving the last good policy, so a network blip can never silently demote production back to the untrained model.
|
|
285
|
+
|
|
286
|
+
`prompt_decision()` is the whole rule in one call, and the SDK never applies a prompt for you: it checks that a prompt is actually being served, that your own baseline prompt has not changed since the pair was certified, that the bundle's status is one a person approved, and that the prompt's identity matches the checkpoint it was certified against. `apply` is false unless all four hold, `reason` says which one failed, and `text` is `None` rather than `""` — so a refusal can never blank the instructions your agent is already running on. The Node SDK asks the identical question, and [`tests/test_prompt_decision_parity.py`](tests/test_prompt_decision_parity.py) drives one table of cases through both implementations and asserts every field of the answer agrees.
|
|
287
|
+
|
|
288
|
+
## ✋ Nothing reaches your users until a person approves it
|
|
289
|
+
|
|
290
|
+
**Both halves of an improvement — the model and the prompt — require a human decision.** Not a threshold, not a scheduler, not a language model reading a report and deciding it looks good.
|
|
291
|
+
|
|
292
|
+
**The model.** Your agent keeps serving the model it is on today — the **champion** — until *all three* of these are true, and every one of them fails closed back to it:
|
|
293
|
+
|
|
294
|
+
1. **A person with the operator role moves that agent onto the candidate**, from the dashboard. It is an explicit, role-gated call on that one agent. No promotion applies it, no percentage rollout applies it, and there is no automatic path to it — moving *back* is the same call with the other value, which is what makes rollback fast enough to use during an incident.
|
|
295
|
+
2. **The candidate's certification did not go against it.** A verdict of *regressed*, *inconclusive* or *escalated* makes it unservable — the same set the promotion gate itself refuses on, so a candidate your agent could be moved onto is one that gate would also have accepted.
|
|
296
|
+
3. **It is genuinely deployed somewhere your agent can call.** An address that is a stored artifact rather than a running endpoint — a bucket or file path — is refused, so an approved candidate that nobody has served yet keeps the champion in place instead of pointing your agent at something that answers nothing. **The SDK re-checks this itself** rather than trusting the answer: `endpoint_kwargs()` — the one place a served policy becomes client arguments, and what both `apply_current_policy()` and `policy_source()` go through — returns `{}` and logs why, on the same prefix set the Node SDK refuses on. [`tests/test_served_model_is_servable.py`](tests/test_served_model_is_servable.py) drives both implementations and asserts the two agree.
|
|
297
|
+
|
|
298
|
+
**The prompt** is a separate chain with its own approval: the control plane serves a prompt only from a delivery a person approved, and `prompt_decision()` above re-checks that status on your side before anything is applied.
|
|
299
|
+
|
|
300
|
+
**And no language model can grant either approval.** Promoting a model, delivering a prompt pair, and promoting a research result are all marked irreversible in the approval system: only the explicit approve or reject control can resolve them. A model reading free-form text and interpreting "sure, go ahead" as consent is recorded as *answered* and never as *approved*, so it cannot promote, deliver, or roll anything out.
|
|
301
|
+
|
|
302
|
+
## 🔭 Production observability
|
|
303
|
+
|
|
304
|
+
`attach()` records the turns you are **already serving** — same adapters, same tri-state outcomes, same trajectory format as a rollout.
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
px = fw.attach(agent_id="support-agent", trajectory_root="/var/lib/percepteye")
|
|
308
|
+
|
|
309
|
+
with px.turn(task_id=case_id, conversation_id=conv_id,
|
|
310
|
+
input_text=user_text) as turn_id:
|
|
311
|
+
reply = agent.invoke(user_text) # your agent, unmodified
|
|
312
|
+
|
|
313
|
+
px.record_answer(turn_id, reply) # after the scope closes — the answer exists now
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**It captures. It never reroutes.** Adapters bind with no endpoint at all, so attaching cannot re-point a single byte of production traffic. Everything fails open: an unreachable control plane costs you the observation and nothing else.
|
|
317
|
+
|
|
318
|
+
For identity-bound production analysis, pass the same live Python agent as
|
|
319
|
+
`execution_entrypoint=` together with adapter-authored
|
|
320
|
+
`attested_execution_components`. The SDK emits
|
|
321
|
+
`agent_fingerprint.execution_sha256` only when its prompt/tools/model/sub-agent
|
|
322
|
+
discovery is exact and the opaque component evidence is valid; otherwise the
|
|
323
|
+
field stays absent. It is persisted per turn and projected to OTel as
|
|
324
|
+
`percepteye.agent.execution_sha256`. `attach()` does not claim a policy identity:
|
|
325
|
+
the policy it reads is informational and does not prove what your application ran.
|
|
326
|
+
|
|
327
|
+
`task_id=` is optional, opaque correlation evidence for your own case, job, or
|
|
328
|
+
work item. The SDK persists it, includes it in the turn upload, and projects it
|
|
329
|
+
as `percepteye.task.id`; it does not interpret the value or generate advice.
|
|
330
|
+
Supplying it grants no training eligibility. Control-plane clustering and
|
|
331
|
+
dataset services validate any later trace-to-task binding.
|
|
332
|
+
|
|
333
|
+
> [!IMPORTANT]
|
|
334
|
+
> **Upload is opt-in per agent and off by default.** A production turn is your end user's message and the answer you gave them, so nothing is sent until an operator enables capture for that agent. Until then turns accumulate as trajectory directories under the `trajectory_root` you named and go nowhere.
|
|
335
|
+
>
|
|
336
|
+
> **No restart needed to enable it.** While the answer is no, the SDK re-reads the verdict on its flush cadence and starts uploading as soon as an operator says yes. `px.may_upload` is the live answer; `px.upload_refusal` is the sentence explaining a `False`.
|
|
337
|
+
|
|
338
|
+
`input_text=` and `record_answer()` are the two lines that carry text, and both are yours to pass — adapters observe tool calls, not turn content. Once capture is on, uploading is automatic: answered turns go every 32 turns on a short-lived daemon thread, and an `atexit` hook sends the rest. Uploads never raise into your process and never block a turn on the network.
|
|
339
|
+
|
|
340
|
+
Production turns are **graded advisory, always** — they were produced by whatever you run, so they inform training rather than being scored as rollouts are. Training capacity still comes from `serve()`.
|
|
341
|
+
|
|
342
|
+
## 🧪 Zero instrumentation, concretely
|
|
343
|
+
|
|
344
|
+
This LangChain agent contains no reference to Agent Flywheel of any kind:
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
# lc_agent.py — your existing agent, untouched
|
|
348
|
+
from langchain_core.tools import tool
|
|
349
|
+
|
|
350
|
+
@tool
|
|
351
|
+
def lookup(q: str) -> str:
|
|
352
|
+
"""Look something up."""
|
|
353
|
+
return f"found: {q}"
|
|
354
|
+
|
|
355
|
+
class Agent:
|
|
356
|
+
tools = [lookup]
|
|
357
|
+
def invoke(self, task: str) -> str:
|
|
358
|
+
return str(lookup.invoke({"q": task}))
|
|
359
|
+
|
|
360
|
+
def build():
|
|
361
|
+
return Agent()
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Served with the two-line `fw.serve()` integration from [Start in one call](#-start-in-one-call), the rollout reports:
|
|
365
|
+
|
|
366
|
+
```json
|
|
367
|
+
"tool_calls": [{"name": "lookup", "arguments": {"q": "count to three"},
|
|
368
|
+
"outcome": "unknown", "status_code": null,
|
|
369
|
+
"output": "found: count to three", "latency_ms": 0.199}]
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Not one `agent_flywheel` import produced that record**, and it is enforced by [`tests/test_zero_instrumentation.py`](tests/test_zero_instrumentation.py). One `record_tool_call` where you *do* know the answer gets you the rest:
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
from agent_flywheel import record_tool_call
|
|
376
|
+
|
|
377
|
+
record_tool_call("charge_card", {"amount": 4200}, outcome="ok", status_code=201)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## ⚙️ Configuration
|
|
381
|
+
|
|
382
|
+
| variable | meaning |
|
|
383
|
+
|:--|:--|
|
|
384
|
+
| `PERCEPTEYE_CONTROL_PLANE_URL` | control-plane base URL. **No default** — unset means nothing is claimed and no trained policy is read, and every call that needed it now says so |
|
|
385
|
+
| `PERCEPTEYE_API_KEY` | your flywheel key |
|
|
386
|
+
| `PERCEPTEYE_AGENT_MODE` | `training` (default) or `production` — [one switch, both ways](#-two-modes-one-image) |
|
|
387
|
+
| `PERCEPTEYE_INTROSPECT` | `0` disables agent discovery |
|
|
388
|
+
| `PERCEPTEYE_TRAJECTORY_DIR` | one explicit trajectory directory, for a single run |
|
|
389
|
+
|
|
390
|
+
Two of them change behaviour; the rest are an address, a credential and a location, and the first two are overridable in code as `control_plane_url=` and `api_key=`. `serve()` also takes `isolation` (`"subprocess"` is the default and the only mode with a real timeout), `concurrency`, `method`, `rollout_timeout_s`, `execution_snapshot` (a generic adapter-owned prepare/attest/release lifecycle), `attested_execution_components` (opaque adapter-owned SHA-256 evidence for exact OPSD execution identity), and `on_event` for a synchronous event callback — `claim_empty` versus `claim_failed` is how you tell an empty queue from a broken route.
|
|
391
|
+
|
|
392
|
+
**Your agent must sample through an OpenAI-compatible client**, because the per-rollout gateway speaks that API. Provider variables we cannot point at it are stripped from the child, and `serve()` warns at startup if it finds any.
|
|
393
|
+
|
|
394
|
+
<details>
|
|
395
|
+
<summary><b>Agent discovery</b> — what is sent, and how to turn it off</summary>
|
|
396
|
+
|
|
397
|
+
<br/>
|
|
398
|
+
|
|
399
|
+
Once at startup the SDK reads the *shape* of the agent you handed `serve()`: system prompt (the registration projection is truncated at 20k, while an available full observation is hashed before projection), tool names/descriptions/JSON Schemas, provider and model name, sub-agents to depth 4, and the framework matched. **Never sent:** your tools' source or return values, your credentials, your data, or anything observed during a rollout. Turn it off with `fw.serve(..., introspect=False)` or `PERCEPTEYE_INTROSPECT=0` — and only a deliberate value (`0`, `false`, `no`, `off`) disables it, so a typo leaves it on. Disabled, unreadable, or lossy discovery produces no affirmative `agent_fingerprint.execution_sha256`, so an OPSD consumer fails closed.
|
|
400
|
+
</details>
|
|
401
|
+
|
|
402
|
+
<details>
|
|
403
|
+
<summary><b>OpenTelemetry</b> — optional, egress only</summary>
|
|
404
|
+
|
|
405
|
+
<br/>
|
|
406
|
+
|
|
407
|
+
`pip install "agent-flywheel[otel]"` and every outcome is additionally emitted as a span, so it lands in whatever you already run. The extra is the **API only, never the SDK**: this package emits into your registered `TracerProvider` and never installs one. Spans carry the GenAI semantic conventions plus `percepteye.tool.outcome`, and an `unknown` leaves the span status unset.
|
|
408
|
+
</details>
|
|
409
|
+
|
|
410
|
+
## 🛠 Development
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
python -m venv .venv
|
|
414
|
+
.venv/bin/pip install -e ".[dev,adapters-test,otel,otel-test]"
|
|
415
|
+
.venv/bin/pytest
|
|
416
|
+
./scripts/cross_language_abi_check.sh # the cross-language contract, both ways
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The last one is **not** the Node package's own suite — that lives in the [Node repository](https://github.com/percepteye-ai/agent-flywheel-node) and runs there with `npm test`. This drives records through the Node writer and asserts the Python `Trajectory` reads them back with the same meaning, which is the half only this repo can check. **It needs a Node checkout**: point `PERCEPTEYE_NODE_SDK` at one, or keep it as a sibling directory. Without one it prints `SKIPPED` and exits 0, and the cross-language tests in the suite above skip with it.
|
|
420
|
+
|
|
421
|
+
982 tests, and CI permits **zero skips** — a skipped test and a passing test look identical in a green check mark. CI names `PERCEPTEYE_NODE_SDK`, so the cross-language half cannot go green by skipping there. Other jobs assert that a bare install still works on Python 3.10→3.13, that the runtime dependency list never grows past `httpx`, and that the Node plugins never drift from the Python reader.
|
|
422
|
+
|
|
423
|
+
[`schema/flywheel-1.json`](schema/flywheel-1.json) is the wire contract and this repository owns it. Absence is never collapsed into zero: `tool_calls=None` means "I did not report tool calls"; `[]` means "I observed zero", a positive claim. The animations above are generated, not drawn — [`docs/assets/render.sh`](docs/assets/render.sh) rebuilds them.
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
**Contributing** — issues and PRs welcome; a new adapter must not import its framework at module scope, must report `unknown` rather than guess, and must fail loudly at bind time. ·
|
|
428
|
+
**Security** — please report vulnerabilities privately to **security@percepteye.ai**. ·
|
|
429
|
+
**License** — [MIT](LICENSE) © PerceptEye.
|