@brettinternet/pi-loop 0.1.5 → 0.1.6
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/README.md +41 -27
- package/index.ts +22 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,47 +1,61 @@
|
|
|
1
1
|
# pi-loop
|
|
2
2
|
|
|
3
|
-
Run a prompt repeatedly,
|
|
3
|
+
Run a prompt repeatedly, each iteration in a fresh session.
|
|
4
4
|
|
|
5
|
-
```
|
|
5
|
+
```sh
|
|
6
6
|
pi install npm:@brettinternet/pi-loop
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
|
-
/loop
|
|
11
|
-
/loop
|
|
12
|
-
/loop
|
|
13
|
-
/loop
|
|
14
|
-
/loop -<count>
|
|
15
|
-
/loop time <duration>
|
|
16
|
-
/loop delay <duration>
|
|
17
|
-
/loop prompt <text>
|
|
18
|
-
/loop append <text>
|
|
19
|
-
/loop status
|
|
20
|
-
/loop
|
|
21
|
-
/loop pause
|
|
22
|
-
/loop end
|
|
23
|
-
/loop resume
|
|
24
|
-
/loop next
|
|
10
|
+
/loop 5 Fix the next failing test
|
|
11
|
+
/loop 10 --delay 30m Check CI and fix failures
|
|
12
|
+
/loop for 8h --delay 1h Review new issues
|
|
13
|
+
/loop 10 /wait 10m /skill:myskill skill argument here
|
|
25
14
|
```
|
|
26
15
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
A pending `/wait` or `until` watch keeps the current iteration's session alive until its wake-up turn settles (or it is cancelled). A paused wait or recurring watch holds the iteration until resumed or completed. Use `/loop delay` for a simple fixed gap between iterations; use `/wait` for a same-session follow-up and `until` for a condition that might become true earlier than a fixed deadline.
|
|
16
|
+
The last example chains commands. Built-in interactive commands can't be chained.
|
|
30
17
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Chain commands in the prompt:
|
|
18
|
+
## Control a running loop
|
|
34
19
|
|
|
35
20
|
```text
|
|
36
|
-
/loop
|
|
21
|
+
/loop status show state
|
|
22
|
+
/loop 3 run 3 more iterations from now
|
|
23
|
+
/loop +2 add 2 iterations
|
|
24
|
+
/loop -1 remove 1 iteration
|
|
25
|
+
/loop time 2h stop 2h from now (requires a delay)
|
|
26
|
+
/loop delay 5m change the gap between iterations
|
|
27
|
+
/loop prompt <text> replace the prompt
|
|
28
|
+
/loop append <text> append to the prompt
|
|
29
|
+
/loop pause pause after this iteration
|
|
30
|
+
/loop resume resume
|
|
31
|
+
/loop next skip a paused iteration and start the next
|
|
32
|
+
/loop end stop gracefully
|
|
33
|
+
/loop same as end
|
|
37
34
|
```
|
|
38
35
|
|
|
39
|
-
|
|
36
|
+
Durations use `ms`, `s`, `m`, `h`, or `d`. Delays range from 1s to 24h. Timed loops require `--delay` and run at most 30 days.
|
|
37
|
+
|
|
38
|
+
## Behavior
|
|
39
|
+
|
|
40
|
+
| Event | Result |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| Model or thinking level changed mid-loop | The next iteration uses it; both initially match the session that invoked `/loop` |
|
|
43
|
+
| Model unavailable | Loop pauses; no fallback to the default |
|
|
44
|
+
| Error | Retries after 30s, 1m, and 2m, then pauses |
|
|
45
|
+
| Abort | Loop pauses |
|
|
46
|
+
| `/loop pause` between iterations | Pauses immediately |
|
|
47
|
+
| Agent calls `loop_pause` | Pauses mid-iteration; resume continues that iteration |
|
|
48
|
+
| Pending `/wait` or `until` watch | Iteration waits for its wake-up turn or cancellation |
|
|
49
|
+
| Paused wait or recurring watch | Iteration waits until resumed or completed |
|
|
50
|
+
|
|
51
|
+
Use `/loop delay` for a fixed gap, `/wait` for a same-session follow-up, and `until` for a condition that may become true sooner.
|
|
52
|
+
|
|
53
|
+
## Run ID
|
|
40
54
|
|
|
41
|
-
|
|
55
|
+
Every command run during a loop gets the loop's ID:
|
|
42
56
|
|
|
43
57
|
```text
|
|
44
58
|
PI_LOOP_RUN_ID=3992f183-e054-4068-a13e-11281d1747e2
|
|
45
59
|
```
|
|
46
60
|
|
|
47
|
-
The
|
|
61
|
+
The UUID stays the same for every iteration of one loop and differs between loops. When the loop ends, the previous value is restored or the variable is removed.
|
package/index.ts
CHANGED
|
@@ -9,6 +9,8 @@ import type {
|
|
|
9
9
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
10
10
|
import { Type } from "typebox";
|
|
11
11
|
|
|
12
|
+
type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
|
|
13
|
+
|
|
12
14
|
export const LOOP_STATE_ENTRY = "pi-loop-state-v1";
|
|
13
15
|
export const LOOP_WIDGET_KEY = "pi-loop";
|
|
14
16
|
export const LOOP_RUN_ID_ENV = "PI_LOOP_RUN_ID";
|
|
@@ -49,6 +51,7 @@ export interface LoopState {
|
|
|
49
51
|
ownerSessionId?: string;
|
|
50
52
|
ownerSessionFile?: string;
|
|
51
53
|
model?: { provider: string; id: string };
|
|
54
|
+
thinkingLevel?: ThinkingLevel;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
export type ParsedLoopCommand =
|
|
@@ -445,6 +448,11 @@ export function parseLoopState(value: unknown): LoopState | undefined {
|
|
|
445
448
|
if (value.ownerSessionId !== undefined && typeof value.ownerSessionId !== "string") return undefined;
|
|
446
449
|
if (value.ownerSessionFile !== undefined && typeof value.ownerSessionFile !== "string") return undefined;
|
|
447
450
|
const model = value.model;
|
|
451
|
+
const thinkingLevel = value.thinkingLevel;
|
|
452
|
+
if (
|
|
453
|
+
thinkingLevel !== undefined &&
|
|
454
|
+
!["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(thinkingLevel as string)
|
|
455
|
+
) return undefined;
|
|
448
456
|
if (
|
|
449
457
|
model !== undefined &&
|
|
450
458
|
(!isRecord(model) || typeof model.provider !== "string" || !model.provider ||
|
|
@@ -472,6 +480,7 @@ export function parseLoopState(value: unknown): LoopState | undefined {
|
|
|
472
480
|
...(isRecord(model) && typeof model.provider === "string" && typeof model.id === "string"
|
|
473
481
|
? { model: { provider: model.provider, id: model.id } }
|
|
474
482
|
: {}),
|
|
483
|
+
...(thinkingLevel !== undefined ? { thinkingLevel: thinkingLevel as ThinkingLevel } : {}),
|
|
475
484
|
};
|
|
476
485
|
}
|
|
477
486
|
|
|
@@ -964,14 +973,14 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
964
973
|
replacement: ReplacementContext,
|
|
965
974
|
state: LoopState,
|
|
966
975
|
): Promise<void> {
|
|
967
|
-
// The replacement owns the new extension runtime. Restore its model
|
|
968
|
-
// starting a turn, rather than using the
|
|
969
|
-
if (state.model) {
|
|
976
|
+
// The replacement owns the new extension runtime. Restore its model and
|
|
977
|
+
// thinking level before starting a turn, rather than using the previous runtime.
|
|
978
|
+
if (state.model || state.thinkingLevel !== undefined) {
|
|
970
979
|
await replacement.sendUserMessage(
|
|
971
980
|
`/loop __restore_model ${state.runId} ${state.currentIteration}`,
|
|
972
981
|
{ expandPromptTemplates: true },
|
|
973
982
|
);
|
|
974
|
-
if (replacement.model?.provider !== state.model.provider || replacement.model.id !== state.model.id) {
|
|
983
|
+
if (state.model && (replacement.model?.provider !== state.model.provider || replacement.model.id !== state.model.id)) {
|
|
975
984
|
throw new Error(`loop model unavailable: ${state.model.provider}/${state.model.id}`);
|
|
976
985
|
}
|
|
977
986
|
}
|
|
@@ -1149,6 +1158,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1149
1158
|
const next: LoopState = {
|
|
1150
1159
|
...withoutPause,
|
|
1151
1160
|
...(ctx.model ? { model: { provider: ctx.model.provider, id: ctx.model.id } } : {}),
|
|
1161
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
1152
1162
|
currentIteration: state.currentIteration + 1,
|
|
1153
1163
|
remainingBudget: state.endsAt === undefined ? nextBudget - 1 : 0,
|
|
1154
1164
|
pendingRetune: null,
|
|
@@ -1202,11 +1212,14 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1202
1212
|
|
|
1203
1213
|
if (parsed.kind === "restoreModel") {
|
|
1204
1214
|
const state = currentState(ctx);
|
|
1205
|
-
if (!state || state.runId !== parsed.runId || state.currentIteration !== parsed.iteration
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1215
|
+
if (!state || state.runId !== parsed.runId || state.currentIteration !== parsed.iteration) return;
|
|
1216
|
+
if (state.model) {
|
|
1217
|
+
const model = ctx.modelRegistry.find(state.model.provider, state.model.id);
|
|
1218
|
+
if (!model || !(await pi.setModel(model))) {
|
|
1219
|
+
throw new Error(`loop model unavailable: ${state.model.provider}/${state.model.id}`);
|
|
1220
|
+
}
|
|
1209
1221
|
}
|
|
1222
|
+
if (state.thinkingLevel !== undefined) pi.setThinkingLevel(state.thinkingLevel);
|
|
1210
1223
|
return;
|
|
1211
1224
|
}
|
|
1212
1225
|
|
|
@@ -1494,6 +1507,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1494
1507
|
retryCount: 0,
|
|
1495
1508
|
phase: "running",
|
|
1496
1509
|
...(ctx.model ? { model: { provider: ctx.model.provider, id: ctx.model.id } } : {}),
|
|
1510
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
1497
1511
|
...(timed ? { endsAt: Date.now() + parsed.duration } : {}),
|
|
1498
1512
|
...(contextIdentity(ctx).id ? { ownerSessionId: contextIdentity(ctx).id } : {}),
|
|
1499
1513
|
...(contextIdentity(ctx).file ? { ownerSessionFile: contextIdentity(ctx).file } : {}),
|