piocloop 0.1.0__tar.gz → 0.1.1__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.
- {piocloop-0.1.0 → piocloop-0.1.1}/.gitignore +2 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/PKG-INFO +1 -1
- {piocloop-0.1.0 → piocloop-0.1.1}/pyproject.toml +1 -1
- {piocloop-0.1.0 → piocloop-0.1.1}/src/piocloop/cli.py +4 -1
- {piocloop-0.1.0 → piocloop-0.1.1}/src/piocloop/loop.py +14 -1
- {piocloop-0.1.0 → piocloop-0.1.1}/src/piocloop/pi_client.py +17 -1
- {piocloop-0.1.0 → piocloop-0.1.1}/tests/fake_pi.py +5 -1
- {piocloop-0.1.0 → piocloop-0.1.1}/tests/test_pi_client.py +6 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/.github/workflows/publish.yml +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/ANALYSIS.md +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/DESIGN.md +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/LICENSE +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/PLAN.md +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/README.md +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/skills/README.md +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/skills/piocloop-setup/SKILL.md +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/skills/piocloop-setup/assets/PLAN.template.md +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/skills/piocloop-setup/assets/loop-prompt.template.md +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/skills/piocloop-setup/references/PLAN-FORMAT.md +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/skills/piocloop-setup/references/PROMPT-AUTHORING.md +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/src/piocloop/__init__.py +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/src/piocloop/__main__.py +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/src/piocloop/pi_events.py +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/src/piocloop/plan_parser.py +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/src/piocloop/tui.py +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/tests/test_loop.py +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/tests/test_pi_events.py +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/tests/test_plan_parser.py +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/tests/test_skill_assets.py +0 -0
- {piocloop-0.1.0 → piocloop-0.1.1}/tests/test_tui.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: piocloop
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: TUI loop harness that orchestrates the PI coding agent to execute tasks from a PLAN.md file iteratively
|
|
5
5
|
Project-URL: Homepage, https://github.com/rbarzic/piocloop
|
|
6
6
|
Project-URL: Repository, https://github.com/rbarzic/piocloop
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "piocloop"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.1"
|
|
8
8
|
description = "TUI loop harness that orchestrates the PI coding agent to execute tasks from a PLAN.md file iteratively"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -101,7 +101,10 @@ def run(
|
|
|
101
101
|
help="How to answer blocking extension dialogs: cancel|allow|deny",
|
|
102
102
|
),
|
|
103
103
|
session_dir: Optional[Path] = typer.Option(None, "--session-dir", help="Passed through to pi"),
|
|
104
|
-
no_session: bool = typer.Option(
|
|
104
|
+
no_session: bool = typer.Option(
|
|
105
|
+
True, "--no-session/--persist-sessions",
|
|
106
|
+
help="Use an ephemeral pi session (default); --persist-sessions enables session files",
|
|
107
|
+
),
|
|
105
108
|
tools: Optional[str] = typer.Option(None, "--tools", help="Comma-separated tool allowlist"),
|
|
106
109
|
exclude_tools: Optional[str] = typer.Option(
|
|
107
110
|
None, "--exclude-tools", help="Comma-separated tool denylist"
|
|
@@ -319,7 +319,20 @@ class LoopEngine:
|
|
|
319
319
|
self._settled.clear()
|
|
320
320
|
self._seen_agent_start = False
|
|
321
321
|
|
|
322
|
-
|
|
322
|
+
try:
|
|
323
|
+
await self.client.new_session()
|
|
324
|
+
except (PiTimeout, PiExited) as exc:
|
|
325
|
+
# A session-switch timeout is not a recoverable iteration error:
|
|
326
|
+
# pi can have accepted the switch while losing its response, so
|
|
327
|
+
# retrying on the same process is unsafe and used to produce the
|
|
328
|
+
# characteristic three consecutive ``new_session`` failures.
|
|
329
|
+
# Replace the process, with start() performing a readiness
|
|
330
|
+
# handshake before the next iteration reaches this point.
|
|
331
|
+
self.ui.log_line("warn", f"new_session failed; restarting pi: {exc}")
|
|
332
|
+
if self.respawns >= self.config.max_respawns:
|
|
333
|
+
raise
|
|
334
|
+
await self._respawn_client()
|
|
335
|
+
raise
|
|
323
336
|
self.iteration += 1
|
|
324
337
|
self.iter_start_time = time.monotonic()
|
|
325
338
|
self.ui.log_line("start", f"Iteration {self.iteration}")
|
|
@@ -137,7 +137,17 @@ class PiClient:
|
|
|
137
137
|
# Lifecycle
|
|
138
138
|
# ------------------------------------------------------------------
|
|
139
139
|
|
|
140
|
-
async def start(self) -> None:
|
|
140
|
+
async def start(self, *, readiness_timeout: float = 60.0) -> None:
|
|
141
|
+
"""Start pi and wait until its RPC session is usable.
|
|
142
|
+
|
|
143
|
+
``create_subprocess_exec`` only means that the OS process exists. Pi
|
|
144
|
+
still has to load its model catalogue, session and extensions before
|
|
145
|
+
it starts consuming RPC commands. Sending ``new_session`` during
|
|
146
|
+
that window is particularly fragile with persistent sessions: the
|
|
147
|
+
command can sit unhandled until the caller's deadline expires. A
|
|
148
|
+
bounded get_state handshake makes readiness explicit and also gives
|
|
149
|
+
respawned clients the same startup guarantee as the first client.
|
|
150
|
+
"""
|
|
141
151
|
if self._proc is not None:
|
|
142
152
|
raise PiError("PiClient already started")
|
|
143
153
|
try:
|
|
@@ -158,6 +168,12 @@ class PiClient:
|
|
|
158
168
|
asyncio.create_task(self._read_stdout(), name="pi-stdout"),
|
|
159
169
|
asyncio.create_task(self._read_stderr(), name="pi-stderr"),
|
|
160
170
|
]
|
|
171
|
+
try:
|
|
172
|
+
# Do not let callers race startup with the first session switch.
|
|
173
|
+
await self.request({"type": "get_state"}, timeout=readiness_timeout)
|
|
174
|
+
except Exception:
|
|
175
|
+
await self.close()
|
|
176
|
+
raise
|
|
161
177
|
|
|
162
178
|
async def close(self, timeout: float = 5.0) -> None:
|
|
163
179
|
"""Shut the process down. Idempotent, and safe to call after a crash."""
|
|
@@ -17,6 +17,7 @@ import time
|
|
|
17
17
|
# fallback cannot mistake a stuck run for a finished one.
|
|
18
18
|
_PRETEND_STREAMING = {"busy", "wedged"}
|
|
19
19
|
_streaming = False
|
|
20
|
+
_ready = False
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def work_the_plan(message: str) -> None:
|
|
@@ -86,7 +87,7 @@ def settle() -> None:
|
|
|
86
87
|
|
|
87
88
|
|
|
88
89
|
def main() -> int:
|
|
89
|
-
global _streaming
|
|
90
|
+
global _streaming, _ready
|
|
90
91
|
scenario = sys.argv[1] if len(sys.argv) > 1 else "normal"
|
|
91
92
|
|
|
92
93
|
while True:
|
|
@@ -116,11 +117,14 @@ def main() -> int:
|
|
|
116
117
|
continue
|
|
117
118
|
|
|
118
119
|
if ctype == "get_state":
|
|
120
|
+
_ready = True
|
|
119
121
|
ok("get_state", rid,
|
|
120
122
|
{"isStreaming": _streaming, "sessionId": "fake", "messageCount": 0})
|
|
121
123
|
continue
|
|
122
124
|
|
|
123
125
|
if ctype == "new_session":
|
|
126
|
+
if scenario == "readiness" and not _ready:
|
|
127
|
+
continue
|
|
124
128
|
ok("new_session", rid, {"cancelled": False})
|
|
125
129
|
continue
|
|
126
130
|
|
|
@@ -126,6 +126,12 @@ async def test_new_session_and_abort():
|
|
|
126
126
|
await collect_until(client, "agent_settled")
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
async def test_start_waits_for_rpc_readiness_before_new_session():
|
|
130
|
+
"""Startup must complete before the first session replacement is sent."""
|
|
131
|
+
async with PiClient(fake_argv("readiness")) as client:
|
|
132
|
+
assert await client.new_session() == {"cancelled": False}
|
|
133
|
+
|
|
134
|
+
|
|
129
135
|
async def test_concurrent_requests_are_correlated():
|
|
130
136
|
"""Interleaved commands must not cross-resolve each other's futures."""
|
|
131
137
|
async with PiClient(fake_argv("normal")) as client:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|