@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.
Files changed (3) hide show
  1. package/README.md +41 -27
  2. package/index.ts +22 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,47 +1,61 @@
1
1
  # pi-loop
2
2
 
3
- Run a prompt repeatedly, with a fresh session for every iteration. Each new session uses the model selected in the preceding session (including a custom non-default model); changing models mid-loop takes effect on the next iteration. If that model is unavailable, the loop pauses rather than falling back to the default.
3
+ Run a prompt repeatedly, each iteration in a fresh session.
4
4
 
5
- ```bash
5
+ ```sh
6
6
  pi install npm:@brettinternet/pi-loop
7
7
  ```
8
8
 
9
9
  ```text
10
- /loop <count> [--delay <duration>] <prompt>
11
- /loop for <duration> --delay <duration> <prompt>
12
- /loop <count>
13
- /loop +<count>
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
- Durations use `ms`, `s`, `m`, `h`, or `d`. Delays range from 1 second to 24 hours. Timed loops run for at most 30 days. During a loop, `/loop time <duration>` switches to a deadline from now or resets the current deadline; timed mode requires a non-zero delay. `/loop <count>` switches back to a count of future iterations.
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
- 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.
32
-
33
- Chain commands in the prompt:
18
+ ## Control a running loop
34
19
 
35
20
  ```text
36
- /loop 10 /wait 10m /skill:myskill skill argument here
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
- Built-in interactive commands cannot be chained.
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
- While a loop is active, every child command receives its stable run ID as `PI_LOOP_RUN_ID`:
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 value is always the full UUID, remains unchanged across every iteration and replacement session in that loop, and differs between independent loops. When the loop ends or its session closes, pi-loop restores the previous value or removes the variable if it was previously unset.
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 before
968
- // starting a turn, rather than using the invalidated previous runtime.
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 || !state.model) return;
1206
- const model = ctx.modelRegistry.find(state.model.provider, state.model.id);
1207
- if (!model || !(await pi.setModel(model))) {
1208
- throw new Error(`loop model unavailable: ${state.model.provider}/${state.model.id}`);
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 } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brettinternet/pi-loop",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Run a prompt repeatedly in fresh Pi sessions",
5
5
  "type": "module",
6
6
  "license": "MIT",