@percepteye/agent-flywheel 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +368 -0
- package/cordis.patch.yml +11 -0
- package/openclaw.plugin.json +134 -0
- package/package.json +67 -0
- package/schema/flywheel-1.json +432 -0
- package/src/capture.js +733 -0
- package/src/classify.js +115 -0
- package/src/config.js +249 -0
- package/src/describe.js +355 -0
- package/src/dsh-classify.js +110 -0
- package/src/dsh.js +130 -0
- package/src/errors.js +37 -0
- package/src/evidence.js +109 -0
- package/src/execution-identity.js +444 -0
- package/src/host.js +63 -0
- package/src/http.js +249 -0
- package/src/index.js +380 -0
- package/src/mode.js +152 -0
- package/src/model-calls.js +214 -0
- package/src/policy.js +934 -0
- package/src/record.js +83 -0
- package/src/rollout.js +884 -0
- package/src/scope.js +242 -0
- package/src/session.js +42 -0
- package/src/trajectory.js +148 -0
- package/src/transport.js +403 -0
- package/src/turns.js +437 -0
- package/src/unattended.js +251 -0
- package/src/wire.js +182 -0
package/LICENSE
ADDED
|
@@ -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.
|
package/README.md
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<!-- FIGURE PATHS ARE RELATIVE, DELIBERATELY — and this is the second time it
|
|
4
|
+
has been settled. They render on GitHub, which is what these GIFs are for.
|
|
5
|
+
Absolute https://raw.githubusercontent.com/... URLs were tried, on the
|
|
6
|
+
reasoning that a registry page cannot resolve a relative path; that broke
|
|
7
|
+
all three figures on github.com instead, because raw.githubusercontent.com
|
|
8
|
+
serves a PRIVATE repo's file to nobody unauthenticated — every reader,
|
|
9
|
+
the owner included. Verified 2026-09-06: all three returned HTTP 404.
|
|
10
|
+
|
|
11
|
+
AND THERE IS NO TRADE-OFF LEFT, so nobody has to rediscover it a third
|
|
12
|
+
time: "a registry page cannot resolve a relative path" is wrong for npm,
|
|
13
|
+
which rewrites each relative <img> src to raw.githubusercontent.com on
|
|
14
|
+
the default branch (seen on the live package page, 2026-09-14). These
|
|
15
|
+
paths render on GitHub now, and on npm the day the repo goes public —
|
|
16
|
+
with no edit here and no new release.
|
|
17
|
+
|
|
18
|
+
Held by a test, not by this comment: `test/oss-boundary.test.js` asserts every
|
|
19
|
+
<img> src is relative AND names a file that exists. Shields badges are
|
|
20
|
+
markdown ``, not <img>, and are correctly absolute. -->
|
|
21
|
+
|
|
22
|
+
# <img src="docs/assets/flywheel-spinner.gif" height="44" alt="" align="absmiddle"> Agent Flywheel — Node
|
|
23
|
+
|
|
24
|
+
Built and Maintained by **[PerceptEye](https://percepteye.ai)**
|
|
25
|
+
|
|
26
|
+
### **Your agent gets better at its own job, on its own work, without you writing a training pipeline.**
|
|
27
|
+
|
|
28
|
+
**It keeps running in your harness, behind your firewall, on your credentials — unchanged.**<br/>
|
|
29
|
+
One plugin. Zero runtime dependencies. Zero lines changed in your agent.
|
|
30
|
+
|
|
31
|
+
<!-- BADGES — `npm` is the only live-data badge in this README: it reads the
|
|
32
|
+
registry, so it cannot claim a release that never happened. Never swap it
|
|
33
|
+
for a static badge with a hand-written version — that asserts a release
|
|
34
|
+
nothing checks. Every other badge here is a hardcoded shields `/badge/`.
|
|
35
|
+
There is no `ci` badge, and none should be added while this repo is
|
|
36
|
+
private: shields.io queries GitHub anonymously and a private repo 404s
|
|
37
|
+
regardless of what CI did.
|
|
38
|
+
|
|
39
|
+
Separately: the `tests` badge below is hardcoded and nothing checks it;
|
|
40
|
+
its 434 was true on 2026-09-14 (it had drifted to 373 while the suite
|
|
41
|
+
grew). Re-derive with `npm test` (prints "tests 434" / "skipped 0")
|
|
42
|
+
before trusting it — the Python SDK's equivalent badge had drifted to 54
|
|
43
|
+
short. -->
|
|
44
|
+
[](https://www.npmjs.com/package/@percepteye/agent-flywheel)
|
|
45
|
+
[](package.json)
|
|
46
|
+
[](#-why-it-earns-its-place)
|
|
47
|
+
[](#-install-in-one-call)
|
|
48
|
+
[](#-develop)
|
|
49
|
+
[](LICENSE)
|
|
50
|
+
|
|
51
|
+
<img src="docs/assets/flywheel-overview.gif"
|
|
52
|
+
width="1040"
|
|
53
|
+
alt="Your agent — prompt, tools and model — sits inside your infrastructure. The plugin loads beside it in your harness with zero lines changed inside. Rollouts and observed outcomes go out; a trained policy comes back. Both the system prompt row and the model row flip to what the control plane approved for this agent, while the tools row stays untouched.">
|
|
54
|
+
|
|
55
|
+
**[Install](#-install-in-one-call)** · [How the loop closes](#-how-the-loop-closes) · [Harnesses](#-harnesses) · [Modes](#-two-modes-one-switch) · [Production observability](#-production-observability) · [Human approval](#-a-person-approves-every-change) · [Configuration](#-configuration) · [Python or Node?](#-python-or-node)
|
|
56
|
+
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
**Your agent already does real work. This turns that work into training data, and hands back a model trained on it.**
|
|
62
|
+
|
|
63
|
+
No container to build. No cluster to run. No GPU to provision. No inbound port to open. No labelling team. No eval suite to write. Your agent does not move — a plugin loads beside it, watches what its tools actually did, and feeds a control plane that does the post-training for you.
|
|
64
|
+
|
|
65
|
+
<br/>
|
|
66
|
+
|
|
67
|
+
## 🎯 What you get
|
|
68
|
+
|
|
69
|
+
| | |
|
|
70
|
+
|:--|:--|
|
|
71
|
+
| 🔁 **A continual-learning loop** | your agent runs graded tasks, its real outcomes go back, and the next model is trained on *this agent's* work — not a generic benchmark |
|
|
72
|
+
| 🤖 **Autonomous post-training** | task generation, grading, dataset construction, training and promotion happen on our side. You build none of it |
|
|
73
|
+
| 🧪 **Honest discovery** | the plugin reads the assembled prompt and effective model; current OpenClaw does not expose tools on `llm_input`, so the catalogue stays explicitly incomplete instead of being invented |
|
|
74
|
+
| 📈 **Honest telemetry on day one** | your agent's true tool-failure rate, recorded locally, with **no account and no network** |
|
|
75
|
+
| 📡 **Production observability** | capture the turns you actually serve — the user's message, the tool calls, the answer — and turn live traffic into evidence |
|
|
76
|
+
| ✅ **The approved pair, applied** | training co-optimizes a system prompt alongside the model. Once *a person on your side approves each*, your production install runs **both** — the prompt, and the checkpoint it was certified with. No redeploy; one switch each to stop |
|
|
77
|
+
| 🙋 **A person approves every change** | nothing reaches your agent that a human with the operator role did not approve through an explicit control. Not a percentage rollout, not an automated promotion, and never a language model reading "sure, go ahead". [How that is enforced](#-a-person-approves-every-change) |
|
|
78
|
+
| 🛡️ **It never touches a tool result** | the recording hook returns `void`. It can never change, block, delay or replace what a tool returns. The two things this plugin *does* change are the system prompt and the model — production mode only, approved bundles only, this agent's own turns only, and off with [`PERCEPTEYE_APPLY_PROMPT=0` / `PERCEPTEYE_APPLY_MODEL=0`](#-two-modes-one-switch) |
|
|
79
|
+
|
|
80
|
+
<br/>
|
|
81
|
+
|
|
82
|
+
## ⚡ Install in one call
|
|
83
|
+
|
|
84
|
+
One package, two harnesses. Pick your line:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
openclaw plugins install @percepteye/agent-flywheel # OpenClaw
|
|
88
|
+
dsh plugin --profile <name> add @percepteye/agent-flywheel # DeepSeek Harness
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**That's the integration.** No import, no wrapper process, no sidecar. Start your agent exactly the way you always did.
|
|
92
|
+
|
|
93
|
+
Want it to start contributing? Add a key and one host setting:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
export PERCEPTEYE_API_KEY=pefw_... # optional, and free at https://launch.percepteye.ai
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Where a `pefw_` key comes from:** PerceptEye runs the control plane, and signing in is free and self-serve at **[launch.percepteye.ai](https://launch.percepteye.ai)**. Connect your agent there and you get its key — shown once, and scoped to that one agent. Without a key nothing leaves the machine: local recording of your agent's real tool-failure rate works without one, forever.
|
|
100
|
+
|
|
101
|
+
<!-- THE KEY ROUTE, confirmed by the owner 2026-09-14: PerceptEye operates the
|
|
102
|
+
control plane, and sign-in is free and self-serve at
|
|
103
|
+
https://launch.percepteye.ai (HTTP 200, "Mission Control · PerceptEye").
|
|
104
|
+
Connecting an agent there issues its key -- shown once, bound to that
|
|
105
|
+
agent. The Python README says the same; keep the two in step. -->
|
|
106
|
+
|
|
107
|
+
```jsonc
|
|
108
|
+
// your OpenClaw config
|
|
109
|
+
{ "plugins": { "entries": { "agent-flywheel": {
|
|
110
|
+
"hooks": { "allowConversationAccess": true } } } } }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
That setting does **three** jobs, and every one of them is dropped without it. It lets the plugin read your agent's answer (`agent_end`), describe your agent (`llm_input`), and **point your agent at the approved model** (`before_model_resolve`) — OpenClaw classes all three as conversation hooks and drops them for non-bundled plugins unless you opt in. The plugin **refuses to claim work without it** rather than half-working, and says once, at startup, which capability it is missing and why. Applying the approved *prompt* is the one thing that does **not** need it.
|
|
114
|
+
|
|
115
|
+
> [!TIP]
|
|
116
|
+
> Scalar settings have both an OpenClaw config key and an environment variable, and config wins over the environment. Exact execution evidence is a nested, config-only adapter snapshot. The full table is under [Configuration](#-configuration).
|
|
117
|
+
|
|
118
|
+
> [!NOTE]
|
|
119
|
+
> **Set `agentId` to the OpenClaw agent this install serves** if your host runs more than one. That name is how the control plane knows the agent *and* how the plugin decides whose approved prompt and model to apply. On a host running a single agent — the ordinary install — there is nothing to disambiguate and you can leave it alone. On a host running several with none of them named, the plugin applies **nothing** and says so at startup, because guessing would swap a bystander's prompt.
|
|
120
|
+
|
|
121
|
+
<br/>
|
|
122
|
+
|
|
123
|
+
## 🔁 How the loop closes
|
|
124
|
+
|
|
125
|
+
<p align="center">
|
|
126
|
+
<img src="docs/assets/flywheel-loop.gif" width="980"
|
|
127
|
+
alt="The SDK inside the customer's agent, talking to the control plane. On the left, inside your infrastructure: install the plugin, your harness loads it, your agent runs unchanged, and its trajectory is recorded. A dashed trust boundary splits the picture. On the right, the control plane: it generates tasks from your tool surface, scores every rollout, trains the policy, and hands a candidate back as an endpoint you can call. A return arrow closes the ring — the better agent runs the next lap. The figure predates delivery: both halves — the approved system prompt and the model it was certified with — now return all the way into the agent, once a person has approved each.">
|
|
128
|
+
</p>
|
|
129
|
+
|
|
130
|
+
| beat | what happens | who does the work |
|
|
131
|
+
|:--|:--|:--|
|
|
132
|
+
| **⓪ Describe** | the plugin records the assembled prompt and effective model; tools remain `null` on the current host API unless an adapter supplies them | automatic, honestly incomplete |
|
|
133
|
+
| **① Claim** | training mode registers this exact worker, then claims one graded task at a time; an exact startup snapshot binds claims to that execution | you dial out — no inbound port |
|
|
134
|
+
| **② Run** | the task is delivered into your agent's context, in a dedicated `percepteye-rollout-*` session | your agent, unchanged |
|
|
135
|
+
| **③ Record** | every executed tool call lands as `ok` / `failed` / **`unknown`** | the `after_tool_call` hook |
|
|
136
|
+
| **④ Report** | the trajectory and answer go back; unsupported evidence stays absent | the plugin |
|
|
137
|
+
| **⑤ Train** | tasks generated, rollouts graded, dataset built, policy trained and promoted | **PerceptEye — you build none of it** |
|
|
138
|
+
| **⑥ Serve** | the approved system prompt **and** the model it was certified with are applied to your production install automatically, once a person has approved each | the plugin |
|
|
139
|
+
|
|
140
|
+
That last beat is what makes it a flywheel rather than a pipeline: **the better agent generates the better evidence.**
|
|
141
|
+
|
|
142
|
+
> [!IMPORTANT]
|
|
143
|
+
> **Beat ⑥ closes the pair.** A production install reads `GET /policy/current` and `GET /prompt/current` once at startup and applies **both** halves: the prompt, by replacing the one your host assembled, and the model, by naming a provider this plugin registers whose transport points at the served `base_url`. That matters because they were certified *together* — a prompt applied over some other checkpoint is not the combination anybody evaluated, and the startup log says so plainly whenever only one half lands.
|
|
144
|
+
>
|
|
145
|
+
> The model half needs two things from your host, and says at startup if it is missing either: `hooks.allowConversationAccess: true`, and a host that accepts the provider registration — the registered provider is the only thing that can supply the served `base_url`, so if the id is refused the model half is refused with it rather than pointing your agent at a provider your host does not know. Separately, if the served endpoint requires a credential it comes from your own `models.providers.percepteye-flywheel.apiKey`.
|
|
146
|
+
|
|
147
|
+
<br/>
|
|
148
|
+
|
|
149
|
+
## 🧩 Harnesses
|
|
150
|
+
|
|
151
|
+
| harness | records outcomes | rollouts | production turn capture | applies the approved prompt | applies the approved model |
|
|
152
|
+
|:--|:--|:--|:--|:--|:--|
|
|
153
|
+
| **OpenClaw** | ✅ `after_tool_call` | ✅ training mode | ✅ | ✅ `before_prompt_build` | ✅ `before_model_resolve` + a registered provider (needs `allowConversationAccess`) |
|
|
154
|
+
| **DeepSeek Harness** | ✅ `tools/result` | — | — | — | — |
|
|
155
|
+
|
|
156
|
+
**Rollouts are the OpenClaw lane.** DeepSeek Harness exposes no seam for handing a task to a running agent, so on dsh this plugin records outcomes and nothing else, in every mode — it writes to `PERCEPTEYE_TRAJECTORY_DIR` and with that unset it writes nothing.
|
|
157
|
+
|
|
158
|
+
**This OpenClaw adapter does not claim `llm_call_count`.** The host's public `llm_input` / `llm_output` hooks bracket a whole attempt and expose no provider-call id; one attempt may contain tool loops, retries, or compaction and therefore several provider calls. Counting those hooks or assistant messages would be a guess. The plugin reports no count, and Mission Control excludes the rollout from verified on-policy training even when its gateway has its own count. A future host adapter may use the framework-neutral counter only when its host exposes independently identified per-call start and terminal evidence. DeepSeek Harness also makes no model-call-count claim.
|
|
159
|
+
|
|
160
|
+
**Every recording hook is structurally incapable of altering a run**: OpenClaw's `after_tool_call` returns `void`, and dsh's `tools/result` is `@mode emit` over deep-frozen objects. The tool-result mutation seams both harnesses expose — result middleware, `tools/post` — are deliberately unused, in every mode. A collector that can corrupt the run it is measuring is worse than no collector.
|
|
161
|
+
|
|
162
|
+
**The behavior-changing seams are separate from recording.** In production,
|
|
163
|
+
`before_prompt_build` returns the approved system prompt and
|
|
164
|
+
`before_model_resolve` returns the approved provider and model — beat ⑥ of the
|
|
165
|
+
loop rather than telemetry. In training, `before_agent_run` blocks a queued
|
|
166
|
+
turn if its lease expired before model submission, and `before_tool_call`
|
|
167
|
+
blocks any later side effects after an active lease is revoked. OpenClaw does
|
|
168
|
+
not expose an ordinary-plugin API for aborting an already-active model loop,
|
|
169
|
+
so that loop may wind down, but it cannot start another tool call or report a
|
|
170
|
+
stale result. None of these gates is registered by the production rollout
|
|
171
|
+
lane, because that lane does not exist in production mode.
|
|
172
|
+
|
|
173
|
+
**The registered provider answers for its own id and nothing else.** OpenClaw asks every registered provider plugin to normalise a transport when the matched one declines, so a provider that answered unconditionally would silently redirect your Anthropic or OpenAI traffic. This one returns nothing unless the provider being assembled is `percepteye-flywheel` — the id it registers, and the id it names in `before_model_resolve`.
|
|
174
|
+
|
|
175
|
+
<br/>
|
|
176
|
+
|
|
177
|
+
## 🎚 Two modes, one switch
|
|
178
|
+
|
|
179
|
+
Everything the plugin does beyond local recording is decided by `PERCEPTEYE_AGENT_MODE`.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
export PERCEPTEYE_AGENT_MODE=training # the default — claim rollouts and run them
|
|
183
|
+
export PERCEPTEYE_AGENT_MODE=production # for the process serving your users
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
| | `training` (default) | `production` |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| Records tool outcomes locally | yes | yes |
|
|
189
|
+
| Alters a tool result | **never** | **never** |
|
|
190
|
+
| Claims rollouts | yes | **no** |
|
|
191
|
+
| Puts a task into your agent's context | yes | **no** |
|
|
192
|
+
| Starts turns by itself | yes | **no** |
|
|
193
|
+
| Captures the turns you serve | n/a | yes |
|
|
194
|
+
| Uploads them | n/a | only once an operator enables it |
|
|
195
|
+
| **Applies your approved system prompt** | **no** | **yes** — off with `PERCEPTEYE_APPLY_PROMPT=0` |
|
|
196
|
+
| **Points your agent at the approved model** | **no** | **yes**, with `hooks.allowConversationAccess: true` — off with `PERCEPTEYE_APPLY_MODEL=0` |
|
|
197
|
+
|
|
198
|
+
**In `production` the rollout hooks are not registered at all** — the guarantee is the absence of the subscription, not a branch inside a handler that a later edit could invert. **`production` is not "observe-only"**, and this README used to say it was: applying a system prompt and a model changes what the agent does, which is the whole point of beat ⑥. What production mode still guarantees absolutely is that it **claims no work and starts no turn**.
|
|
199
|
+
|
|
200
|
+
**Both halves apply to this agent's own served turns and to nothing else.** Not to a subagent it spawns, not to a scheduled (cron) turn, and not to another agent in the same gateway. Those are ordinary runs distinguished only by their session key, and the plugin reads the `agentId` and `sessionKey` OpenClaw puts on every hook context to tell them apart. If it cannot tell, it applies nothing.
|
|
201
|
+
|
|
202
|
+
**In `training` neither half is applied**, deliberately. A rollout is evidence about the agent *as configured*; changing its prompt or its model mid-flight moves the behaviour with no weight change behind it, and the loop would attribute the difference to training.
|
|
203
|
+
|
|
204
|
+
**In `training` the plugin works the flywheel on its own**, starting each rollout's turn in its own `percepteye-rollout-*` session — never one you are using. If the host closes that seam, it says so once and falls back to delivering the task into the next turn you start; rollouts keep working either way.
|
|
205
|
+
|
|
206
|
+
`PERCEPTEYE_TRAJECTORY_DIR` names where a **training** rollout's outcomes are written, and the rollout driver sets it per rollout. It is not an off switch. These are:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
export PERCEPTEYE_CAPTURE=0 # register nothing, record nothing, claim nothing
|
|
210
|
+
export PERCEPTEYE_APPLY_PROMPT=0 # keep recording, but leave the system prompt alone
|
|
211
|
+
export PERCEPTEYE_APPLY_MODEL=0 # keep recording, but leave the model alone
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
All three are also OpenClaw config settings (`capture`, `applyPrompt`, `applyModel`), where config wins over the environment. `PERCEPTEYE_APPLY_PROMPT=0` subscribes no prompt hook at all and does not even fetch the prompt text; `PERCEPTEYE_APPLY_MODEL=0` subscribes no model hook and registers no provider. Your host can also block them from the outside: `plugins.entries.agent-flywheel.hooks.allowPromptInjection: false` stops the prompt half, and simply not setting `allowConversationAccess` stops the model half.
|
|
215
|
+
|
|
216
|
+
<br/>
|
|
217
|
+
|
|
218
|
+
## 📡 Production observability
|
|
219
|
+
|
|
220
|
+
In `production` mode the plugin records the turns your agent is already serving — one directory per turn, in the same on-disk format the Python SDK writes:
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
$PERCEPTEYE_CAPTURE_DIR/<runId>/ # default ~/.percepteye/captured
|
|
224
|
+
turns.jsonl {"kind":"task"|"answer","text","turn_id","conversation_id","task_id"}
|
|
225
|
+
conversation_id
|
|
226
|
+
task_id # optional caller-authored case/work-item correlation
|
|
227
|
+
task_id.conflict # present only when writers disagreed; task_id is then omitted
|
|
228
|
+
tool_calls.jsonl
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`task_id` is opaque correlation evidence only. The SDK never interprets it as
|
|
232
|
+
success, reward, a hint, policy identity, or permission to train. OpenClaw host
|
|
233
|
+
adapters may supply it as `taskId`/`task_id` on an event or hook context. Invalid
|
|
234
|
+
or conflicting values are omitted rather than cleaned into another identifier;
|
|
235
|
+
valid task-only turns still reach the control plane for downstream validation.
|
|
236
|
+
|
|
237
|
+
**Uploading is off until an operator turns it on, per agent.** A production turn carries *your end users'* messages, so the default cannot be to send them. With a key set, the plugin registers this agent with the control plane at startup — that is what makes it appear so an operator can enable capture — then asks a plain read whether it may upload. It never learns its permission by uploading and finding out. Once enabled, turns go up every 32 turns and again at gateway shutdown.
|
|
238
|
+
|
|
239
|
+
Recording itself is unaffected by any of that: it never leaves the machine, and works with no account at all.
|
|
240
|
+
|
|
241
|
+
### The approved pair, applied
|
|
242
|
+
|
|
243
|
+
With a key set, a `production` install also asks the control plane, **once at startup**, what this agent should be running:
|
|
244
|
+
|
|
245
|
+
- `GET /policy/current` — which model. **Applied**: the plugin registers a provider (`percepteye-flywheel`), returns it from `before_model_resolve` as the provider and model for the turn, and supplies the served `base_url` through that provider's transport. The startup line still names the `base_url`, model and `serving_generation`, because that is what a fleet cycles a replica on.
|
|
246
|
+
- `GET /prompt/current` — the approved system prompt. **Applied**, by replacing the prompt your host assembled, for every attempt.
|
|
247
|
+
|
|
248
|
+
Once, not per turn: a policy that moves under a running agent splits a process's traffic across two behaviours with nothing recording where the seam was. Restart to pick up a new one — `serving_generation` changes when and only when the answer does.
|
|
249
|
+
|
|
250
|
+
**What stops the prompt being applied**, each logged with its own reason:
|
|
251
|
+
|
|
252
|
+
| | |
|
|
253
|
+
|:--|:--|
|
|
254
|
+
| no API key | the ordinary first install: the control plane is never asked and no hook is subscribed |
|
|
255
|
+
| `PERCEPTEYE_APPLY_PROMPT=0` | you switched it off — the hook is not subscribed and the prompt text is not even fetched |
|
|
256
|
+
| an empty answer | nothing has passed the approval gate — your prompt stands. It never means "run with no prompt" |
|
|
257
|
+
| `stale_baseline` | your own baseline prompt changed after the pair was certified, so the approved text was shaped against a prompt you no longer ship |
|
|
258
|
+
| a status that is not `approved`/`delivered` | a draft or pending bundle never serves |
|
|
259
|
+
| the bundle disagreeing with itself | its prompt checksum and the checkpoint it was certified against differ — that is not the pair anybody approved |
|
|
260
|
+
|
|
261
|
+
**And what stops the model being applied**, each with its own reason too:
|
|
262
|
+
|
|
263
|
+
| | |
|
|
264
|
+
|:--|:--|
|
|
265
|
+
| no API key | as above — the same one cause, said the same way |
|
|
266
|
+
| `PERCEPTEYE_APPLY_MODEL=0` | you switched it off |
|
|
267
|
+
| the conversation opt-in missing | `before_model_resolve` is a conversation hook; without `hooks.allowConversationAccess: true` this host never delivers it |
|
|
268
|
+
| no provider could be registered | the registered provider is the only thing that can supply the served `base_url`, so if your host has no provider surface or refuses the id, the model half is refused rather than pointing your agent at a provider the host does not know |
|
|
269
|
+
| no endpoint served | this agent is on the champion arm, or its candidate is not deployed anywhere — nothing to point at |
|
|
270
|
+
| an archive reference as the model | a `gs://` or `s3://` path is a stored artifact, not a name an endpoint can serve; sending it would 404 every completion |
|
|
271
|
+
|
|
272
|
+
**And either half can be refused for one run rather than for the process** — a subagent turn, a scheduled turn, another agent in the same gateway, or a host that named no agent for the run. Each of those is logged too, once per distinct reason, naming which half and which run.
|
|
273
|
+
|
|
274
|
+
**The pair check is the line worth reading.** When both halves land, the startup log confirms it: the approved prompt is running on the checkpoint it was certified with, which is the combination that was evaluated. When only one lands, it *warns* and names **which** half is missing and why — because half of a validated combination is not the improvement anybody measured, and an operator who is not told will read the behaviour change as the certified one.
|
|
275
|
+
|
|
276
|
+
A control plane that is unreachable costs the improvement and nothing else: no answer, no change, no error.
|
|
277
|
+
|
|
278
|
+
<br/>
|
|
279
|
+
|
|
280
|
+
## 🙋 A person approves every change
|
|
281
|
+
|
|
282
|
+
**Both halves of an improvement require a human decision.** Neither the plugin nor the control plane can move your agent on its own, and no language model can approve either one by reading prose.
|
|
283
|
+
|
|
284
|
+
**The model.** Your agent only leaves the champion arm when **all three** of these hold, and each fails closed back to the champion:
|
|
285
|
+
|
|
286
|
+
1. **A person with the operator role moves it.** `POST /flywheel-agents/{agent_id}/policy-channel` requires the operator role and a signed-in human identity — the flywheel key your agent uses cannot make this call at all. Rollback is the same call with the other value. No promotion applies it, and there is no percentage rollout that can.
|
|
287
|
+
2. **The candidate's certification verdict is not a refusal.** `regressed`, `inconclusive` and `escalated` are all refused — the very same list the promotion gate uses, so an agent can never be served a model the promote path would have rejected.
|
|
288
|
+
3. **It is actually deployed somewhere servable.** A stored-artifact reference (`gs://`, `s3://`, `file://`) is refused rather than handed out as a model name.
|
|
289
|
+
|
|
290
|
+
**The prompt.** A separate chain with its own approval: only a `PromptPairDelivery` whose status is `approved` or `delivered` is ever served, and this plugin re-checks that status itself before applying the text.
|
|
291
|
+
|
|
292
|
+
**And neither can be approved by a language model reading free text.** `model_promotion`, `prompt_pair_delivery` and `research_promotion` are all classed as irreversible decisions in the control plane, which means only the explicit approve/reject control can resolve them. An interpreted "sure, go ahead" — in a chat thread, in a document, anywhere — never can.
|
|
293
|
+
|
|
294
|
+
<br/>
|
|
295
|
+
|
|
296
|
+
## 📄 What it writes
|
|
297
|
+
|
|
298
|
+
One JSON object per executed tool call, appended to `tool_calls.jsonl`:
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{"name":"exec","arguments":{"command":"exit 3"},"outcome":"failed","status_code":null,"error":"(Command exited with code 3)","error_class":"ToolError","latency_ms":155,"agent_name":"main","tool_call_id":"call_percepteye_1"}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
That line is real output from a live OpenClaw run, and it is the same format the Python SDK writes and reads.
|
|
305
|
+
|
|
306
|
+
<br/>
|
|
307
|
+
|
|
308
|
+
## ⚙️ Configuration
|
|
309
|
+
|
|
310
|
+
Scalar settings are settable two ways: in your OpenClaw config under `plugins.entries.agent-flywheel.config`, or in the environment. **Config wins over the environment**, because config is hot-reloaded and puts a secret under the host's own secret handling, while a gateway reads its environment once at launch. The nested execution snapshot is config-only so component names and completeness remain typed rather than being parsed from a shell string.
|
|
311
|
+
|
|
312
|
+
| config key | environment variable | default | what it decides |
|
|
313
|
+
|:--|:--|:--|:--|
|
|
314
|
+
| `apiKey` | `PERCEPTEYE_API_KEY` | *(none)* | the consent boundary. Without it nothing leaves the machine: no registration, no claim, no report, no control-plane read |
|
|
315
|
+
| `mode` | `PERCEPTEYE_AGENT_MODE` | `training` | `training` claims rollouts and drives turns; `production` claims none and starts none, captures the turns you serve, and applies the approved pair. An unreadable value degrades to `production` and says so |
|
|
316
|
+
| `agentId` | `PERCEPTEYE_AGENT_ID` | `openclaw` | the agent name this install reports as, **and** which OpenClaw agent's approved prompt and model it applies. Set it if your host runs more than one agent |
|
|
317
|
+
| `controlPlaneUrl` | `PERCEPTEYE_CONTROL_PLANE_URL` | `https://launch.percepteye.ai/api/flywheel/v1` | the control plane this install talks to |
|
|
318
|
+
| `capture` | `PERCEPTEYE_CAPTURE` | on | `0`/`false`/`no`/`off` registers no hooks and records nothing, in either mode |
|
|
319
|
+
| `applyPrompt` | `PERCEPTEYE_APPLY_PROMPT` | on | `0` subscribes no prompt hook and does not fetch the prompt text |
|
|
320
|
+
| `applyModel` | `PERCEPTEYE_APPLY_MODEL` | on | `0` subscribes no model hook and registers no provider |
|
|
321
|
+
| `executionSnapshot` | *(config only)* | *(none)* | opaque SHA-256 components from the host adapter, its completeness assertion, and optionally an exact startup prompt/tool/provider/model observation. Exact startup evidence binds registration and claims to the same SDK-authored execution identity later reported; missing, invalid, lossy, or conflicting evidence abstains |
|
|
322
|
+
|
|
323
|
+
The snapshot contains `adapterName`, `executionIdentityComplete: true`, and a non-empty `executionComponents` object whose values are lowercase SHA-256 digests. An adapter that can resolve the full execution before work is claimed may also provide `startupObservation: {systemPrompt, tools, provider, model}`. Core runs startup and per-run evidence through the same canonical descriptor; it never accepts a caller-authored final digest. Component names belong to the adapter, and core does not assign them meaning. The resulting `execution_sha256` identifies the observed execution scaffold, not the serving policy, and this package does not generate training hints; those decisions belong downstream where evidence from multiple turns can be compared.
|
|
324
|
+
|
|
325
|
+
Three more environment variables name directories rather than switching anything. `PERCEPTEYE_HOME` relocates everything this package writes (default `~/.percepteye`); `PERCEPTEYE_CAPTURE_DIR` overrides just the production turn capture (default `$PERCEPTEYE_HOME/captured`); and `PERCEPTEYE_TRAJECTORY_DIR` names where a training rollout's outcomes go, which the rollout driver sets per rollout.
|
|
326
|
+
|
|
327
|
+
One host setting is not ours and has no environment form: `plugins.entries.agent-flywheel.hooks.allowConversationAccess`, which OpenClaw itself requires before it will deliver conversation hooks to a non-bundled plugin. See [Install](#-install-in-one-call).
|
|
328
|
+
|
|
329
|
+
<br/>
|
|
330
|
+
|
|
331
|
+
## ✨ Why it earns its place
|
|
332
|
+
|
|
333
|
+
- 🪶 **Zero runtime dependencies.** Node builtins only — no version conflict imposed on a customer who did not ask for one, enforced by a CI job.
|
|
334
|
+
- 🔌 **Kill switches that really are ones.** `PERCEPTEYE_CAPTURE=0` subscribes no hooks and records nothing; `PERCEPTEYE_APPLY_PROMPT=0` subscribes no prompt hook and does not even fetch the prompt text; `PERCEPTEYE_APPLY_MODEL=0` subscribes no model hook and registers no provider. In every case the guarantee is the absence of the subscription, not a branch inside a handler.
|
|
335
|
+
- ⚖️ **It applies an approved pair, or says why it did not.** Every refusal names its own cause rather than one message covering all of them — and when only one half lands it warns and names *which*, because half of a certified combination is not the improvement that was measured.
|
|
336
|
+
- 🎯 **One agent's approval changes one agent.** The approved prompt and model reach that agent's own served turns and nothing else — not its subagents, not its scheduled turns, not another agent in the same gateway. Where identity cannot be established, nothing is applied.
|
|
337
|
+
- 🎯 **`unknown` is a real answer** — a tool call whose verdict nobody stated is never rounded up to `ok`, because a failure scored as a success is training data that teaches the model the failure was fine.
|
|
338
|
+
- 🤝 **One on-disk format across both SDKs**, kept honest by a shared contract schema ([`schema/flywheel-1.json`](schema/flywheel-1.json)) and a test that asserts this package's wire types match it.
|
|
339
|
+
- 🧯 **It degrades, it doesn't fail.** A missing key, an unreachable control plane, a closed host seam — each costs the feature that needed it and says so once. Local recording keeps running.
|
|
340
|
+
- 🔒 **Every lane is opened from your side.** There is no listening socket anywhere in this package.
|
|
341
|
+
|
|
342
|
+
<br/>
|
|
343
|
+
|
|
344
|
+
## 🐍 Python or Node?
|
|
345
|
+
|
|
346
|
+
**Pick one.** Two implementations of the same flywheel for two different homes — install whichever matches the harness your agent already runs in, never both.
|
|
347
|
+
|
|
348
|
+
| | this package | [`agent-flywheel`](https://github.com/percepteye-ai/agent-flywheel) (Python) |
|
|
349
|
+
|---|---|---|
|
|
350
|
+
| **you have** | an OpenClaw or DeepSeek Harness agent | a Python agent, or a CLI agent you launch yourself |
|
|
351
|
+
| **you install** | a harness plugin | a pip package — `pip install agent-flywheel` |
|
|
352
|
+
| **you change** | nothing — the harness loads it | nothing — one import, one call |
|
|
353
|
+
| **the approved prompt** | applied for you — the plugin *is* your host's prompt seam | handed back by `current_prompt()` for you to apply; a library in someone else's process has nowhere to put it |
|
|
354
|
+
| **the approved model** | applied for you — the plugin registers a provider and points the turn at it | handed back as an endpoint for you to call |
|
|
355
|
+
|
|
356
|
+
<br/>
|
|
357
|
+
|
|
358
|
+
## 🛠 Develop
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
npm test # 434 tests, no install step — node:test, zero dependencies
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
The suite includes payloads captured verbatim from a live OpenClaw run (`test/host-fixtures.test.js`). If a future OpenClaw changes the shape, those fail — rather than someone discovering it months later from bad training data.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
MIT — see [LICENSE](LICENSE) · security reports: **security@percepteye.ai** · issues and PRs welcome at [agent-flywheel-node](https://github.com/percepteye-ai/agent-flywheel-node/issues).
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Record tool-call outcomes into the active rollout's trajectory.
|
|
2
|
+
#
|
|
3
|
+
# Observes only: `tools/result` is an `@mode emit` listener over a deep-frozen
|
|
4
|
+
# execution and result, so this plugin has no way to alter what the model sees.
|
|
5
|
+
#
|
|
6
|
+
# Inert unless PERCEPTEYE_TRAJECTORY_DIR is set, which makes it safe to leave
|
|
7
|
+
# in a profile permanently — it is a no-op in production and a recorder during
|
|
8
|
+
# a rollout.
|
|
9
|
+
- insert:
|
|
10
|
+
- id: agent-flywheel
|
|
11
|
+
name: '@percepteye/agent-flywheel/dsh'
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "agent-flywheel",
|
|
3
|
+
"name": "Agent Flywheel",
|
|
4
|
+
"description": "Close the loop on your OpenClaw or DeepSeek Harness agent: record what your tools actually did — ok, failed or unknown — claim graded rollouts in training mode, and report them back. Never alters a tool result; set PERCEPTEYE_AGENT_MODE=production for a serving install, which claims no rollouts and starts no turn but does capture the turns you serve and apply the system prompt and the model your control plane has approved.",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"providers": [
|
|
7
|
+
"percepteye-flywheel"
|
|
8
|
+
],
|
|
9
|
+
"configSchema": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"additionalProperties": false,
|
|
12
|
+
"properties": {
|
|
13
|
+
"apiKey": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"minLength": 1,
|
|
16
|
+
"description": "PerceptEye flywheel key (pefw_...). Without it the plugin records locally and contacts nothing."
|
|
17
|
+
},
|
|
18
|
+
"agentId": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"minLength": 1,
|
|
21
|
+
"maxLength": 255,
|
|
22
|
+
"pattern": "^(?!\\.{1,2}$)(?!\\s)(?!.*\\s$)(?!.*[\\\\/])[^\\u0000-\\u001F\\u007F-\\u009F]+$",
|
|
23
|
+
"description": "The agent name this install reports as. Unset means PERCEPTEYE_AGENT_ID, then \"openclaw\"."
|
|
24
|
+
},
|
|
25
|
+
"controlPlaneUrl": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"format": "uri",
|
|
28
|
+
"description": "Override the control-plane base URL."
|
|
29
|
+
},
|
|
30
|
+
"capture": {
|
|
31
|
+
"type": [
|
|
32
|
+
"string",
|
|
33
|
+
"boolean"
|
|
34
|
+
],
|
|
35
|
+
"description": "Set false/0/off to disable local capture entirely. Unset means PERCEPTEYE_CAPTURE, then on."
|
|
36
|
+
},
|
|
37
|
+
"applyPrompt": {
|
|
38
|
+
"type": [
|
|
39
|
+
"string",
|
|
40
|
+
"boolean"
|
|
41
|
+
],
|
|
42
|
+
"description": "Set false/0/off to stop applying the control plane's approved system prompt in production mode. Unset means PERCEPTEYE_APPLY_PROMPT, then on."
|
|
43
|
+
},
|
|
44
|
+
"applyModel": {
|
|
45
|
+
"type": [
|
|
46
|
+
"string",
|
|
47
|
+
"boolean"
|
|
48
|
+
],
|
|
49
|
+
"description": "Set false/0/off to stop pointing this agent at the control plane's approved model in production mode. Unset means PERCEPTEYE_APPLY_MODEL, then on. Needs hooks.allowConversationAccess=true to take effect at all."
|
|
50
|
+
},
|
|
51
|
+
"executionSnapshot": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"additionalProperties": false,
|
|
54
|
+
"required": [
|
|
55
|
+
"adapterName",
|
|
56
|
+
"executionIdentityComplete",
|
|
57
|
+
"executionComponents"
|
|
58
|
+
],
|
|
59
|
+
"properties": {
|
|
60
|
+
"adapterName": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"minLength": 1
|
|
63
|
+
},
|
|
64
|
+
"executionIdentityComplete": {
|
|
65
|
+
"type": "boolean"
|
|
66
|
+
},
|
|
67
|
+
"executionComponents": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"minProperties": 1,
|
|
70
|
+
"propertyNames": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"minLength": 1
|
|
73
|
+
},
|
|
74
|
+
"additionalProperties": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"pattern": "^[0-9a-f]{64}$"
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"startupObservation": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"additionalProperties": false,
|
|
82
|
+
"required": [
|
|
83
|
+
"systemPrompt",
|
|
84
|
+
"tools",
|
|
85
|
+
"provider",
|
|
86
|
+
"model"
|
|
87
|
+
],
|
|
88
|
+
"properties": {
|
|
89
|
+
"systemPrompt": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"minLength": 1
|
|
92
|
+
},
|
|
93
|
+
"tools": {
|
|
94
|
+
"type": "array",
|
|
95
|
+
"items": {
|
|
96
|
+
"type": "object"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"provider": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"minLength": 1
|
|
102
|
+
},
|
|
103
|
+
"model": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"minLength": 1
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"description": "Optional exact prompt/tool/provider/model observation available before the first claim. It lets registration carry the same SDK-authored execution fingerprint later reports must match."
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"description": "Host-adapter evidence for exact execution identity. Component names are opaque to the SDK; incomplete or invalid evidence emits no identity. A startup observation enables pre-claim registration binding."
|
|
112
|
+
},
|
|
113
|
+
"mode": {
|
|
114
|
+
"type": "string",
|
|
115
|
+
"enum": [
|
|
116
|
+
"training",
|
|
117
|
+
"train",
|
|
118
|
+
"rollout",
|
|
119
|
+
"rollouts",
|
|
120
|
+
"fine-tuning",
|
|
121
|
+
"finetuning",
|
|
122
|
+
"improving",
|
|
123
|
+
"production",
|
|
124
|
+
"prod",
|
|
125
|
+
"serving"
|
|
126
|
+
],
|
|
127
|
+
"description": "training: this install claims rollouts and drives turns. production: claims no rollout and starts no turn -- those hooks are never registered -- but captures the turns you serve and applies the system prompt your control plane approved. Unset means PERCEPTEYE_AGENT_MODE, then training; a value here overrides the environment."
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"activation": {
|
|
132
|
+
"onStartup": true
|
|
133
|
+
}
|
|
134
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@percepteye/agent-flywheel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Close the loop on your OpenClaw or DeepSeek Harness agent: record what your tools actually did — ok, failed or unknown — claim graded rollouts in training mode, and report them back. Never alters a tool result; set PERCEPTEYE_AGENT_MODE=production for a serving install, which claims no rollouts and starts no turn but does capture the turns you serve and apply the system prompt and the model your control plane has approved.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "PerceptEye",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./src/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js",
|
|
11
|
+
"./classify": "./src/classify.js",
|
|
12
|
+
"./trajectory": "./src/trajectory.js",
|
|
13
|
+
"./dsh": "./src/dsh.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"schema",
|
|
18
|
+
"openclaw.plugin.json",
|
|
19
|
+
"cordis.patch.yml",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"openclaw",
|
|
28
|
+
"openclaw-plugin",
|
|
29
|
+
"dsh-plugin",
|
|
30
|
+
"deepseek-harness",
|
|
31
|
+
"percepteye",
|
|
32
|
+
"agent",
|
|
33
|
+
"reinforcement-learning",
|
|
34
|
+
"continual-learning",
|
|
35
|
+
"observability"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22.22.3"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/percepteye-ai/agent-flywheel-node.git"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/percepteye-ai/agent-flywheel-node",
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test": "node --test --test-timeout=30000 test/*.test.js"
|
|
47
|
+
},
|
|
48
|
+
"openclaw": {
|
|
49
|
+
"extensions": [
|
|
50
|
+
"./src/index.js"
|
|
51
|
+
],
|
|
52
|
+
"compat": {
|
|
53
|
+
"pluginApi": ">=2026.7.1"
|
|
54
|
+
},
|
|
55
|
+
"build": {
|
|
56
|
+
"openclawVersion": "2026.7.1"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"dsh": {
|
|
60
|
+
"bundle": {
|
|
61
|
+
"patch": "./cordis.patch.yml"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"bugs": {
|
|
65
|
+
"url": "https://github.com/percepteye-ai/agent-flywheel-node/issues"
|
|
66
|
+
}
|
|
67
|
+
}
|