@brettinternet/pi-loop 0.1.4 → 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 -25
- package/index.ts +55 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,45 +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
|
-
Errors retry after 30 seconds, 1 minute, and 2 minutes, then pause. Aborting pauses the loop. `/loop pause` pauses after the active iteration settles; if the loop is between iterations, it pauses immediately. Resuming then starts the next iteration. An agent `loop_pause` request pauses mid-iteration for human blockers, and resuming continues that iteration. Recovery preserves the loop so you can resume or advance it.
|
|
16
|
+
The last example chains commands. Built-in interactive commands can't be chained.
|
|
30
17
|
|
|
31
|
-
|
|
18
|
+
## Control a running loop
|
|
32
19
|
|
|
33
20
|
```text
|
|
34
|
-
/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
|
|
35
34
|
```
|
|
36
35
|
|
|
37
|
-
|
|
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
|
|
38
54
|
|
|
39
|
-
|
|
55
|
+
Every command run during a loop gets the loop's ID:
|
|
40
56
|
|
|
41
57
|
```text
|
|
42
58
|
PI_LOOP_RUN_ID=3992f183-e054-4068-a13e-11281d1747e2
|
|
43
59
|
```
|
|
44
60
|
|
|
45
|
-
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
|
|
|
@@ -639,6 +648,24 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
639
648
|
let pendingFailure: PendingFailure | undefined;
|
|
640
649
|
let currentSessionManagerRef: unknown;
|
|
641
650
|
let herdrBlocked = false;
|
|
651
|
+
const pendingWakes = new Map<string, boolean>();
|
|
652
|
+
let waitingForWake = false;
|
|
653
|
+
let wakeContext: ExtensionContext | undefined;
|
|
654
|
+
|
|
655
|
+
for (const channel of ["pi-until:busy", "pi-wait:busy"]) {
|
|
656
|
+
pi.events.on(channel, (value) => {
|
|
657
|
+
if (typeof value !== "boolean") return;
|
|
658
|
+
pendingWakes.set(channel, value);
|
|
659
|
+
if (!waitingForWake || [...pendingWakes.values()].some(Boolean)) return;
|
|
660
|
+
const ctx = wakeContext;
|
|
661
|
+
if (!ctx || !ctx.isIdle()) return;
|
|
662
|
+
const state = currentState(ctx);
|
|
663
|
+
if (!state || !statusIsActive(state) || transitionInFlight) return;
|
|
664
|
+
waitingForWake = false;
|
|
665
|
+
handledSettlementKey = stateKey(ctx, state);
|
|
666
|
+
scheduleContinuation(ctx, state);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
642
669
|
|
|
643
670
|
function reportHerdrBlocked(state: LoopState | undefined): void {
|
|
644
671
|
const blocked = state?.status === "paused";
|
|
@@ -946,14 +973,14 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
946
973
|
replacement: ReplacementContext,
|
|
947
974
|
state: LoopState,
|
|
948
975
|
): Promise<void> {
|
|
949
|
-
// The replacement owns the new extension runtime. Restore its model
|
|
950
|
-
// starting a turn, rather than using the
|
|
951
|
-
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) {
|
|
952
979
|
await replacement.sendUserMessage(
|
|
953
980
|
`/loop __restore_model ${state.runId} ${state.currentIteration}`,
|
|
954
981
|
{ expandPromptTemplates: true },
|
|
955
982
|
);
|
|
956
|
-
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)) {
|
|
957
984
|
throw new Error(`loop model unavailable: ${state.model.provider}/${state.model.id}`);
|
|
958
985
|
}
|
|
959
986
|
}
|
|
@@ -1131,6 +1158,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1131
1158
|
const next: LoopState = {
|
|
1132
1159
|
...withoutPause,
|
|
1133
1160
|
...(ctx.model ? { model: { provider: ctx.model.provider, id: ctx.model.id } } : {}),
|
|
1161
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
1134
1162
|
currentIteration: state.currentIteration + 1,
|
|
1135
1163
|
remainingBudget: state.endsAt === undefined ? nextBudget - 1 : 0,
|
|
1136
1164
|
pendingRetune: null,
|
|
@@ -1184,11 +1212,14 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1184
1212
|
|
|
1185
1213
|
if (parsed.kind === "restoreModel") {
|
|
1186
1214
|
const state = currentState(ctx);
|
|
1187
|
-
if (!state || state.runId !== parsed.runId || state.currentIteration !== parsed.iteration
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
+
}
|
|
1191
1221
|
}
|
|
1222
|
+
if (state.thinkingLevel !== undefined) pi.setThinkingLevel(state.thinkingLevel);
|
|
1192
1223
|
return;
|
|
1193
1224
|
}
|
|
1194
1225
|
|
|
@@ -1476,6 +1507,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1476
1507
|
retryCount: 0,
|
|
1477
1508
|
phase: "running",
|
|
1478
1509
|
...(ctx.model ? { model: { provider: ctx.model.provider, id: ctx.model.id } } : {}),
|
|
1510
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
1479
1511
|
...(timed ? { endsAt: Date.now() + parsed.duration } : {}),
|
|
1480
1512
|
...(contextIdentity(ctx).id ? { ownerSessionId: contextIdentity(ctx).id } : {}),
|
|
1481
1513
|
...(contextIdentity(ctx).file ? { ownerSessionFile: contextIdentity(ctx).file } : {}),
|
|
@@ -1523,6 +1555,9 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1523
1555
|
clearCommandInterruption();
|
|
1524
1556
|
pendingFailure = undefined;
|
|
1525
1557
|
currentSessionManagerRef = ctx.sessionManager;
|
|
1558
|
+
pendingWakes.clear();
|
|
1559
|
+
waitingForWake = false;
|
|
1560
|
+
wakeContext = ctx;
|
|
1526
1561
|
transitionInFlight = false;
|
|
1527
1562
|
handledSettlementKey = undefined;
|
|
1528
1563
|
const loaded = latestStateFromContext(ctx);
|
|
@@ -1593,6 +1628,12 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1593
1628
|
return;
|
|
1594
1629
|
}
|
|
1595
1630
|
if (handledSettlementKey === key) return;
|
|
1631
|
+
if ([...pendingWakes.values()].some(Boolean)) {
|
|
1632
|
+
waitingForWake = true;
|
|
1633
|
+
wakeContext = ctx;
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
waitingForWake = false;
|
|
1596
1637
|
handledSettlementKey = key;
|
|
1597
1638
|
scheduleContinuation(ctx, loaded);
|
|
1598
1639
|
});
|
|
@@ -1604,6 +1645,9 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1604
1645
|
clearCommandInterruption();
|
|
1605
1646
|
pendingFailure = undefined;
|
|
1606
1647
|
currentSessionManagerRef = ctx.sessionManager;
|
|
1648
|
+
pendingWakes.clear();
|
|
1649
|
+
waitingForWake = false;
|
|
1650
|
+
wakeContext = ctx;
|
|
1607
1651
|
handledSettlementKey = undefined;
|
|
1608
1652
|
const loaded = latestStateFromContext(ctx);
|
|
1609
1653
|
const owned = loaded && stateBelongsToContext(loaded, ctx) ? loaded : undefined;
|
|
@@ -1633,6 +1677,9 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
1633
1677
|
reportHerdrBlocked(undefined);
|
|
1634
1678
|
clearWidget(ctx);
|
|
1635
1679
|
currentSessionManagerRef = undefined;
|
|
1680
|
+
waitingForWake = false;
|
|
1681
|
+
wakeContext = undefined;
|
|
1682
|
+
pendingWakes.clear();
|
|
1636
1683
|
});
|
|
1637
1684
|
|
|
1638
1685
|
pi.registerCommand("loop", {
|