@danypops/papyrus 0.2.0 → 0.2.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
CHANGED
|
@@ -129,7 +129,7 @@ Proof types are `file`, `symbol`, `code`, `test`, `command`, `artifact`, and `ur
|
|
|
129
129
|
|
|
130
130
|
Papyrus also injects an Alef-style reconciliation block on every agent turn while work remains: `Current`, `Desired`, `Verify`, and `Next`. The agent is explicitly instructed to ask **“Did we accomplish this task?”** and run gates before marking it done. The injection disappears when every task is complete.
|
|
131
131
|
|
|
132
|
-
In TUI and RPC modes, the extension checks bounded active Tasks at Pi’s public `agent_settled` lifecycle boundary. If active work remains and no continuation is already pending, it queues one hidden next turn so the agent continues instead of handing off merely because a low-level run ended. Driving is single-flight and pauses after 20 automatic turns or 6 unchanged task snapshots
|
|
132
|
+
In TUI and RPC modes, the extension checks bounded active Tasks at Pi’s public `agent_settled` lifecycle boundary. If active work remains and no continuation is already pending, it queues one hidden next turn so the agent continues instead of handing off merely because a low-level run ended. Active Tasks are the trigger; no manual command is required. Driving is single-flight and pauses after 20 automatic turns or 6 unchanged task snapshots; human input and task progress reset the bounded counters automatically.
|
|
133
133
|
|
|
134
134
|
## Why
|
|
135
135
|
|
|
@@ -4,20 +4,19 @@ export interface ActiveTaskMarker {
|
|
|
4
4
|
updated_at: string;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export interface
|
|
7
|
+
export interface ActiveTaskContinuationOptions {
|
|
8
8
|
maxTurns: number;
|
|
9
9
|
maxUnchangedTurns: number;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export interface
|
|
13
|
-
enabled: boolean;
|
|
12
|
+
export interface ActiveTaskContinuationState {
|
|
14
13
|
queued: boolean;
|
|
15
14
|
consecutiveTurns: number;
|
|
16
15
|
unchangedTurns: number;
|
|
17
16
|
pausedReason?: string;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
export interface
|
|
19
|
+
export interface ActiveTaskContinuationDecision {
|
|
21
20
|
action: "continue" | "wait" | "pause";
|
|
22
21
|
reason: string;
|
|
23
22
|
prompt?: string;
|
|
@@ -37,29 +36,27 @@ function continuationPrompt(tasks: ActiveTaskMarker[]): string {
|
|
|
37
36
|
const names = tasks.slice(0, DISPLAYED_TASK_LIMIT).map((task) => `- ${task.id}: ${task.title.slice(0, TITLE_LIMIT)}`);
|
|
38
37
|
return [
|
|
39
38
|
"Continue active Papyrus work now; do not hand off merely because the previous Pi run settled.",
|
|
40
|
-
"Reconcile the active Tasks, choose the next concrete action, use tools, run gates before completion, and continue until done, blocked, or the bounded
|
|
39
|
+
"Reconcile the active Tasks, choose the next concrete action, use tools, run gates before completion, and continue until done, blocked, or the bounded continuation pauses.",
|
|
41
40
|
"Active tasks:",
|
|
42
41
|
...names,
|
|
43
42
|
].join("\n");
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
export class
|
|
47
|
-
private enabled = true;
|
|
45
|
+
export class ActiveTaskContinuation {
|
|
48
46
|
private queued = false;
|
|
49
47
|
private consecutiveTurns = 0;
|
|
50
48
|
private unchangedTurns = 0;
|
|
51
49
|
private lastFingerprint: string | undefined;
|
|
52
50
|
private pausedReason: string | undefined;
|
|
53
51
|
|
|
54
|
-
constructor(private readonly options:
|
|
52
|
+
constructor(private readonly options: ActiveTaskContinuationOptions) {
|
|
55
53
|
if (!Number.isInteger(options.maxTurns) || options.maxTurns < 1) throw new Error("maxTurns must be a positive integer");
|
|
56
54
|
if (!Number.isInteger(options.maxUnchangedTurns) || options.maxUnchangedTurns < 1) {
|
|
57
55
|
throw new Error("maxUnchangedTurns must be a positive integer");
|
|
58
56
|
}
|
|
59
57
|
}
|
|
60
58
|
|
|
61
|
-
evaluate(tasks: ActiveTaskMarker[], context: { idle: boolean; pendingMessages: boolean }):
|
|
62
|
-
if (!this.enabled) return { action: "wait", reason: "disabled" };
|
|
59
|
+
evaluate(tasks: ActiveTaskMarker[], context: { idle: boolean; pendingMessages: boolean }): ActiveTaskContinuationDecision {
|
|
63
60
|
if (!context.idle) return { action: "wait", reason: "Pi is not settled" };
|
|
64
61
|
if (context.pendingMessages) return { action: "wait", reason: "Pi already has pending messages" };
|
|
65
62
|
if (this.queued) return { action: "wait", reason: "continuation already queued" };
|
|
@@ -101,15 +98,8 @@ export class TaskDriver {
|
|
|
101
98
|
this.resetProgress();
|
|
102
99
|
}
|
|
103
100
|
|
|
104
|
-
|
|
105
|
-
this.enabled = enabled;
|
|
106
|
-
if (enabled) this.resetProgress();
|
|
107
|
-
else this.queued = false;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
status(): TaskDriverState {
|
|
101
|
+
status(): ActiveTaskContinuationState {
|
|
111
102
|
return {
|
|
112
|
-
enabled: this.enabled,
|
|
113
103
|
queued: this.queued,
|
|
114
104
|
consecutiveTurns: this.consecutiveTurns,
|
|
115
105
|
unchangedTurns: this.unchangedTurns,
|
|
@@ -117,7 +107,7 @@ export class TaskDriver {
|
|
|
117
107
|
};
|
|
118
108
|
}
|
|
119
109
|
|
|
120
|
-
private pause(reason: string):
|
|
110
|
+
private pause(reason: string): ActiveTaskContinuationDecision {
|
|
121
111
|
this.pausedReason = reason;
|
|
122
112
|
return { action: "pause", reason };
|
|
123
113
|
}
|
package/extension/src/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { formatMetadata } from "./artifact-format.ts";
|
|
|
21
21
|
import { callService } from "./service-client.ts";
|
|
22
22
|
import { registerDomainTools } from "./domain-tools.ts";
|
|
23
23
|
import type { TaskGraph } from "../../src/task-service.ts";
|
|
24
|
-
import {
|
|
24
|
+
import { ActiveTaskContinuation, type ActiveTaskMarker } from "./active-task-continuation.ts";
|
|
25
25
|
import { buildTaskWidgetProjection } from "./task-widget.ts";
|
|
26
26
|
|
|
27
27
|
function text(t: string, details: Record<string, unknown> = {}) {
|
|
@@ -137,7 +137,7 @@ class TaskOverlay {
|
|
|
137
137
|
|
|
138
138
|
export default async function (pi: ExtensionAPI) {
|
|
139
139
|
registerDomainTools(pi);
|
|
140
|
-
const
|
|
140
|
+
const taskContinuation = new ActiveTaskContinuation({
|
|
141
141
|
maxTurns: TASK_DRIVER_MAX_TURNS,
|
|
142
142
|
maxUnchangedTurns: TASK_DRIVER_MAX_UNCHANGED_TURNS,
|
|
143
143
|
});
|
|
@@ -149,7 +149,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
149
149
|
status: "active",
|
|
150
150
|
limit: TASK_DRIVER_ACTIVE_LIMIT,
|
|
151
151
|
});
|
|
152
|
-
const decision =
|
|
152
|
+
const decision = taskContinuation.evaluate(active, {
|
|
153
153
|
idle: ctx.isIdle(),
|
|
154
154
|
pendingMessages: ctx.hasPendingMessages(),
|
|
155
155
|
});
|
|
@@ -160,40 +160,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
160
160
|
display: false,
|
|
161
161
|
}, { triggerTurn: true, deliverAs: "nextTurn" });
|
|
162
162
|
} else if (decision.action === "pause" && ctx.hasUI) {
|
|
163
|
-
ctx.ui.notify(`Papyrus task driving paused: ${decision.reason}.
|
|
163
|
+
ctx.ui.notify(`Papyrus task driving paused: ${decision.reason}. Human input or task progress resumes it automatically.`, "warning");
|
|
164
164
|
}
|
|
165
165
|
} catch {
|
|
166
166
|
// The daemon may be unavailable during startup, reload, or shutdown.
|
|
167
167
|
}
|
|
168
168
|
};
|
|
169
169
|
|
|
170
|
-
pi.registerCommand("task-drive", {
|
|
171
|
-
description: "Control bounded automatic continuation while active Papyrus Tasks remain",
|
|
172
|
-
handler: async (args, ctx) => {
|
|
173
|
-
const action = args.trim().toLowerCase() || "status";
|
|
174
|
-
if (action === "on") {
|
|
175
|
-
taskDriver.setEnabled(true);
|
|
176
|
-
ctx.ui.notify("Papyrus task driving enabled", "info");
|
|
177
|
-
await driveActiveTasks(ctx);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
if (action === "off") {
|
|
181
|
-
taskDriver.setEnabled(false);
|
|
182
|
-
ctx.ui.notify("Papyrus task driving disabled", "info");
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
if (action !== "status") {
|
|
186
|
-
ctx.ui.notify("Usage: /task-drive <on|off|status>", "warning");
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
const status = taskDriver.status();
|
|
190
|
-
ctx.ui.notify(
|
|
191
|
-
`Papyrus task driving: ${status.enabled ? "on" : "off"} · ${status.consecutiveTurns}/${TASK_DRIVER_MAX_TURNS} automatic turns${status.pausedReason ? ` · paused: ${status.pausedReason}` : ""}`,
|
|
192
|
-
"info",
|
|
193
|
-
);
|
|
194
|
-
},
|
|
195
|
-
});
|
|
196
|
-
|
|
197
170
|
// ── Low-level graph-store tools ────────────────────────────────────
|
|
198
171
|
|
|
199
172
|
pi.registerTool({
|
|
@@ -393,9 +366,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
393
366
|
// retry, compaction retry, and queued follow-up processing have finished.
|
|
394
367
|
|
|
395
368
|
pi.on("input", (event) => {
|
|
396
|
-
if (event.source !== "extension")
|
|
369
|
+
if (event.source !== "extension") taskContinuation.onHumanInput();
|
|
397
370
|
});
|
|
398
|
-
pi.on("agent_start", () => {
|
|
371
|
+
pi.on("agent_start", () => { taskContinuation.onAgentStart(); });
|
|
399
372
|
pi.on("agent_settled", async (_event, ctx) => { await driveActiveTasks(ctx); });
|
|
400
373
|
|
|
401
374
|
// ── "Are we there yet?" — inject active tasks into every turn ──────
|
package/package.json
CHANGED