pi-long-task 0.5.0 → 0.7.0
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/CHANGELOG.md +40 -0
- package/README.md +153 -3
- package/package.json +1 -1
- package/src/coordinator.ts +861 -62
- package/src/goal_discovery.ts +2 -0
- package/src/goal_loop.ts +76 -0
- package/src/goal_orchestrator.ts +98 -1
- package/src/goal_review.ts +206 -15
- package/src/goal_todo_execution.ts +7 -0
- package/src/goal_todo_generation.ts +108 -13
- package/src/index.ts +15 -1
- package/src/network_failure.ts +574 -0
- package/src/network_recovery.ts +395 -0
- package/src/network_recovery_config.ts +89 -0
- package/src/planner_config.ts +214 -0
- package/src/planner_progress.ts +156 -0
- package/src/render.ts +38 -0
- package/src/session_guard.ts +120 -7
- package/src/todo_generator.ts +84 -7
- package/src/types.ts +68 -0
- package/src/worker_capabilities.ts +103 -0
- package/src/worker_config.ts +148 -7
- package/src/worker_session.ts +33 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { PlannerBudget, PlannerComplexitySignal } from "./planner_config.ts";
|
|
2
|
+
|
|
3
|
+
export type PlannerProgressState = "started" | "active" | "grace";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One user-facing planner timing update. Millisecond fields remain available to
|
|
7
|
+
* integrations while `message` consistently uses friendly duration wording.
|
|
8
|
+
*/
|
|
9
|
+
export interface PlannerProgressEvent {
|
|
10
|
+
state: PlannerProgressState;
|
|
11
|
+
message: string;
|
|
12
|
+
budgetMs: number;
|
|
13
|
+
gracePeriodMs: number;
|
|
14
|
+
elapsedMs: number;
|
|
15
|
+
remainingMs: number;
|
|
16
|
+
graceRemainingMs?: number;
|
|
17
|
+
budget: Readonly<PlannerBudget>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type PlannerProgressHandler = (event: Readonly<PlannerProgressEvent>) => void;
|
|
21
|
+
|
|
22
|
+
/** Formats a duration for people rather than exposing raw millisecond counts. */
|
|
23
|
+
export function formatFriendlyDuration(durationMs: number): string {
|
|
24
|
+
const milliseconds = Math.max(0, Number.isFinite(durationMs) ? durationMs : 0);
|
|
25
|
+
if (milliseconds > 0 && milliseconds < 1_000) {
|
|
26
|
+
return "less than 1 second";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const totalSeconds = Math.round(milliseconds / 1_000);
|
|
30
|
+
if (totalSeconds < 60) {
|
|
31
|
+
return plural(totalSeconds, "second");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
35
|
+
const seconds = totalSeconds % 60;
|
|
36
|
+
if (totalMinutes < 60) {
|
|
37
|
+
return joinDurationParts(plural(totalMinutes, "minute"), seconds > 0 ? plural(seconds, "second") : undefined);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const totalHours = Math.floor(totalMinutes / 60);
|
|
41
|
+
const minutes = totalMinutes % 60;
|
|
42
|
+
if (totalHours < 24) {
|
|
43
|
+
return joinDurationParts(plural(totalHours, "hour"), minutes > 0 ? plural(minutes, "minute") : undefined);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const days = Math.floor(totalHours / 24);
|
|
47
|
+
const hours = totalHours % 24;
|
|
48
|
+
return joinDurationParts(plural(days, "day"), hours > 0 ? plural(hours, "hour") : undefined);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createPlannerStartedProgress(
|
|
52
|
+
budget: Readonly<PlannerBudget>,
|
|
53
|
+
gracePeriodMs: number,
|
|
54
|
+
): PlannerProgressEvent {
|
|
55
|
+
const budgetText = formatFriendlyDuration(budget.timeoutMs);
|
|
56
|
+
const sourceText = budget.source === "explicit" ? " (explicitly configured)" : "";
|
|
57
|
+
const adaptiveText = adaptiveExtensionExplanation(budget);
|
|
58
|
+
const graceText =
|
|
59
|
+
gracePeriodMs > 0
|
|
60
|
+
? ` A ${formatFriendlyDurationModifier(gracePeriodMs)} graceful-shutdown period is available afterward.`
|
|
61
|
+
: " No graceful-shutdown period is configured.";
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
state: "started",
|
|
65
|
+
message: `Creating TODO plan. Effective planning budget: ${budgetText}${sourceText}.${adaptiveText}${graceText}`,
|
|
66
|
+
budgetMs: budget.timeoutMs,
|
|
67
|
+
gracePeriodMs,
|
|
68
|
+
elapsedMs: 0,
|
|
69
|
+
remainingMs: budget.timeoutMs,
|
|
70
|
+
budget,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createPlannerActiveProgress(
|
|
75
|
+
budget: Readonly<PlannerBudget>,
|
|
76
|
+
gracePeriodMs: number,
|
|
77
|
+
elapsedMs: number,
|
|
78
|
+
): PlannerProgressEvent {
|
|
79
|
+
const elapsed = clamp(elapsedMs, 0, budget.timeoutMs);
|
|
80
|
+
const remaining = Math.max(0, budget.timeoutMs - elapsed);
|
|
81
|
+
return {
|
|
82
|
+
state: "active",
|
|
83
|
+
message: `Still planning: ${formatFriendlyDuration(elapsed)} elapsed; about ${formatFriendlyDuration(remaining)} remaining in the ${formatFriendlyDuration(budget.timeoutMs)} budget.`,
|
|
84
|
+
budgetMs: budget.timeoutMs,
|
|
85
|
+
gracePeriodMs,
|
|
86
|
+
elapsedMs: elapsed,
|
|
87
|
+
remainingMs: remaining,
|
|
88
|
+
budget,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function createPlannerGraceProgress(
|
|
93
|
+
budget: Readonly<PlannerBudget>,
|
|
94
|
+
gracePeriodMs: number,
|
|
95
|
+
): PlannerProgressEvent {
|
|
96
|
+
const grace = Math.max(0, gracePeriodMs);
|
|
97
|
+
return {
|
|
98
|
+
state: "grace",
|
|
99
|
+
message: `Planning budget reached after ${formatFriendlyDuration(budget.timeoutMs)}; entering a ${formatFriendlyDurationModifier(grace)} graceful-shutdown period to finish a valid plan.`,
|
|
100
|
+
budgetMs: budget.timeoutMs,
|
|
101
|
+
gracePeriodMs: grace,
|
|
102
|
+
elapsedMs: budget.timeoutMs,
|
|
103
|
+
remainingMs: 0,
|
|
104
|
+
graceRemainingMs: grace,
|
|
105
|
+
budget,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Three bounded checkpoints provide useful timing without per-second noise. */
|
|
110
|
+
export function plannerProgressCheckpoints(budgetMs: number): number[] {
|
|
111
|
+
const budget = Math.max(0, Math.floor(budgetMs));
|
|
112
|
+
if (budget <= 1) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
return [...new Set([0.25, 0.5, 0.75].map((fraction) => Math.max(1, Math.floor(budget * fraction))))].filter(
|
|
116
|
+
(checkpoint) => checkpoint < budget,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function formatFriendlyDurationModifier(durationMs: number): string {
|
|
121
|
+
return formatFriendlyDuration(durationMs).replace(
|
|
122
|
+
/(\d[\d,]*) (seconds?|minutes?|hours?|days?)/g,
|
|
123
|
+
(_match, value: string, unit: string) => `${value}-${unit.replace(/s$/, "")}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function adaptiveExtensionExplanation(budget: Readonly<PlannerBudget>): string {
|
|
128
|
+
if (!budget.extensionApplied || budget.extensionMs <= 0 || !budget.trigger) {
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
return ` Adaptive extension: ${formatFriendlyDuration(budget.extensionMs)} because the request includes ${complexitySignalText(budget.trigger)}.`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function complexitySignalText(signal: Readonly<PlannerComplexitySignal>): string {
|
|
135
|
+
switch (signal.kind) {
|
|
136
|
+
case "separately_planned_tasks":
|
|
137
|
+
return `${signal.itemCount} separately planned ${signal.itemCount === 1 ? "task" : "tasks"}`;
|
|
138
|
+
case "explicit_item_count":
|
|
139
|
+
return `an explicit count of ${signal.itemCount} ${signal.itemCount === 1 ? "deliverable" : "deliverables"}`;
|
|
140
|
+
case "enumerated_deliverables":
|
|
141
|
+
return `${signal.itemCount} enumerated ${signal.itemCount === 1 ? "deliverable" : "deliverables"}`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function plural(value: number, unit: string): string {
|
|
146
|
+
return `${value.toLocaleString("en-US")} ${unit}${value === 1 ? "" : "s"}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function joinDurationParts(first: string, second: string | undefined): string {
|
|
150
|
+
return second ? `${first} ${second}` : first;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function clamp(value: number, minimum: number, maximum: number): number {
|
|
154
|
+
const finite = Number.isFinite(value) ? value : minimum;
|
|
155
|
+
return Math.min(maximum, Math.max(minimum, Math.round(finite)));
|
|
156
|
+
}
|
package/src/render.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { Text, type Component } from "@earendil-works/pi-tui";
|
|
|
4
4
|
import type { GoalLoopState, GoalLoopStatus } from "./goal_loop.ts";
|
|
5
5
|
import type { TaskProgressModel } from "./task_progress.ts";
|
|
6
6
|
import type { CoordinatorCommitSummary, CoordinatorRemainingTask, CoordinatorStatus } from "./types.ts";
|
|
7
|
+
import type { WorkerCapabilityWarning } from "./worker_capabilities.ts";
|
|
7
8
|
|
|
8
9
|
export interface CoordinatorResultForRendering {
|
|
9
10
|
status: CoordinatorStatus;
|
|
@@ -19,6 +20,7 @@ export interface CoordinatorResultForRendering {
|
|
|
19
20
|
remainingTasks?: CoordinatorRemainingTask[];
|
|
20
21
|
taskProgress?: TaskProgressModel;
|
|
21
22
|
workerCostTotal?: number;
|
|
23
|
+
capabilityWarnings?: readonly WorkerCapabilityWarning[];
|
|
22
24
|
goal?: string;
|
|
23
25
|
error?: string;
|
|
24
26
|
}
|
|
@@ -56,6 +58,10 @@ export function formatCoordinatorResultMessage(result: CoordinatorResultForRende
|
|
|
56
58
|
lines.push(`Worker spend: ${formatCost(result.workerCostTotal)}`);
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
if (result.capabilityWarnings?.length) {
|
|
62
|
+
lines.push("Worker capability warnings:", ...result.capabilityWarnings.map((warning) => `- ${warning.message}`));
|
|
63
|
+
}
|
|
64
|
+
|
|
59
65
|
const commitLines = commits
|
|
60
66
|
.filter((commit) => commit.hash || commit.error)
|
|
61
67
|
.map((commit) => {
|
|
@@ -385,6 +391,9 @@ function renderLongTaskSummary(details: CoordinatorToolRenderDetails, expanded:
|
|
|
385
391
|
}
|
|
386
392
|
|
|
387
393
|
const lines = [summary.join(" — "), theme.fg("muted", details.summary)];
|
|
394
|
+
for (const warning of details.capabilityWarnings ?? []) {
|
|
395
|
+
lines.push(theme.fg("warning", `Warning: ${warning.message}`));
|
|
396
|
+
}
|
|
388
397
|
lines.push(theme.fg("dim", `Result: ${details.resultPath ?? details.taskResultPath ?? "unknown"}`));
|
|
389
398
|
lines.push(theme.fg("dim", `TODO: ${details.todoPath}`));
|
|
390
399
|
|
|
@@ -450,12 +459,16 @@ function progressSubtaskDetails(value: unknown): ProgressSubtaskRenderDetails[]
|
|
|
450
459
|
|
|
451
460
|
function progressPhaseLabel(phase: string): string {
|
|
452
461
|
switch (phase) {
|
|
462
|
+
case "capability_warning":
|
|
463
|
+
return "Warning";
|
|
453
464
|
case "planning":
|
|
454
465
|
case "planned":
|
|
455
466
|
return "Thought";
|
|
456
467
|
case "task_start":
|
|
457
468
|
case "worker_tool":
|
|
458
469
|
return "Build";
|
|
470
|
+
case "network_wait":
|
|
471
|
+
return "Network";
|
|
459
472
|
case "task_done":
|
|
460
473
|
return "Done";
|
|
461
474
|
case "task_failed":
|
|
@@ -603,10 +616,35 @@ function longTaskDetails(details: Record<string, unknown> | undefined): Coordina
|
|
|
603
616
|
remainingTasks: remainingTaskSummaries(details.remainingTasks),
|
|
604
617
|
taskProgress: taskProgressModel(details.taskProgress),
|
|
605
618
|
workerCostTotal: nonNegativeNumberValue(details.workerCostTotal),
|
|
619
|
+
capabilityWarnings: capabilityWarningDetails(details.capabilityWarnings),
|
|
606
620
|
error: stringValue(details.error),
|
|
607
621
|
};
|
|
608
622
|
}
|
|
609
623
|
|
|
624
|
+
function capabilityWarningDetails(value: unknown): WorkerCapabilityWarning[] {
|
|
625
|
+
if (!Array.isArray(value)) {
|
|
626
|
+
return [];
|
|
627
|
+
}
|
|
628
|
+
return value.flatMap((item) => {
|
|
629
|
+
const record = recordOrUndefined(item);
|
|
630
|
+
const code = stringValue(record?.code);
|
|
631
|
+
const message = stringValue(record?.message);
|
|
632
|
+
const planningConstraint = stringValue(record?.planningConstraint);
|
|
633
|
+
if (code !== "unavailable_browser_capability" || !message || !planningConstraint) {
|
|
634
|
+
return [];
|
|
635
|
+
}
|
|
636
|
+
return [
|
|
637
|
+
{
|
|
638
|
+
code,
|
|
639
|
+
message,
|
|
640
|
+
planningConstraint,
|
|
641
|
+
requestedCapabilities: stringArray(record?.requestedCapabilities),
|
|
642
|
+
availableTools: stringArray(record?.availableTools),
|
|
643
|
+
},
|
|
644
|
+
];
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
610
648
|
function commitSummaries(value: unknown): CoordinatorCommitSummary[] {
|
|
611
649
|
if (!Array.isArray(value)) {
|
|
612
650
|
return [];
|
package/src/session_guard.ts
CHANGED
|
@@ -15,14 +15,31 @@ export interface GuardedSessionPromptOptions {
|
|
|
15
15
|
gracefulShutdownPrompt?: string;
|
|
16
16
|
diagnostics?: string[];
|
|
17
17
|
onEvent?: (event: unknown) => void;
|
|
18
|
+
/** Bounded elapsed-time checkpoints while the primary deadline is active. */
|
|
19
|
+
progressCheckpointsMs?: readonly number[];
|
|
20
|
+
onProgressCheckpoint?: (elapsedMs: number) => void;
|
|
21
|
+
/** Called exactly when a positive grace period begins. */
|
|
22
|
+
onGracePeriodStart?: (gracePeriodMs: number) => void;
|
|
18
23
|
dispose?: boolean;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export interface GuardedSessionPromptResult {
|
|
22
27
|
assistantText: string;
|
|
28
|
+
/** True when the primary prompt deadline elapsed, even if the prompt safely completed during grace. */
|
|
23
29
|
timedOut: boolean;
|
|
30
|
+
/** True only when a timed-out prompt settled during the configured grace period. */
|
|
31
|
+
completedDuringGrace: boolean;
|
|
32
|
+
/** True when non-whitespace assistant output was observed before prompt termination. */
|
|
33
|
+
outputObserved: boolean;
|
|
34
|
+
/** True when the session had to be stopped because its grace period expired. */
|
|
35
|
+
graceExpired: boolean;
|
|
36
|
+
/** True when the session was stopped for any reason, including hard timeout. */
|
|
24
37
|
aborted: boolean;
|
|
38
|
+
/** True only when the caller's AbortSignal cancelled the prompt. */
|
|
39
|
+
cancelled: boolean;
|
|
25
40
|
error?: string;
|
|
41
|
+
/** Untouched prompt failure for coordinator-level provider/transport classification. */
|
|
42
|
+
failure?: unknown;
|
|
26
43
|
diagnostics: string[];
|
|
27
44
|
events: unknown[];
|
|
28
45
|
sessionFile?: string;
|
|
@@ -37,13 +54,19 @@ export async function runGuardedSessionPrompt(
|
|
|
37
54
|
const events: unknown[] = [];
|
|
38
55
|
const timers = new Set<ReturnType<typeof setTimeout>>();
|
|
39
56
|
let assistantText = "";
|
|
57
|
+
let currentAssistantText = "";
|
|
58
|
+
let outputObserved = false;
|
|
40
59
|
let timedOut = false;
|
|
60
|
+
let graceExpired = false;
|
|
41
61
|
let aborted = false;
|
|
62
|
+
let cancelled = false;
|
|
42
63
|
let error: string | undefined;
|
|
64
|
+
let failure: unknown;
|
|
43
65
|
let promptSettled = false;
|
|
44
66
|
let finished = false;
|
|
45
67
|
let unsubscribe: (() => void) | undefined;
|
|
46
68
|
let complete: (() => void) | undefined;
|
|
69
|
+
const assistantTextAtStart = latestAssistantText(session, [], "");
|
|
47
70
|
|
|
48
71
|
const completed = new Promise<void>((resolve) => {
|
|
49
72
|
complete = resolve;
|
|
@@ -87,6 +110,14 @@ export async function runGuardedSessionPrompt(
|
|
|
87
110
|
}
|
|
88
111
|
};
|
|
89
112
|
|
|
113
|
+
const notifyTiming = (callback: (() => void) | undefined, label: string) => {
|
|
114
|
+
try {
|
|
115
|
+
callback?.();
|
|
116
|
+
} catch (exc) {
|
|
117
|
+
diagnostics.push(`${label} listener failed: ${errorMessage(exc)}`);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
90
121
|
const requestGracefulShutdown = () => {
|
|
91
122
|
const message = options.gracefulShutdownPrompt?.trim();
|
|
92
123
|
if (!message || finished || promptSettled || aborted) {
|
|
@@ -127,13 +158,18 @@ export async function runGuardedSessionPrompt(
|
|
|
127
158
|
}
|
|
128
159
|
timedOut = true;
|
|
129
160
|
diagnostics.push(`session prompt timed out after ${formatMilliseconds(timeoutMs(options.timeoutMs))}`);
|
|
130
|
-
requestGracefulShutdown();
|
|
131
161
|
|
|
132
162
|
const graceMs = nonNegativeMilliseconds(options.gracefulShutdownMs);
|
|
163
|
+
if (graceMs > 0) {
|
|
164
|
+
notifyTiming(() => options.onGracePeriodStart?.(graceMs), "grace-period progress");
|
|
165
|
+
}
|
|
166
|
+
requestGracefulShutdown();
|
|
167
|
+
|
|
133
168
|
const hardAbort = () => {
|
|
134
169
|
if (finished || promptSettled) {
|
|
135
170
|
return;
|
|
136
171
|
}
|
|
172
|
+
graceExpired = true;
|
|
137
173
|
abortSession(`session prompt exceeded ${formatMilliseconds(timeoutMs(options.timeoutMs))} timeout`);
|
|
138
174
|
resolveCompleted();
|
|
139
175
|
};
|
|
@@ -149,20 +185,34 @@ export async function runGuardedSessionPrompt(
|
|
|
149
185
|
if (finished || promptSettled) {
|
|
150
186
|
return;
|
|
151
187
|
}
|
|
152
|
-
|
|
188
|
+
cancelled = true;
|
|
189
|
+
abortSession(abortReason(options.abortSignal, "session prompt cancelled by outer signal"));
|
|
153
190
|
resolveCompleted();
|
|
154
191
|
};
|
|
155
192
|
|
|
156
193
|
try {
|
|
157
194
|
if (options.abortSignal?.aborted) {
|
|
158
195
|
aborted = true;
|
|
159
|
-
|
|
196
|
+
cancelled = true;
|
|
197
|
+
error = abortReason(options.abortSignal, "session prompt cancelled before start");
|
|
160
198
|
} else {
|
|
161
199
|
unsubscribe = session.subscribe((event: unknown) => {
|
|
162
200
|
events.push(event);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
201
|
+
if (isAssistantMessageStart(event)) {
|
|
202
|
+
currentAssistantText = "";
|
|
203
|
+
}
|
|
204
|
+
const delta = assistantTextDeltaFromEvent(event);
|
|
205
|
+
if (delta !== undefined) {
|
|
206
|
+
currentAssistantText += delta;
|
|
207
|
+
assistantText = currentAssistantText || assistantText;
|
|
208
|
+
outputObserved ||= delta.trim().length > 0;
|
|
209
|
+
} else {
|
|
210
|
+
const text = assistantTextFromEvent(event);
|
|
211
|
+
if (text) {
|
|
212
|
+
currentAssistantText = text;
|
|
213
|
+
assistantText = text;
|
|
214
|
+
outputObserved ||= text.trim().length > 0;
|
|
215
|
+
}
|
|
166
216
|
}
|
|
167
217
|
try {
|
|
168
218
|
options.onEvent?.(event);
|
|
@@ -180,6 +230,7 @@ export async function runGuardedSessionPrompt(
|
|
|
180
230
|
},
|
|
181
231
|
(exc: unknown) => {
|
|
182
232
|
promptSettled = true;
|
|
233
|
+
failure ??= exc;
|
|
183
234
|
error = error ?? errorMessage(exc);
|
|
184
235
|
resolveCompleted();
|
|
185
236
|
},
|
|
@@ -188,12 +239,20 @@ export async function runGuardedSessionPrompt(
|
|
|
188
239
|
|
|
189
240
|
const limitMs = timeoutMs(options.timeoutMs);
|
|
190
241
|
if (limitMs > 0) {
|
|
242
|
+
for (const checkpoint of normalizedProgressCheckpoints(options.progressCheckpointsMs, limitMs)) {
|
|
243
|
+
schedule(() => {
|
|
244
|
+
if (!finished && !promptSettled && !timedOut && !aborted) {
|
|
245
|
+
notifyTiming(() => options.onProgressCheckpoint?.(checkpoint), "timing progress");
|
|
246
|
+
}
|
|
247
|
+
}, checkpoint);
|
|
248
|
+
}
|
|
191
249
|
schedule(triggerTimeout, limitMs);
|
|
192
250
|
}
|
|
193
251
|
|
|
194
252
|
await completed;
|
|
195
253
|
}
|
|
196
254
|
} catch (exc) {
|
|
255
|
+
failure ??= exc;
|
|
197
256
|
error = error ?? errorMessage(exc);
|
|
198
257
|
} finally {
|
|
199
258
|
finished = true;
|
|
@@ -201,6 +260,7 @@ export async function runGuardedSessionPrompt(
|
|
|
201
260
|
options.abortSignal?.removeEventListener("abort", abortListener);
|
|
202
261
|
unsubscribe?.();
|
|
203
262
|
assistantText = latestAssistantText(session, events, assistantText);
|
|
263
|
+
outputObserved ||= assistantText.trim().length > 0 && assistantText !== assistantTextAtStart;
|
|
204
264
|
if (options.dispose !== false) {
|
|
205
265
|
try {
|
|
206
266
|
const disposeResult = (session.dispose as (() => unknown) | undefined)?.();
|
|
@@ -215,7 +275,20 @@ export async function runGuardedSessionPrompt(
|
|
|
215
275
|
}
|
|
216
276
|
}
|
|
217
277
|
|
|
218
|
-
return buildResult(
|
|
278
|
+
return buildResult(
|
|
279
|
+
session,
|
|
280
|
+
events,
|
|
281
|
+
assistantText,
|
|
282
|
+
timedOut,
|
|
283
|
+
timedOut && promptSettled && !graceExpired && !aborted && failure === undefined,
|
|
284
|
+
outputObserved,
|
|
285
|
+
graceExpired,
|
|
286
|
+
aborted,
|
|
287
|
+
cancelled,
|
|
288
|
+
error,
|
|
289
|
+
failure,
|
|
290
|
+
diagnostics,
|
|
291
|
+
);
|
|
219
292
|
}
|
|
220
293
|
|
|
221
294
|
function buildResult(
|
|
@@ -223,15 +296,25 @@ function buildResult(
|
|
|
223
296
|
events: unknown[],
|
|
224
297
|
assistantText: string,
|
|
225
298
|
timedOut: boolean,
|
|
299
|
+
completedDuringGrace: boolean,
|
|
300
|
+
outputObserved: boolean,
|
|
301
|
+
graceExpired: boolean,
|
|
226
302
|
aborted: boolean,
|
|
303
|
+
cancelled: boolean,
|
|
227
304
|
error: string | undefined,
|
|
305
|
+
failure: unknown,
|
|
228
306
|
diagnostics: string[],
|
|
229
307
|
): GuardedSessionPromptResult {
|
|
230
308
|
return {
|
|
231
309
|
assistantText: latestAssistantText(session, events, assistantText),
|
|
232
310
|
timedOut,
|
|
311
|
+
completedDuringGrace,
|
|
312
|
+
outputObserved,
|
|
313
|
+
graceExpired,
|
|
233
314
|
aborted,
|
|
315
|
+
cancelled,
|
|
234
316
|
error,
|
|
317
|
+
...(failure === undefined ? {} : { failure }),
|
|
235
318
|
diagnostics: [...diagnostics],
|
|
236
319
|
events: [...events],
|
|
237
320
|
sessionFile: session.sessionFile,
|
|
@@ -259,6 +342,16 @@ function timeoutMs(value: number | undefined): number {
|
|
|
259
342
|
return Math.max(0, value);
|
|
260
343
|
}
|
|
261
344
|
|
|
345
|
+
function normalizedProgressCheckpoints(values: readonly number[] | undefined, limitMs: number): number[] {
|
|
346
|
+
if (!values) {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
return [...new Set(values)]
|
|
350
|
+
.filter((value) => Number.isFinite(value) && value > 0 && value < limitMs)
|
|
351
|
+
.map((value) => Math.floor(value))
|
|
352
|
+
.sort((left, right) => left - right);
|
|
353
|
+
}
|
|
354
|
+
|
|
262
355
|
function nonNegativeMilliseconds(value: number | undefined): number {
|
|
263
356
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
264
357
|
return 0;
|
|
@@ -270,6 +363,26 @@ function formatMilliseconds(ms: number): string {
|
|
|
270
363
|
return `${(ms / 1000).toFixed(3)}s`;
|
|
271
364
|
}
|
|
272
365
|
|
|
366
|
+
function isAssistantMessageStart(event: unknown): boolean {
|
|
367
|
+
return (
|
|
368
|
+
isRecord(event) && event.type === "message_start" && isRecord(event.message) && event.message.role === "assistant"
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function assistantTextDeltaFromEvent(event: unknown): string | undefined {
|
|
373
|
+
if (!isRecord(event) || event.type !== "message_update" || !isRecord(event.assistantMessageEvent)) {
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
const assistantEvent = event.assistantMessageEvent;
|
|
377
|
+
return assistantEvent.type === "text_delta" && typeof assistantEvent.delta === "string"
|
|
378
|
+
? assistantEvent.delta
|
|
379
|
+
: undefined;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
383
|
+
return typeof value === "object" && value !== null;
|
|
384
|
+
}
|
|
385
|
+
|
|
273
386
|
function abortReason(signal: AbortSignal | undefined, fallback: string): string {
|
|
274
387
|
const reason = signal?.reason;
|
|
275
388
|
if (reason === undefined) {
|
package/src/todo_generator.ts
CHANGED
|
@@ -14,8 +14,8 @@ const NUMBERED_ITEM_RE = /^\s*\d+[.)]\s+(.+?)\s*$/;
|
|
|
14
14
|
const FENCE_RE = /```(?:markdown|md)?\s*\n([\s\S]*?)\n```/gi;
|
|
15
15
|
|
|
16
16
|
export class TodoGenerationError extends Error {
|
|
17
|
-
constructor(message: string) {
|
|
18
|
-
super(message);
|
|
17
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
18
|
+
super(message, options);
|
|
19
19
|
this.name = "TodoGenerationError";
|
|
20
20
|
}
|
|
21
21
|
}
|
|
@@ -133,7 +133,7 @@ export function applyGoalInstructionsToTodoMarkdown(markdown: string, goal?: str
|
|
|
133
133
|
return markdown;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
let next =
|
|
136
|
+
let next = insertGlobalInstructions(markdown, goalInstructionLines(trimmedGoal));
|
|
137
137
|
const coverageGoal = parseCoverageGoal(trimmedGoal);
|
|
138
138
|
if (coverageGoal) {
|
|
139
139
|
next = appendCoverageVerificationToTasks(next, coverageGoalVerifyBullet(coverageGoal));
|
|
@@ -142,14 +142,33 @@ export function applyGoalInstructionsToTodoMarkdown(markdown: string, goal?: str
|
|
|
142
142
|
return next;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
function
|
|
145
|
+
export function applyWorkerCapabilityConstraintsToTodoMarkdown(
|
|
146
|
+
markdown: string,
|
|
147
|
+
constraints: readonly string[],
|
|
148
|
+
): string {
|
|
149
|
+
const additions = constraints
|
|
150
|
+
.map(oneLine)
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.map((item) => `- ${item}`);
|
|
153
|
+
if (additions.length === 0) {
|
|
154
|
+
return markdown;
|
|
155
|
+
}
|
|
156
|
+
const next = insertGlobalInstructions(markdown, additions);
|
|
157
|
+
validateTodoMarkdown(next);
|
|
158
|
+
return next;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function goalInstructionLines(goal: string): string[] {
|
|
146
162
|
const coverageGoal = parseCoverageGoal(goal);
|
|
147
163
|
const additions = [`- Long task goal: ${goal}`];
|
|
148
164
|
if (coverageGoal) {
|
|
149
165
|
additions.push(`- Coverage goal: ${coverageGoalAction(coverageGoal)}`);
|
|
150
166
|
additions.push(`- Coverage verification: ${coverageGoalVerification(coverageGoal)}`);
|
|
151
167
|
}
|
|
168
|
+
return additions;
|
|
169
|
+
}
|
|
152
170
|
|
|
171
|
+
function insertGlobalInstructions(markdown: string, additions: readonly string[]): string {
|
|
153
172
|
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
154
173
|
const progressIndex = lines.findIndex((line) => /^##\s+Progress\s*$/i.test(line.trim()));
|
|
155
174
|
if (progressIndex < 0) {
|
|
@@ -229,9 +248,43 @@ function oneLine(value: string): string {
|
|
|
229
248
|
return value.replace(/\s+/g, " ").trim();
|
|
230
249
|
}
|
|
231
250
|
|
|
232
|
-
export function
|
|
251
|
+
export function todoPlanningOnlyPromptBlock(capabilityConstraints: readonly string[] = []): string {
|
|
252
|
+
const capabilityBlock = capabilityConstraints.length
|
|
253
|
+
? `\n\nWorker capability constraints (preserve these above ## Progress and in affected tasks):\n${capabilityConstraints
|
|
254
|
+
.map((constraint) => `- ${oneLine(constraint)}`)
|
|
255
|
+
.join("\n")}`
|
|
256
|
+
: "";
|
|
257
|
+
return `Planning-only boundary:
|
|
258
|
+
- Produce only a concise executable plan for future workers.
|
|
259
|
+
- Do not perform requested end work: do not implement or write code, execute research or report findings, create requested creative output (prose, stories, copy, designs, or assets), or produce any other final deliverable.
|
|
260
|
+
- Use future-worker action language; do not claim work is complete or invent results.
|
|
261
|
+
- Keep repeated task sections compact: use a one-sentence Goal and Done when, plus only the necessary Status and Verify bullets. Omit rationale, lengthy analysis, summaries, duplicated context, unrequested examples, and boilerplate.
|
|
262
|
+
- Preserve every instruction, constraint, required deliverable, and acceptance condition from the source request and supplied planning context. Put shared constraints above ## Progress and task-specific requirements in the relevant task.${capabilityBlock}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function buildTodoCreationPrompt(
|
|
266
|
+
rawInput: string,
|
|
267
|
+
goal?: string,
|
|
268
|
+
capabilityConstraints: readonly string[] = [],
|
|
269
|
+
): string {
|
|
233
270
|
const goalBlock = todoGoalPromptBlock(goal);
|
|
234
|
-
return `Convert the following raw project request into Pi Long Task-compatible TODO markdown
|
|
271
|
+
return `Convert the following raw project request into Pi Long Task-compatible TODO markdown.
|
|
272
|
+
|
|
273
|
+
${todoPlanningOnlyPromptBlock(capabilityConstraints)}
|
|
274
|
+
|
|
275
|
+
Required format:
|
|
276
|
+
- Output only markdown, with no commentary and no code fence.
|
|
277
|
+
- Start with exactly: # Pi Long Task TODO
|
|
278
|
+
- Include a ## Progress section with one unchecked line per task: - [ ] TODO N — Title
|
|
279
|
+
- Include a --- separator before task sections.
|
|
280
|
+
- Create sequential sections named ## TODO N — Title.
|
|
281
|
+
- Each task section must include **Goal:**, **Status:** with unchecked checkbox items, **Verify:** with concrete verification guidance, and **Done when:**.
|
|
282
|
+
- Keep tasks focused and independently assignable to worker sessions.
|
|
283
|
+
${goalBlock}
|
|
284
|
+
Raw input:
|
|
285
|
+
|
|
286
|
+
${rawInput.trim()}
|
|
287
|
+
`;
|
|
235
288
|
}
|
|
236
289
|
|
|
237
290
|
export function buildTodoRepairPrompt(
|
|
@@ -239,9 +292,33 @@ export function buildTodoRepairPrompt(
|
|
|
239
292
|
invalidOutput: string,
|
|
240
293
|
validationError: string,
|
|
241
294
|
goal?: string,
|
|
295
|
+
capabilityConstraints: readonly string[] = [],
|
|
242
296
|
): string {
|
|
243
297
|
const goalBlock = todoGoalPromptBlock(goal);
|
|
244
|
-
return `
|
|
298
|
+
return `Repair the previous response into valid Pi Long Task TODO markdown. Correct its plan and format only; do not continue or perform any attempted end work.
|
|
299
|
+
|
|
300
|
+
${todoPlanningOnlyPromptBlock(capabilityConstraints)}
|
|
301
|
+
|
|
302
|
+
Validation/extraction error:
|
|
303
|
+
${validationError.trim() || "Unknown validation error."}
|
|
304
|
+
|
|
305
|
+
Required format:
|
|
306
|
+
- Output only corrected markdown, with no commentary and no code fence.
|
|
307
|
+
- Start with exactly: # Pi Long Task TODO
|
|
308
|
+
- Include a ## Progress section with one unchecked line per task: - [ ] TODO N — Title
|
|
309
|
+
- Include a --- separator before task sections.
|
|
310
|
+
- Create sequential sections named ## TODO N — Title.
|
|
311
|
+
- Each task section must include **Goal:**, **Status:** with unchecked checkbox items, **Verify:** with concrete verification guidance, and **Done when:**.
|
|
312
|
+
- Keep tasks focused and independently assignable to worker sessions.
|
|
313
|
+
${goalBlock}
|
|
314
|
+
Original raw input:
|
|
315
|
+
|
|
316
|
+
${rawInput.trim()}
|
|
317
|
+
|
|
318
|
+
Previous invalid output (repair its planning content; do not extend its end work):
|
|
319
|
+
|
|
320
|
+
${invalidOutput.trim()}
|
|
321
|
+
`;
|
|
245
322
|
}
|
|
246
323
|
|
|
247
324
|
function todoGoalPromptBlock(goal: string | undefined): string {
|