@nothumanwork/pi-canary 0.1.0 → 0.1.1
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 +8 -3
- package/canary.ts +32 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,7 +41,12 @@ Thinking/reasoning blocks, structured tool calls and their arguments, and tool r
|
|
|
41
41
|
|
|
42
42
|
## Recovery
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
The prefix is checked after each assistant message. A handover is only useful while the agent is still working, so recovery depends on whether that message ends the turn:
|
|
45
|
+
|
|
46
|
+
- **Mid-run message** (it has tool calls, so the agent will continue): recovery starts immediately, as below. Tool calls in the offending message have already run; tool calls after that are blocked.
|
|
47
|
+
- **Final message** (no tool calls, so the agent is ending its turn): a warning is shown, and nothing else happens. No handover request, tool blocking, compaction, or continuation.
|
|
48
|
+
|
|
49
|
+
Recovery sequence:
|
|
45
50
|
|
|
46
51
|
1. Flag the failure with a visible instruction and require the agent's next response to be a self-contained handover prompt. Tool execution is blocked during this phase.
|
|
47
52
|
2. Accept a complete, correctly prefixed, tool-free response beginning with `chirp: # Canary handover`, followed by a newline and handover content.
|
|
@@ -50,7 +55,7 @@ At the end of the agent's response to a user message, a missing prefix triggers
|
|
|
50
55
|
|
|
51
56
|
“Restart” means **fresh model context within the same session file**, not a new Pi process or new session file. The restarted model receives the system/tool checkpoint, compaction summary, and handover. Older conversation entries remain in the session history but are not replayed into the restarted context. Successful summary usage is attached to the compaction entry. Known limitation: usage from rejected or interrupted summary requests is not included in Pi's session totals; the current extension API does not expose an independent usage-recording operation.
|
|
52
57
|
|
|
53
|
-
Recovery allows at most two handover responses and one automatic restart per user message. The handover-attempt budget also applies when an earlier extension requests continuation. A deferred handover pauses recovery immediately instead of submitting more requests while a provider job may still be running; this does not explicitly cancel or retrieve that provider job. Further violations pause recovery rather than creating a loop. Provider errors, missing/empty/truncated summaries, and user cancellation do not discard history or automatically restart. Queued user input takes priority; a new user message resets the recovery state. If recovery pauses, inspect the preserved handover/history and send a new instruction, or disable canary mode.
|
|
58
|
+
Recovery allows at most two handover responses and one automatic restart per user message. If the restarted agent misses the prefix mid-run, the run is stopped and recovery pauses. A miss on its final message only shows a warning. The handover-attempt budget also applies when an earlier extension requests continuation. A deferred handover pauses recovery immediately instead of submitting more requests while a provider job may still be running; this does not explicitly cancel or retrieve that provider job. Further violations pause recovery rather than creating a loop. Provider errors, missing/empty/truncated summaries, and user cancellation do not discard history or automatically restart. Queued user input takes priority; a new user message resets the recovery state. If recovery pauses, inspect the preserved handover/history and send a new instruction, or disable canary mode.
|
|
54
59
|
|
|
55
60
|
## Tests
|
|
56
61
|
|
|
@@ -66,6 +71,6 @@ For a standalone extension, point the tests at your installed coding-agent entry
|
|
|
66
71
|
PI_CANARY_SDK=/absolute/path/to/pi/dist/index.js bun test ./canary.test.ts
|
|
67
72
|
```
|
|
68
73
|
|
|
69
|
-
The tests cover keyword persistence and automatic enabling/reload, settings precedence, case preservation, malformed/locked/BOM-prefixed settings, Unicode separators, symlinks, cancelled command changes, exact-prefix checks, reasoning/tool exclusions, branch restoration, bounded/deferred handover attempts, compaction failures, cancellation, queued input, preservation of other extensions' boundary drafts, headless operation, and Pi's real file loader/lifecycle with a scripted, offline provider.
|
|
74
|
+
The tests cover keyword persistence and automatic enabling/reload, settings precedence, case preservation, malformed/locked/BOM-prefixed settings, Unicode separators, symlinks, cancelled command changes, exact-prefix checks, per-message checks (mid-run misses recover, final-message misses only warn), reasoning/tool exclusions, branch restoration, bounded/deferred handover attempts, compaction failures, cancellation, queued input, preservation of other extensions' boundary drafts, headless operation, and Pi's real file loader/lifecycle with a scripted, offline provider.
|
|
70
75
|
|
|
71
76
|
See [Measured changes](canary.value.md) for before/after results and proposals deliberately not retained. To run the same tests against an older implementation, set `PI_CANARY_EXTENSION` to its absolute file path; both the unit harness and real loader use that implementation.
|
package/canary.ts
CHANGED
|
@@ -279,8 +279,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
279
279
|
text.slice(`${recovery.keyword}: ${HANDOVER_HEADING}\n`.length).trim()
|
|
280
280
|
? text
|
|
281
281
|
: undefined;
|
|
282
|
-
} else
|
|
283
|
-
|
|
282
|
+
} else {
|
|
283
|
+
// Judged at this message's turn_end, once it is known whether the agent continues.
|
|
284
|
+
violated = message.stopReason !== "deferred" && !!text.trim() && !text.startsWith(`${keywordForSpan}: `);
|
|
284
285
|
}
|
|
285
286
|
});
|
|
286
287
|
|
|
@@ -290,17 +291,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
290
291
|
}
|
|
291
292
|
});
|
|
292
293
|
|
|
294
|
+
const requestHandover = (current: Recovery, entries: AgentBeforeSettleEvent["entries"]) => ({
|
|
295
|
+
entries: [...entries, {
|
|
296
|
+
type: "custom_message" as const,
|
|
297
|
+
customType: "canary-handover-request",
|
|
298
|
+
content: handoverRequest(current.keyword),
|
|
299
|
+
display: true,
|
|
300
|
+
}],
|
|
301
|
+
continue: true,
|
|
302
|
+
});
|
|
303
|
+
|
|
293
304
|
const handleBoundary = async (
|
|
294
305
|
event: AgentBeforeSettleEvent | TurnEndEvent,
|
|
295
306
|
ctx: ExtensionContext,
|
|
296
307
|
) => {
|
|
297
|
-
|
|
308
|
+
const missed = event.type === "turn_end" && violated;
|
|
309
|
+
if (event.type === "turn_end") violated = false;
|
|
310
|
+
if (!recovery && !missed) return;
|
|
298
311
|
if (!keywordForSpan || activeKeyword() !== keywordForSpan || paused) return;
|
|
299
312
|
// Never override Escape, provider errors, or work already queued by the user.
|
|
300
313
|
// In particular, do not compact a stale snapshot.
|
|
301
314
|
if (event.outcome !== "completed" || ctx.signal?.aborted) {
|
|
302
315
|
recovery = undefined;
|
|
303
|
-
violated = false;
|
|
304
316
|
return;
|
|
305
317
|
}
|
|
306
318
|
if (event.context.pendingMessages.length || ctx.hasPendingMessages()) return;
|
|
@@ -316,26 +328,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
316
328
|
: "The agent did not produce a complete, correctly prefixed handover after two attempts";
|
|
317
329
|
return { ...pauseRecovery(ctx, reason, event.entries), continue: false };
|
|
318
330
|
}
|
|
319
|
-
if (event.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (!
|
|
323
|
-
|
|
331
|
+
if (!recovery && event.type === "turn_end") {
|
|
332
|
+
// A message without tool calls ends the agent's turn. A handover there is
|
|
333
|
+
// too late to be useful, so report the miss without blocking or compacting.
|
|
334
|
+
if (event.message.role !== "assistant" || !event.message.content.some((block) => block.type === "toolCall")) {
|
|
335
|
+
notify(ctx, "Canary prefix missing on the final message. No handover requested because the turn has ended.", "warning");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (restarted) {
|
|
339
|
+
ctx.abort();
|
|
340
|
+
return { ...pauseRecovery(ctx, "The restarted agent missed the prefix again; automatic recovery is limited to once per user message", event.entries), continue: false };
|
|
341
|
+
}
|
|
324
342
|
recovery = { keyword: keywordForSpan, attempts: 0 };
|
|
325
343
|
notify(ctx, "Canary prefix missing. Requesting a handover before compaction.", "warning");
|
|
344
|
+
return requestHandover(recovery, event.entries);
|
|
326
345
|
}
|
|
346
|
+
if (event.continue || !recovery) return;
|
|
347
|
+
|
|
327
348
|
const current = recovery;
|
|
328
349
|
if (!current.handover) {
|
|
350
|
+
// Re-request only once the agent has stopped without a usable handover.
|
|
329
351
|
if (event.type === "turn_end") return;
|
|
330
|
-
return
|
|
331
|
-
entries: [...event.entries, {
|
|
332
|
-
type: "custom_message" as const,
|
|
333
|
-
customType: "canary-handover-request",
|
|
334
|
-
content: handoverRequest(current.keyword),
|
|
335
|
-
display: true,
|
|
336
|
-
}],
|
|
337
|
-
continue: true,
|
|
338
|
-
};
|
|
352
|
+
return requestHandover(current, event.entries);
|
|
339
353
|
}
|
|
340
354
|
|
|
341
355
|
// Summarize at turn_end, while ctx.signal still belongs to the live run.
|
|
@@ -363,7 +377,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
363
377
|
}
|
|
364
378
|
const handover = current.handover;
|
|
365
379
|
recovery = undefined;
|
|
366
|
-
violated = false;
|
|
367
380
|
restarted = true;
|
|
368
381
|
notify(ctx, "Canary recovery complete. Restarting with the summary and handover.");
|
|
369
382
|
// A boundary compaction is atomic with continuation. Retain-none gives a
|