@monotykamary/pi-loop 0.1.12
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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +285 -0
- package/media/demo.mp4 +0 -0
- package/media/pi-loop.jpg +0 -0
- package/media/screenshot.png +0 -0
- package/package.json +89 -0
- package/src/core/analyzer.ts +51 -0
- package/src/core/content-extractor.ts +79 -0
- package/src/core/inference.ts +137 -0
- package/src/core/prompt-builder.ts +217 -0
- package/src/core/prompt-loader.ts +126 -0
- package/src/core/reframe.ts +30 -0
- package/src/core/snapshot-builder.ts +252 -0
- package/src/global-config.ts +38 -0
- package/src/index.ts +532 -0
- package/src/session/client.ts +47 -0
- package/src/session/loop-session.ts +102 -0
- package/src/session/response-parser.ts +37 -0
- package/src/state/manager.ts +164 -0
- package/src/state/patterns.ts +81 -0
- package/src/state/reframe.ts +33 -0
- package/src/subagent-detector.ts +94 -0
- package/src/types.ts +83 -0
- package/src/ui/animations.ts +70 -0
- package/src/ui/model-picker.ts +79 -0
- package/src/ui/renderer.ts +257 -0
- package/src/ui/status-widget.ts +30 -0
- package/src/ui/types.ts +48 -0
- package/tests/compaction.test.ts +754 -0
- package/tests/continue-action-regression.test.ts +456 -0
- package/tests/engine.test.ts +770 -0
- package/tests/ephemeral-supervision.test.ts +391 -0
- package/tests/full-fidelity-snapshot.test.ts +843 -0
- package/tests/parsing.test.ts +303 -0
- package/tests/state.test.ts +525 -0
- package/tests/status-widget.test.ts +703 -0
- package/tests/subagent-detector.test.ts +191 -0
- package/tests/supervise-command.test.ts +381 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +15 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-04-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Initial release of `pi-loop`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tintinweb
|
|
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,285 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🔁 pi-loop
|
|
4
|
+
|
|
5
|
+
**Close the verification loop for [pi](https://github.com/earendil-works/pi-coding-agent)**
|
|
6
|
+
|
|
7
|
+
_Verify with different tools than used to create — multi-modal verification before declaring done._
|
|
8
|
+
|
|
9
|
+
[](https://github.com/earendil-works/pi-coding-agent)
|
|
10
|
+
[](./LICENSE)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
> Close the loop — verify before you commit.
|
|
17
|
+
|
|
18
|
+
> **Status:** Early release.
|
|
19
|
+
|
|
20
|
+
<img alt="pi-loop" src="./media/pi-loop.jpg" />
|
|
21
|
+
|
|
22
|
+
## How It Works
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
/loop # Auto-infer goal and enter loop mode
|
|
26
|
+
# or
|
|
27
|
+
/loop Refactor auth to use DI
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
In loop mode, the agent:
|
|
31
|
+
|
|
32
|
+
1. **Infers "done" criteria** from the task description (documented in `loop.md`)
|
|
33
|
+
2. **Works normally** — uses all tools (read, edit, write, bash)
|
|
34
|
+
3. **Closes the loop** — verifies with DIFFERENT tools than used to create:
|
|
35
|
+
- Wrote with `edit` → verify with `read` or `bash` (tests)
|
|
36
|
+
- Created with `write` → verify with `read` or `search` (orphans)
|
|
37
|
+
4. **Only then declares done** — with confidence score and verification log
|
|
38
|
+
|
|
39
|
+
An external observer watches from outside without modifying the agent's context. It validates that the agent actually closed the loop (not just claimed to).
|
|
40
|
+
|
|
41
|
+
**Anti-cheat via multi-modal:** The agent cannot game verification because it must re-access work via different cognitive paths.
|
|
42
|
+
|
|
43
|
+
**Token efficiency:** The observer reuses its session across analyses and builds conversation snapshots incrementally, using ~85% fewer tokens than naive oversight. The snapshot captures the last 20 messages (user prompts, assistant responses, tool calls, and tool outputs) to keep context focused while ensuring the observer can see multi-modal verification evidence.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pi install https://github.com/monotykamary/pi-loop
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or load directly for development:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pi -e ~/projects/pi-loop/src/index.ts
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Commands
|
|
58
|
+
|
|
59
|
+
| Command | Description |
|
|
60
|
+
| ----------------- | ----------------------------------- |
|
|
61
|
+
| `/loop` | Auto-infer goal and enter loop mode |
|
|
62
|
+
| `/loop <outcome>` | Start loop mode with explicit goal |
|
|
63
|
+
| `/loop stop` | Stop loop mode |
|
|
64
|
+
| `/loop widget` | Toggle the status widget on/off |
|
|
65
|
+
|
|
66
|
+
### Examples
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
/loop
|
|
70
|
+
|
|
71
|
+
/loop Refactor the auth module to use dependency injection and verify all call sites updated
|
|
72
|
+
|
|
73
|
+
/loop stop
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The agent can also initiate loop mode itself by calling the `start_loop` tool — useful when it recognises a task needs verification. The tool uses the global config model or active chat model; the AI cannot specify a model. Once active, loop mode is locked: only the user can change or stop it.
|
|
77
|
+
|
|
78
|
+
## UI
|
|
79
|
+
|
|
80
|
+
### Live Widget
|
|
81
|
+
|
|
82
|
+
The widget displays loop state in a compact one-line format (text truncates to fit window width):
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
◉ Loop · Goal: "Refactor auth module…" · ↗ 2 · validating
|
|
86
|
+
Agent is working... will verify with different tools before declaring done
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Header states:
|
|
90
|
+
|
|
91
|
+
- **Inferring** — analyzing conversation to suggest a goal (`◉ Inferring · scanning`)
|
|
92
|
+
- **Loop** — active loop mode in progress
|
|
93
|
+
- **Closed** — loop closed, goal achieved, widget clears after delay
|
|
94
|
+
|
|
95
|
+
When the observer detects an ineffective pattern, the reframe tier appears (e.g., `↻2`):
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
◉ Loop · Goal: "Implement payment flow…" · ↗ 5 · ↻2 · analyzing
|
|
99
|
+
Breaking into smaller milestone: get the checkout form rendering first…
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The thinking text streams naturally into multiple lines. When loop mode ends or steers, thoughts animate away line-by-line from bottom to top (clearing newest first), then the widget clears. Toggle the widget with `/loop widget`.
|
|
103
|
+
|
|
104
|
+
## How Loop Mode Works
|
|
105
|
+
|
|
106
|
+
**Analysis triggers:**
|
|
107
|
+
|
|
108
|
+
| When | Why |
|
|
109
|
+
| ----------------------------- | ------------------------------------------------ |
|
|
110
|
+
| Agent goes idle (`agent_end`) | Critical decision point — must choose done/steer |
|
|
111
|
+
| After we steered | Verify the steer worked |
|
|
112
|
+
| Every 8th turn | Safety valve to catch runaway drift |
|
|
113
|
+
| Tool errors detected | If agent hits an error, we check |
|
|
114
|
+
|
|
115
|
+
The observer only intervenes when it has high confidence the agent is off track or hasn't properly closed the loop. It validates multi-modal verification happened before accepting "done".
|
|
116
|
+
|
|
117
|
+
## Reframe Escalation
|
|
118
|
+
|
|
119
|
+
When the observer detects that steering isn't working, it escalates through **4 tiers** of reframing strategies rather than giving up:
|
|
120
|
+
|
|
121
|
+
| Tier | Trigger | Strategy |
|
|
122
|
+
| ---- | ------------------------- | -------------------------------------------------------------------------- |
|
|
123
|
+
| 0 | (default) | Standard steering |
|
|
124
|
+
| 1 | Similar messages detected | **Directive** — be extremely specific about the next single action |
|
|
125
|
+
| 2 | Pattern continues | **Subgoal** — break the goal into a smaller, verifiable milestone |
|
|
126
|
+
| 3 | Still stuck | **Pivot** — suggest a completely different strategy or implementation path |
|
|
127
|
+
| 4 | Persistent stall | **Minimal slice** — strip to absolute essentials, demand tangible output |
|
|
128
|
+
|
|
129
|
+
**Pattern detection:** The observer tracks two indicators of ineffectiveness:
|
|
130
|
+
|
|
131
|
+
- **Message similarity** — when 2+ recent steering messages are similar (suggesting the agent isn't responding)
|
|
132
|
+
- **Stagnation** — when 3+ turns pass without progress after a steer
|
|
133
|
+
|
|
134
|
+
When either pattern is detected, the observer escalates the reframe tier and injects tier-specific guidance into its prompt. The tier resets when the goal is achieved and the loop is closed. This allows the observer to adapt to long-horizon projects that may take hours or days, rather than forcing early termination.
|
|
135
|
+
|
|
136
|
+
## Observer Model
|
|
137
|
+
|
|
138
|
+
The observer runs on a **separate model** — it can be a cheaper/faster model than the one doing the actual work.
|
|
139
|
+
|
|
140
|
+
**Resolution order:**
|
|
141
|
+
|
|
142
|
+
1. Previous session state (persists within a session)
|
|
143
|
+
2. `.pi/loop-config.json` in the project root (saved when you pick a model)
|
|
144
|
+
3. Active chat model (`ctx.model`) — so it works out of the box with no configuration
|
|
145
|
+
|
|
146
|
+
Change at any time by running `/loop <goal>` with a different model active, or delete `.pi/loop-config.json` to reset.
|
|
147
|
+
|
|
148
|
+
## Focus and Loop Discipline
|
|
149
|
+
|
|
150
|
+
The observer is a pure outside observer — it does not modify the agent's system prompt. Goal discipline is enforced entirely through steering messages when the agent drifts. Loop discipline validates that the agent properly closed the loop with multi-modal verification before accepting "done".
|
|
151
|
+
|
|
152
|
+
Unlike earlier versions, there are **no artificial limits** on steering attempts. The observer uses [reframe escalation](#reframe-escalation) to adapt its strategy when standard steering isn't working, allowing it to manage long-horizon projects that may take hours or days to complete.
|
|
153
|
+
|
|
154
|
+
## Customizing the Observer: LOOP.md
|
|
155
|
+
|
|
156
|
+
The observer's reasoning is controlled by its **system prompt** — not the goal. The goal is always set at runtime via `/loop`. `LOOP.md` defines _how_ the observer thinks: its rules, persona, and project-specific constraints.
|
|
157
|
+
|
|
158
|
+
**Discovery order** (mirrors pi's `SYSTEM.md` convention):
|
|
159
|
+
|
|
160
|
+
| Priority | Location | Use for |
|
|
161
|
+
| -------- | --------------------- | ---------------------- |
|
|
162
|
+
| 1 | `.pi/LOOP.md` | Project-specific rules |
|
|
163
|
+
| 2 | `~/.pi/agent/LOOP.md` | Global personal rules |
|
|
164
|
+
| 3 | Built-in template | Fallback |
|
|
165
|
+
|
|
166
|
+
The active source is shown when you run `/loop <goal>` or when the tool is invoked.
|
|
167
|
+
|
|
168
|
+
### Built-in system prompt
|
|
169
|
+
|
|
170
|
+
The default prompt the observer uses when no `LOOP.md` is found:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
You are a supervisor monitoring a coding AI assistant conversation.
|
|
174
|
+
Your job: ensure the assistant fully achieves a specific outcome without needing the human to intervene.
|
|
175
|
+
|
|
176
|
+
═══ WHEN THE AGENT IS IDLE (finished its turn, waiting for user input) ═══
|
|
177
|
+
This is your most important moment. The agent has stopped and is waiting.
|
|
178
|
+
You MUST choose "done" or "steer". Never return "continue" when the agent is idle.
|
|
179
|
+
|
|
180
|
+
- "done" → only when the outcome is completely and verifiably achieved.
|
|
181
|
+
- "steer" → everything else: incomplete work, partial progress, open questions, waiting for confirmation.
|
|
182
|
+
|
|
183
|
+
If the agent asked a clarifying question or needs a decision:
|
|
184
|
+
FIRST check: is this question necessary to achieve the goal?
|
|
185
|
+
- YES (directly blocks goal progress): answer with a sensible default and tell agent to proceed.
|
|
186
|
+
- NO (out of scope, nice-to-have, unrelated feature): do NOT answer it. Redirect:
|
|
187
|
+
"That's outside the scope of the goal. Focus on: [restate the specific missing piece]."
|
|
188
|
+
DO NOT answer: passwords, credentials, secrets, anything requiring real user knowledge.
|
|
189
|
+
|
|
190
|
+
Your steer message speaks AS the user. Make it clear, direct, and actionable (1–3 sentences).
|
|
191
|
+
Do not ask the agent to verify its own work — tell it what to do next.
|
|
192
|
+
|
|
193
|
+
═══ WHEN THE AGENT IS ACTIVELY WORKING (mid-turn) ═══
|
|
194
|
+
Only intervene if it is clearly heading in the wrong direction.
|
|
195
|
+
Trust the agent to complete what it has started. Avoid interrupting productive work.
|
|
196
|
+
|
|
197
|
+
═══ STEERING RULES ═══
|
|
198
|
+
- Be specific: reference the outcome, missing pieces, or the question being answered.
|
|
199
|
+
- Never repeat a steering message that had no effect — escalate or change approach.
|
|
200
|
+
- A good steer answers the agent's question OR redirects to the missing piece of the outcome.
|
|
201
|
+
- If the agent is taking shortcuts to satisfy the goal without properly achieving it, always steer and remind it not to take shortcuts.
|
|
202
|
+
|
|
203
|
+
"done" CRITERIA: The core outcome is complete and functional. Minor polish, style tweaks, or
|
|
204
|
+
optional improvements do NOT block "done". Prefer stopping when the goal is substantially
|
|
205
|
+
achieved rather than looping forever chasing perfection.
|
|
206
|
+
|
|
207
|
+
Respond ONLY with valid JSON — no prose, no markdown fences.
|
|
208
|
+
Response schema (strict JSON):
|
|
209
|
+
{
|
|
210
|
+
"action": "continue" | "steer" | "done",
|
|
211
|
+
"message": "...", // Required when action === "steer"
|
|
212
|
+
"reasoning": "...", // Brief internal reasoning
|
|
213
|
+
"confidence": 0.85 // Float 0-1
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Dynamic reframe guidance:** When the observer detects an ineffective pattern, it injects tier-specific guidance into the prompt (see [Reframe Escalation](#reframe-escalation)).
|
|
218
|
+
|
|
219
|
+
### Writing a custom LOOP.md
|
|
220
|
+
|
|
221
|
+
You must preserve the JSON response schema. Everything else is up to you.
|
|
222
|
+
|
|
223
|
+
```markdown
|
|
224
|
+
You are a supervisor for a TypeScript project. Your priorities: type safety and test coverage.
|
|
225
|
+
|
|
226
|
+
Rules:
|
|
227
|
+
|
|
228
|
+
- Steer if the agent uses `any` types or skips tests for new code
|
|
229
|
+
- When steering, be direct: one sentence max, reference the specific file/function if possible
|
|
230
|
+
- "done" only when the new code has types and tests — not before
|
|
231
|
+
- Do not steer about code style, naming, or documentation
|
|
232
|
+
|
|
233
|
+
Response schema (strict JSON, required):
|
|
234
|
+
{
|
|
235
|
+
"action": "continue" | "steer" | "done",
|
|
236
|
+
"message": "...",
|
|
237
|
+
"reasoning": "...",
|
|
238
|
+
"confidence": 0.85
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Session Persistence
|
|
243
|
+
|
|
244
|
+
Supervision state (outcome, model, intervention history) is stored in the pi session file and restored automatically on restart, session switch, fork, and tree navigation.
|
|
245
|
+
|
|
246
|
+
## Testing
|
|
247
|
+
|
|
248
|
+
Run the test suite:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
npm test # Run once
|
|
252
|
+
npm run test:watch # Watch mode
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Coverage report generated in `coverage/`.
|
|
256
|
+
|
|
257
|
+
## Project Structure
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
src/
|
|
261
|
+
index.ts # Extension entry point, event wiring, /supervise command, start_supervision tool
|
|
262
|
+
types.ts # SupervisorState, SteeringDecision, ConversationMessage, ReframeTier
|
|
263
|
+
state/ # State management
|
|
264
|
+
manager.ts # SupervisorStateManager — persistence, reframe tier, pattern detection
|
|
265
|
+
core/ # Core supervision logic
|
|
266
|
+
analyzer.ts # Main analysis engine
|
|
267
|
+
inference.ts # Goal inference from conversation
|
|
268
|
+
prompt-loader.ts # SUPERVISOR.md loading
|
|
269
|
+
session/ # Model session management
|
|
270
|
+
client.ts # SupervisorSession (reusable), API calls
|
|
271
|
+
ui/ # User interface
|
|
272
|
+
renderer.ts # Widget rendering and footer management
|
|
273
|
+
animations.ts # Thought clearing animations
|
|
274
|
+
types.ts # Widget state types
|
|
275
|
+
model-picker.ts # Interactive model picker
|
|
276
|
+
global-config.ts # .pi/supervisor-config.json read/write
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Acknowledgments
|
|
280
|
+
|
|
281
|
+
This project is a fork of [pi-supervisor](https://github.com/tintinweb/pi-supervisor) by [tintinweb](https://github.com/tintinweb). The original supervision concepts and architecture served as the foundation for pi-loop's verification loop methodology.
|
|
282
|
+
|
|
283
|
+
## License
|
|
284
|
+
|
|
285
|
+
MIT — [tintinweb](https://github.com/tintinweb) (forked by [monotykamary](https://github.com/monotykamary))
|
package/media/demo.mp4
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@monotykamary/pi-loop",
|
|
3
|
+
"version": "0.1.12",
|
|
4
|
+
"description": "A pi extension that closes the verification loop on task completion",
|
|
5
|
+
"author": "monotykamary",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/monotykamary/pi-loop.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/monotykamary/pi-loop#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/monotykamary/pi-loop/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"pi-package",
|
|
17
|
+
"pi",
|
|
18
|
+
"pi-extension",
|
|
19
|
+
"loop",
|
|
20
|
+
"verification",
|
|
21
|
+
"close-loop",
|
|
22
|
+
"agent",
|
|
23
|
+
"productivity"
|
|
24
|
+
],
|
|
25
|
+
"files": [
|
|
26
|
+
"src/**/*.ts",
|
|
27
|
+
"tests/**/*.ts",
|
|
28
|
+
"vitest.config.ts",
|
|
29
|
+
"tsconfig.json",
|
|
30
|
+
"README.md",
|
|
31
|
+
"CHANGELOG.md",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"media/"
|
|
34
|
+
],
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@earendil-works/pi-ai": ">=0.80.8",
|
|
37
|
+
"@earendil-works/pi-coding-agent": ">=0.80.8",
|
|
38
|
+
"@sinclair/typebox": "0.34.49"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@commitlint/cli": "21.0.1",
|
|
42
|
+
"@commitlint/config-conventional": "21.0.1",
|
|
43
|
+
"@earendil-works/pi-tui": "0.80.8",
|
|
44
|
+
"@types/node": "25.9.1",
|
|
45
|
+
"@vitest/coverage-v8": "4.1.7",
|
|
46
|
+
"knip": "6.14.1",
|
|
47
|
+
"lint-staged": "17.0.5",
|
|
48
|
+
"prettier": "3.8.3",
|
|
49
|
+
"simple-git-hooks": "2.13.1",
|
|
50
|
+
"standard-version": "9.5.0",
|
|
51
|
+
"typescript": "6.0.3",
|
|
52
|
+
"vitest": "4.1.7"
|
|
53
|
+
},
|
|
54
|
+
"simple-git-hooks": {
|
|
55
|
+
"pre-commit": "npx lint-staged",
|
|
56
|
+
"pre-push": "npm run typecheck && npm run test",
|
|
57
|
+
"commit-msg": "npx commitlint --edit ${1}"
|
|
58
|
+
},
|
|
59
|
+
"lint-staged": {
|
|
60
|
+
"*.{ts,js,json,md}": [
|
|
61
|
+
"prettier --write"
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
"pi": {
|
|
65
|
+
"extensions": [
|
|
66
|
+
"./src/index.ts"
|
|
67
|
+
],
|
|
68
|
+
"skills": [
|
|
69
|
+
"./skills/pi-loop"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"overrides": {
|
|
73
|
+
"brace-expansion": "5.0.6",
|
|
74
|
+
"fast-xml-builder": "1.2.0",
|
|
75
|
+
"protobufjs": "8.4.0",
|
|
76
|
+
"ws": "8.20.1"
|
|
77
|
+
},
|
|
78
|
+
"scripts": {
|
|
79
|
+
"test": "vitest run",
|
|
80
|
+
"test:watch": "vitest",
|
|
81
|
+
"typecheck": "tsc --noEmit",
|
|
82
|
+
"lint:dead": "knip --no-gitignore",
|
|
83
|
+
"format": "prettier --write .",
|
|
84
|
+
"format:check": "prettier --check .",
|
|
85
|
+
"release": "standard-version",
|
|
86
|
+
"release:dry-run": "standard-version --dry-run",
|
|
87
|
+
"postinstall": "simple-git-hooks 2>/dev/null || true"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main analyzer - orchestrates supervisor analysis.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import type { SteeringDecision, LoopState } from '../types.js';
|
|
7
|
+
import { callObserverModel } from '../session/client.js';
|
|
8
|
+
import { loadSystemPrompt } from './prompt-loader.js';
|
|
9
|
+
import { updateSnapshot } from './snapshot-builder.js';
|
|
10
|
+
import { buildUserPrompt } from './prompt-builder.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Analyze the current conversation and return a steering decision.
|
|
14
|
+
* Falls back to { action: "steer" } when the agent is idle to prevent it from staying stuck.
|
|
15
|
+
*/
|
|
16
|
+
export async function analyze(
|
|
17
|
+
ctx: ExtensionContext,
|
|
18
|
+
state: LoopState,
|
|
19
|
+
agentIsIdle: boolean,
|
|
20
|
+
ineffectivePattern?: { detected: boolean; similarCount: number; turnsSinceLastSteer: number },
|
|
21
|
+
signal?: AbortSignal,
|
|
22
|
+
onDelta?: (accumulated: string) => void
|
|
23
|
+
): Promise<SteeringDecision> {
|
|
24
|
+
const { prompt: systemPrompt } = loadSystemPrompt(ctx.cwd);
|
|
25
|
+
|
|
26
|
+
// Update snapshot incrementally
|
|
27
|
+
const snapshot = updateSnapshot(ctx, state);
|
|
28
|
+
const userPrompt = buildUserPrompt(state, snapshot, agentIsIdle, ineffectivePattern);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
return await callObserverModel(
|
|
32
|
+
ctx,
|
|
33
|
+
state.provider,
|
|
34
|
+
state.modelId,
|
|
35
|
+
systemPrompt,
|
|
36
|
+
userPrompt,
|
|
37
|
+
signal,
|
|
38
|
+
onDelta
|
|
39
|
+
);
|
|
40
|
+
} catch {
|
|
41
|
+
// When idle and analysis fails, nudge rather than silently do nothing
|
|
42
|
+
return agentIsIdle
|
|
43
|
+
? {
|
|
44
|
+
action: 'steer',
|
|
45
|
+
message: 'Please continue working toward the goal.',
|
|
46
|
+
reasoning: 'Analysis error',
|
|
47
|
+
confidence: 0,
|
|
48
|
+
}
|
|
49
|
+
: { action: 'continue', reasoning: 'Analysis error', confidence: 0 };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content block extraction and manipulation utilities.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ContentBlock } from '../types.js';
|
|
6
|
+
|
|
7
|
+
/** Extract ALL content blocks from message content - full fidelity including images and tool calls. */
|
|
8
|
+
export function extractAllBlocks(content: unknown): ContentBlock[] {
|
|
9
|
+
if (!Array.isArray(content)) return [];
|
|
10
|
+
return content
|
|
11
|
+
.map((b: any): ContentBlock | null => {
|
|
12
|
+
if (b.type === 'text' && b.text) {
|
|
13
|
+
return { type: 'text', text: b.text };
|
|
14
|
+
}
|
|
15
|
+
if (b.type === 'image' && b.source) {
|
|
16
|
+
return { type: 'image', source: b.source, mimeType: b.mimeType };
|
|
17
|
+
}
|
|
18
|
+
if (b.type === 'tool_use' || b.type === 'tool_call') {
|
|
19
|
+
return {
|
|
20
|
+
type: 'tool_call',
|
|
21
|
+
id: b.id || b.tool_use_id || 'unknown',
|
|
22
|
+
name: b.name || b.tool_name || 'unknown',
|
|
23
|
+
input: b.input || b.arguments || {},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (b.type === 'tool_result') {
|
|
27
|
+
return {
|
|
28
|
+
type: 'tool_result',
|
|
29
|
+
toolCallId: b.tool_use_id || b.toolCallId || 'unknown',
|
|
30
|
+
content: b.content || [],
|
|
31
|
+
isError: b.is_error || b.isError || false,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
})
|
|
36
|
+
.filter((b): b is ContentBlock => b !== null);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Extract text content from message - for backward compatibility. */
|
|
40
|
+
export function extractText(content: unknown): string {
|
|
41
|
+
if (typeof content === 'string') return content;
|
|
42
|
+
if (Array.isArray(content)) {
|
|
43
|
+
return content
|
|
44
|
+
.filter((b: any) => b.type === 'text')
|
|
45
|
+
.map((b: any) => b.text as string)
|
|
46
|
+
.join('\n')
|
|
47
|
+
.trim();
|
|
48
|
+
}
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Extract text from assistant message content - for backward compatibility. */
|
|
53
|
+
export function extractAssistantText(content: unknown): string {
|
|
54
|
+
if (!Array.isArray(content)) return '';
|
|
55
|
+
const textParts = content.filter((b: any) => b.type === 'text').map((b: any) => b.text as string);
|
|
56
|
+
return textParts.join('\n').trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extract metrics from conversation text.
|
|
61
|
+
* Simple pass-through: the LLM supervisor can read the raw text.
|
|
62
|
+
* Only explicitly marked METRIC lines are extracted for convenience.
|
|
63
|
+
*/
|
|
64
|
+
export function extractMetrics(text: string): Record<string, number> {
|
|
65
|
+
const metrics: Record<string, number> = {};
|
|
66
|
+
|
|
67
|
+
// Pattern: "METRIC name=value" (autoresearch-style - explicit marker)
|
|
68
|
+
const metricLines = text.match(/METRIC\s+(\w+)\s*=\s*([\d.]+)/g);
|
|
69
|
+
if (metricLines) {
|
|
70
|
+
for (const line of metricLines) {
|
|
71
|
+
const match = line.match(/METRIC\s+(\w+)\s*=\s*([\d.]+)/);
|
|
72
|
+
if (match) {
|
|
73
|
+
metrics[match[1]] = parseFloat(match[2]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return metrics;
|
|
79
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outcome inference from conversation history.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import type { ConversationMessage } from '../types.js';
|
|
7
|
+
import { LoopSession } from '../session/loop-session.js';
|
|
8
|
+
import { extractAllBlocks, extractText, extractAssistantText } from './content-extractor.js';
|
|
9
|
+
import { SNAPSHOT_LIMIT } from './snapshot-builder.js';
|
|
10
|
+
|
|
11
|
+
/** System prompt for inferring an outcome from conversation history. */
|
|
12
|
+
const INFER_OUTCOME_SYSTEM_PROMPT = `You are a goal extraction assistant. Your task is to analyze a conversation between a user and a coding AI assistant, and extract the user's primary desired outcome or goal.
|
|
13
|
+
|
|
14
|
+
The outcome should be:
|
|
15
|
+
- Specific and measurable (not vague like "make it better")
|
|
16
|
+
- Action-oriented (what needs to be built, fixed, or achieved)
|
|
17
|
+
- Concise (1-2 sentences, ideally under 100 characters)
|
|
18
|
+
- Focused on the core intent, not implementation details
|
|
19
|
+
|
|
20
|
+
Examples of good outcomes:
|
|
21
|
+
- "Add JWT authentication with refresh tokens and test coverage"
|
|
22
|
+
- "Refactor the database layer to use connection pooling"
|
|
23
|
+
- "Fix the memory leak in the file upload handler"
|
|
24
|
+
- "Implement dark mode toggle with system preference detection"
|
|
25
|
+
|
|
26
|
+
Respond with ONLY the outcome statement. No quotes, no markdown, no explanations.`;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Infer a supervision outcome from the conversation history.
|
|
30
|
+
* Returns null if inference fails or there's no conversation to analyze.
|
|
31
|
+
*/
|
|
32
|
+
export async function inferOutcome(
|
|
33
|
+
ctx: ExtensionContext,
|
|
34
|
+
provider: string,
|
|
35
|
+
modelId: string,
|
|
36
|
+
signal?: AbortSignal
|
|
37
|
+
): Promise<string | null> {
|
|
38
|
+
// Build a focused snapshot for the immediate goal (last 6 messages = ~3 turns)
|
|
39
|
+
const entries = ctx.sessionManager.getBranch();
|
|
40
|
+
const messages: ConversationMessage[] = [];
|
|
41
|
+
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
// Capture regular messages
|
|
44
|
+
if (entry.type === 'message') {
|
|
45
|
+
const msg = (entry as any).message;
|
|
46
|
+
if (!msg) continue;
|
|
47
|
+
|
|
48
|
+
if (msg.role === 'user') {
|
|
49
|
+
const textContent = extractText(msg.content);
|
|
50
|
+
const allBlocks = extractAllBlocks(msg.content);
|
|
51
|
+
if (textContent || allBlocks.length > 0) {
|
|
52
|
+
messages.push({ role: 'user', content: textContent, blocks: allBlocks });
|
|
53
|
+
}
|
|
54
|
+
} else if (msg.role === 'assistant') {
|
|
55
|
+
const textContent = extractAssistantText(msg.content);
|
|
56
|
+
const allBlocks = extractAllBlocks(msg.content);
|
|
57
|
+
if (textContent || allBlocks.length > 0) {
|
|
58
|
+
messages.push({ role: 'assistant', content: textContent, blocks: allBlocks });
|
|
59
|
+
}
|
|
60
|
+
} else if (msg.role === 'tool') {
|
|
61
|
+
// Include tool results for context
|
|
62
|
+
const textContent = extractText(msg.content);
|
|
63
|
+
const allBlocks = extractAllBlocks(msg.content);
|
|
64
|
+
if (textContent || allBlocks.length > 0) {
|
|
65
|
+
messages.push({ role: 'tool_results', content: textContent, blocks: allBlocks });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Capture custom_message entries (often contain tool results)
|
|
71
|
+
if (entry.type === 'custom_message') {
|
|
72
|
+
const customMsg = entry as any;
|
|
73
|
+
const content = customMsg.content;
|
|
74
|
+
|
|
75
|
+
if (typeof content === 'string') {
|
|
76
|
+
messages.push({
|
|
77
|
+
role: 'tool_results',
|
|
78
|
+
content: content,
|
|
79
|
+
blocks: [{ type: 'text', text: content }],
|
|
80
|
+
});
|
|
81
|
+
} else if (Array.isArray(content)) {
|
|
82
|
+
const allBlocks = extractAllBlocks(content);
|
|
83
|
+
const textContent = content
|
|
84
|
+
.filter((b: any) => b.type === 'text')
|
|
85
|
+
.map((b: any) => b.text)
|
|
86
|
+
.join('\n');
|
|
87
|
+
messages.push({
|
|
88
|
+
role: 'tool_results',
|
|
89
|
+
content: textContent || `[${customMsg.customType}]`,
|
|
90
|
+
blocks: allBlocks,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const snapshot = messages.slice(-SNAPSHOT_LIMIT);
|
|
97
|
+
if (snapshot.length === 0) return null;
|
|
98
|
+
|
|
99
|
+
const conversationText = snapshot
|
|
100
|
+
.map(
|
|
101
|
+
(m) =>
|
|
102
|
+
`${m.role === 'user' ? 'USER' : m.role === 'assistant' ? 'ASSISTANT' : 'TOOL RESULTS'}: ${m.content}`
|
|
103
|
+
)
|
|
104
|
+
.join('\n\n---\n\n');
|
|
105
|
+
|
|
106
|
+
const userPrompt = `Analyze this conversation and extract the user's primary goal or desired outcome:
|
|
107
|
+
|
|
108
|
+
${conversationText}
|
|
109
|
+
|
|
110
|
+
What is the specific outcome the user is trying to achieve?`;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// Use fresh LoopSession (not callObserverModel) to avoid
|
|
114
|
+
// interfering with the global loop session
|
|
115
|
+
const session = new LoopSession();
|
|
116
|
+
const started = await session.ensureStarted(
|
|
117
|
+
ctx,
|
|
118
|
+
provider,
|
|
119
|
+
modelId,
|
|
120
|
+
INFER_OUTCOME_SYSTEM_PROMPT
|
|
121
|
+
);
|
|
122
|
+
if (!started) return null;
|
|
123
|
+
|
|
124
|
+
const result = await session.prompt(userPrompt, signal);
|
|
125
|
+
session.dispose();
|
|
126
|
+
|
|
127
|
+
if (!result) return null;
|
|
128
|
+
// Clean up the result: remove quotes, trim whitespace, limit length
|
|
129
|
+
return result
|
|
130
|
+
.replace(/^["']|["']$/g, '') // Remove surrounding quotes
|
|
131
|
+
.replace(/\n/g, ' ') // Replace newlines with spaces
|
|
132
|
+
.trim()
|
|
133
|
+
.slice(0, 200); // Hard limit at 200 chars
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|