pi-long-task 0.3.9 → 0.3.11
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 +221 -5
- package/package.json +1 -1
- package/src/coordinator.ts +33 -7
- package/src/coverage_goal.ts +90 -0
- package/src/goal_discovery.ts +741 -0
- package/src/goal_loop.ts +586 -0
- package/src/goal_orchestrator.ts +399 -0
- package/src/goal_review.ts +616 -0
- package/src/goal_spec.ts +670 -0
- package/src/goal_state.ts +228 -0
- package/src/goal_todo_execution.ts +309 -0
- package/src/goal_todo_generation.ts +542 -0
- package/src/index.ts +89 -2
- package/src/input_router.ts +153 -6
- package/src/render.ts +238 -4
- package/src/todo_generator.ts +135 -7
- package/src/types.ts +74 -3
- package/src/worker_session.ts +23 -2
package/src/input_router.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { inferCoverageGoalText } from "./coverage_goal.ts";
|
|
2
|
+
|
|
1
3
|
const LONG_TASK_RE = /\b(?:long[-\s]?task|longtask|large[-\s]?task|big[-\s]?task|multi[-\s]?step(?:\s+task)?)\b/i;
|
|
2
4
|
const DIRECT_LONG_TASK_REQUEST_RE =
|
|
3
5
|
/\b(?:run|start|do|handle|execute|launch|kick\s+off|use)\s+(?:a\s+|the\s+)?(?:long[-\s]?task|longtask|large[-\s]?task|big[-\s]?task|multi[-\s]?step(?:\s+task)?)\b/i;
|
|
@@ -6,20 +8,69 @@ const WANT_LONG_TASK_RE =
|
|
|
6
8
|
const NEGATED_LONG_TASK_RE =
|
|
7
9
|
/\b(?:do\s+not|don't|dont|never)\s+(?:run|start|do|handle|execute|launch|kick\s+off|use)\s+(?:a\s+|the\s+)?(?:long[-\s]?task|longtask|large[-\s]?task|big[-\s]?task|multi[-\s]?step(?:\s+task)?)\b/i;
|
|
8
10
|
const INFORMATION_QUESTION_RE = /^\s*(?:how|what|why|when|where|who)\b/i;
|
|
9
|
-
const EXPLICIT_TOOL_RE = /\
|
|
11
|
+
const EXPLICIT_TOOL_RE = /\b(?:pi_long_task|pi_goal_task)\b/i;
|
|
12
|
+
const GOAL_LOOP_RE =
|
|
13
|
+
/\b(?:goal[-\s]?oriented\s+(?:loop|long[-\s]?task|task)|goal\s+(?:loop|task)|iterative\s+long[-\s]?task|repeat\s+until\s+complete|iterate\s+until\s+complete)\b/i;
|
|
14
|
+
const DIRECT_GOAL_TASK_REQUEST_RE =
|
|
15
|
+
/\b(?:run|start|do|handle|execute|launch|kick\s+off|use)\s+(?:a\s+|the\s+)?(?:goal[-\s]?oriented\s+(?:loop|long[-\s]?task|task)|goal\s+(?:loop|task)|iterative\s+long[-\s]?task)\b/i;
|
|
10
16
|
|
|
11
17
|
const COMMIT_FALSE_RE =
|
|
12
18
|
/\b(?:without|no|disable|disabled|off)\s+commits?\b|\bcommits?\s*(?:false|off|disabled)\b|\bcommit\s*:\s*(?:false|off|no)\b|\b(?:do\s+not|don't|dont)\s+commit\b/i;
|
|
13
19
|
const COMMIT_TRUE_RE =
|
|
14
20
|
/\bwith\s+commits?\b|\bcommits?\s*(?:true|on|enabled)\b|\bcommit\s*:\s*(?:true|on|yes)\b|\bcommit(?:ting)?\s+as\s+(?:you|we)\s+go\b|\b(?:make|create|include|allow|enable)\s+commits?\b/i;
|
|
21
|
+
const GOAL_RE = /\b(?:with\s+(?:the\s+)?goal|goal)\s*(?::|=|\b(?:to|of|for|that)\b)\s*([\s\S]+)$/i;
|
|
22
|
+
const TRAILING_COMMIT_MODIFIER_RE =
|
|
23
|
+
/(?:\s+(?:with|without|no|enable|enabled|disable|disabled)\s+commits?|\s+commits?\s*(?:true|false|on|off|enabled|disabled)|\s+commit\s*:\s*(?:true|false|on|off|yes|no))\s*$/i;
|
|
24
|
+
const TRAILING_ITERATION_MODIFIER_RE =
|
|
25
|
+
/(?:[.;,\s]+(?:(?:use|set)?\s*(?:min(?:imum)?|max(?:imum)?)\s+(?:iterations?|loops?|passes?|cycles?)\s*(?::|=|of|to)?\s*\d+|at\s+least\s+\d+\s*(?:iterations?|loops?|passes?|cycles?)|(?:i\s+want\s+it\s+to\s+)?run\s+(?:for|in)\s*\d+\s*(?:iterations?|loops?|passes?|cycles?)))[.!?]*\s*$/i;
|
|
26
|
+
|
|
27
|
+
export interface ParsedLongTaskRequestOptions {
|
|
28
|
+
commit: boolean;
|
|
29
|
+
goal?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ParsedGoalTaskRequestOptions extends ParsedLongTaskRequestOptions {
|
|
33
|
+
minIterations?: number;
|
|
34
|
+
maxIterations?: number;
|
|
35
|
+
}
|
|
15
36
|
|
|
16
37
|
export function longTaskInputTransform(text: string): string | undefined {
|
|
38
|
+
const goalLoopPrompt = goalTaskInputTransform(text);
|
|
39
|
+
if (goalLoopPrompt) {
|
|
40
|
+
return goalLoopPrompt;
|
|
41
|
+
}
|
|
42
|
+
|
|
17
43
|
if (!isNaturalLanguageLongTaskRequest(text)) {
|
|
18
44
|
return undefined;
|
|
19
45
|
}
|
|
20
46
|
|
|
21
|
-
|
|
22
|
-
|
|
47
|
+
return buildLongTaskToolPrompt(text, parseLongTaskRequestOptions(text));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function goalTaskInputTransform(text: string): string | undefined {
|
|
51
|
+
if (!isNaturalLanguageGoalTaskRequest(text)) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return buildGoalTaskToolPrompt(text, parseGoalTaskRequestOptions(text));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseLongTaskRequestOptions(text: string): ParsedLongTaskRequestOptions {
|
|
59
|
+
return {
|
|
60
|
+
commit: inferCommitSetting(text) ?? false,
|
|
61
|
+
goal: inferGoalSetting(text),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseGoalTaskRequestOptions(text: string): ParsedGoalTaskRequestOptions {
|
|
66
|
+
const maxIterations = inferMaxIterations(text);
|
|
67
|
+
const minIterations = inferMinIterations(text) ?? inferExactIterationCount(text) ?? maxIterations;
|
|
68
|
+
return {
|
|
69
|
+
commit: inferCommitSetting(text) ?? true,
|
|
70
|
+
goal: inferGoalSetting(text) ?? inferGoalLoopText(text),
|
|
71
|
+
minIterations,
|
|
72
|
+
maxIterations,
|
|
73
|
+
};
|
|
23
74
|
}
|
|
24
75
|
|
|
25
76
|
export function isNaturalLanguageLongTaskRequest(text: string): boolean {
|
|
@@ -33,6 +84,22 @@ export function isNaturalLanguageLongTaskRequest(text: string): boolean {
|
|
|
33
84
|
return DIRECT_LONG_TASK_REQUEST_RE.test(trimmed) || WANT_LONG_TASK_RE.test(trimmed);
|
|
34
85
|
}
|
|
35
86
|
|
|
87
|
+
export function isNaturalLanguageGoalTaskRequest(text: string): boolean {
|
|
88
|
+
const trimmed = text.trim();
|
|
89
|
+
if (!trimmed || trimmed.startsWith("/") || EXPLICIT_TOOL_RE.test(trimmed)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (INFORMATION_QUESTION_RE.test(trimmed) || NEGATED_LONG_TASK_RE.test(trimmed)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return (
|
|
96
|
+
GOAL_LOOP_RE.test(trimmed) &&
|
|
97
|
+
(DIRECT_GOAL_TASK_REQUEST_RE.test(trimmed) ||
|
|
98
|
+
DIRECT_LONG_TASK_REQUEST_RE.test(trimmed) ||
|
|
99
|
+
WANT_LONG_TASK_RE.test(trimmed))
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
36
103
|
export function inferCommitSetting(text: string): boolean | undefined {
|
|
37
104
|
if (COMMIT_FALSE_RE.test(text)) {
|
|
38
105
|
return false;
|
|
@@ -43,11 +110,91 @@ export function inferCommitSetting(text: string): boolean | undefined {
|
|
|
43
110
|
return undefined;
|
|
44
111
|
}
|
|
45
112
|
|
|
46
|
-
function
|
|
113
|
+
export function inferGoalSetting(text: string): string | undefined {
|
|
114
|
+
const match = GOAL_RE.exec(text);
|
|
115
|
+
if (!match?.[1]) {
|
|
116
|
+
return inferCoverageGoalText(text);
|
|
117
|
+
}
|
|
118
|
+
return normalizeGoalText(match[1]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizeGoalText(text: string): string | undefined {
|
|
122
|
+
let goal = text.trim();
|
|
123
|
+
while (TRAILING_COMMIT_MODIFIER_RE.test(goal) || TRAILING_ITERATION_MODIFIER_RE.test(goal)) {
|
|
124
|
+
goal = goal.replace(TRAILING_COMMIT_MODIFIER_RE, "").replace(TRAILING_ITERATION_MODIFIER_RE, "").trim();
|
|
125
|
+
}
|
|
126
|
+
goal = goal.replace(/^["'“”‘’]+|["'“”‘’.,;:!?]+$/g, "").trim();
|
|
127
|
+
return goal || undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function inferMaxIterations(text: string): number | undefined {
|
|
131
|
+
const match = /\bmax(?:imum)?\s+(?:iterations?|loops?|passes?|cycles?)\s*(?::|=|of|to)?\s*(\d+)\b/i.exec(text);
|
|
132
|
+
return positiveIntegerMatch(match?.[1]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function inferMinIterations(text: string): number | undefined {
|
|
136
|
+
const explicitMin = /\bmin(?:imum)?\s+(?:iterations?|loops?|passes?|cycles?)\s*(?::|=|of|to)?\s*(\d+)\b/i.exec(text);
|
|
137
|
+
if (explicitMin?.[1]) {
|
|
138
|
+
return positiveIntegerMatch(explicitMin[1]);
|
|
139
|
+
}
|
|
140
|
+
const atLeast = /\bat\s+least\s+(\d+)\s*(?:iterations?|loops?|passes?|cycles?)\b/i.exec(text);
|
|
141
|
+
return positiveIntegerMatch(atLeast?.[1]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function inferExactIterationCount(text: string): number | undefined {
|
|
145
|
+
const match =
|
|
146
|
+
/\b(?:run|iterate|loop|cycle|spin)(?:\s+\w+){0,6}?\s+(?:for|in)?\s*(\d+)\s*(?:iterations?|loops?|passes?|cycles?)\b/i.exec(
|
|
147
|
+
text,
|
|
148
|
+
);
|
|
149
|
+
return positiveIntegerMatch(match?.[1]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function positiveIntegerMatch(value: string | undefined): number | undefined {
|
|
153
|
+
const parsed = value ? Number.parseInt(value, 10) : undefined;
|
|
154
|
+
return parsed && Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function inferGoalLoopText(text: string): string | undefined {
|
|
158
|
+
let goal = text.trim();
|
|
159
|
+
goal = goal.replace(
|
|
160
|
+
/^\s*(?:please\s+)?(?:run|start|do|handle|execute|launch|kick\s+off|use)\s+(?:a\s+|the\s+)?/i,
|
|
161
|
+
"",
|
|
162
|
+
);
|
|
163
|
+
goal = goal.replace(GOAL_LOOP_RE, "").trim();
|
|
164
|
+
goal = goal.replace(/^\s*(?:with\s+)?(?:goal|to|for|that)\b\s*/i, "");
|
|
165
|
+
return normalizeGoalText(goal);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildLongTaskToolPrompt(originalText: string, options: ParsedLongTaskRequestOptions): string {
|
|
169
|
+
const goalLine = options.goal ? [`Set goal to ${JSON.stringify(options.goal)}.`] : [];
|
|
47
170
|
return [
|
|
48
171
|
"Use the pi_long_task tool for this request.",
|
|
49
|
-
`Set commit to ${commit ? "true" : "false"}.`,
|
|
50
|
-
|
|
172
|
+
`Set commit to ${options.commit ? "true" : "false"}.`,
|
|
173
|
+
...goalLine,
|
|
174
|
+
"Do not rely on inputText for parsed options; commit and goal are parsed separately. Use the original request as inputText only when the tool call includes inputText.",
|
|
175
|
+
"Do not perform the work directly outside pi_long_task.",
|
|
176
|
+
"",
|
|
177
|
+
"Original request:",
|
|
178
|
+
"```text",
|
|
179
|
+
originalText.trim(),
|
|
180
|
+
"```",
|
|
181
|
+
].join("\n");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildGoalTaskToolPrompt(originalText: string, options: ParsedGoalTaskRequestOptions): string {
|
|
185
|
+
const goalLine = options.goal
|
|
186
|
+
? `Set goal to ${JSON.stringify(options.goal)}.`
|
|
187
|
+
: "Set goal from the original request.";
|
|
188
|
+
const minIterationsLine = options.minIterations ? [`Set minIterations to ${options.minIterations}.`] : [];
|
|
189
|
+
const maxIterationsLine = options.maxIterations ? [`Set maxIterations to ${options.maxIterations}.`] : [];
|
|
190
|
+
return [
|
|
191
|
+
"Use the pi_goal_task tool for this request.",
|
|
192
|
+
goalLine,
|
|
193
|
+
`Set commit to ${options.commit ? "true" : "false"}.`,
|
|
194
|
+
...minIterationsLine,
|
|
195
|
+
...maxIterationsLine,
|
|
196
|
+
"The goal loop will generate TODO markdown, execute it as a long task, review completion, and repeat until complete or stopped by limits.",
|
|
197
|
+
"Do not perform the work directly outside pi_goal_task.",
|
|
51
198
|
"",
|
|
52
199
|
"Original request:",
|
|
53
200
|
"```text",
|
package/src/render.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentToolResult, Theme, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Text, type Component } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
|
+
import type { GoalLoopState, GoalLoopStatus } from "./goal_loop.ts";
|
|
4
5
|
import type { TaskProgressModel } from "./task_progress.ts";
|
|
5
6
|
import type { CoordinatorCommitSummary, CoordinatorRemainingTask, CoordinatorStatus } from "./types.ts";
|
|
6
7
|
|
|
@@ -18,6 +19,7 @@ export interface CoordinatorResultForRendering {
|
|
|
18
19
|
remainingTasks?: CoordinatorRemainingTask[];
|
|
19
20
|
taskProgress?: TaskProgressModel;
|
|
20
21
|
workerCostTotal?: number;
|
|
22
|
+
goal?: string;
|
|
21
23
|
error?: string;
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -80,11 +82,16 @@ export function formatCoordinatorResultMessage(result: CoordinatorResultForRende
|
|
|
80
82
|
return lines.join("\n");
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
export function renderLongTaskToolCall(
|
|
85
|
+
export function renderLongTaskToolCall(
|
|
86
|
+
args: { inputText?: string; commit?: boolean; goal?: string },
|
|
87
|
+
theme: Theme,
|
|
88
|
+
): Text {
|
|
84
89
|
const commit = args.commit ? theme.fg("warning", "commit:on") : theme.fg("dim", "commit:off");
|
|
85
90
|
const input = oneLine(args.inputText ?? "");
|
|
91
|
+
const goal = oneLine(args.goal ?? "");
|
|
92
|
+
const goalPreview = goal ? ` ${theme.fg("muted", `goal:${quote(truncatePlain(goal, 48))}`)}` : "";
|
|
86
93
|
const preview = input ? ` ${theme.fg("muted", quote(truncatePlain(input, 96)))}` : "";
|
|
87
|
-
return new Text(`${theme.fg("toolTitle", theme.bold("pi_long_task"))} ${commit}${preview}`, 0, 0);
|
|
94
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("pi_long_task"))} ${commit}${goalPreview}${preview}`, 0, 0);
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
export function renderLongTaskToolResult(
|
|
@@ -105,6 +112,218 @@ export function renderLongTaskToolResult(
|
|
|
105
112
|
return new Text(renderLongTaskSummary(finalDetails, options.expanded, theme), 0, 0);
|
|
106
113
|
}
|
|
107
114
|
|
|
115
|
+
export interface GoalLoopResultForRendering {
|
|
116
|
+
state: GoalLoopState;
|
|
117
|
+
resultPath: string;
|
|
118
|
+
workerCostTotal: number;
|
|
119
|
+
reviewerCostTotal: number;
|
|
120
|
+
totalCost: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface GoalTaskToolRenderDetails {
|
|
124
|
+
goalRunId: string;
|
|
125
|
+
goal: string;
|
|
126
|
+
status: GoalLoopStatus;
|
|
127
|
+
currentIteration: number;
|
|
128
|
+
totalIterations: number;
|
|
129
|
+
minIterations: number;
|
|
130
|
+
maxIterations: number;
|
|
131
|
+
resultPath: string;
|
|
132
|
+
statePath?: string;
|
|
133
|
+
tracePath?: string;
|
|
134
|
+
workerCostTotal?: number;
|
|
135
|
+
reviewerCostTotal?: number;
|
|
136
|
+
totalCost?: number;
|
|
137
|
+
completionReason?: string;
|
|
138
|
+
latestReviewerDecision?: string;
|
|
139
|
+
remainingWork?: string[];
|
|
140
|
+
error?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function formatGoalLoopResultMessage(result: GoalLoopResultForRendering): string {
|
|
144
|
+
const state = result.state;
|
|
145
|
+
const lines = [
|
|
146
|
+
`Pi Goal Task: ${state.status}`,
|
|
147
|
+
`Goal: ${state.goal}`,
|
|
148
|
+
`Iterations: ${state.iterations.length}/${state.limits.maxIterations}`,
|
|
149
|
+
`Minimum iterations: ${state.limits.minIterations}`,
|
|
150
|
+
`Result file: ${result.resultPath}`,
|
|
151
|
+
`State file: ${state.goalRunDir}/GOAL_STATE.json`,
|
|
152
|
+
];
|
|
153
|
+
if (state.completion?.reason) {
|
|
154
|
+
lines.push(`Outcome: ${state.completion.reason}`);
|
|
155
|
+
}
|
|
156
|
+
if (result.totalCost > 0) {
|
|
157
|
+
lines.push(`Worker/reviewer spend: ${formatCost(result.totalCost)}`);
|
|
158
|
+
}
|
|
159
|
+
const latestReview = [...state.iterations].reverse().find((iteration) => iteration.reviewerResult)?.reviewerResult;
|
|
160
|
+
if (latestReview) {
|
|
161
|
+
lines.push(`Latest review: ${latestReview.decision} — ${latestReview.summary}`);
|
|
162
|
+
if (latestReview.remainingWork.length > 0) {
|
|
163
|
+
lines.push("Remaining work:", ...latestReview.remainingWork.map((item) => `- ${item}`));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return lines.join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function goalTaskDetailsFromResult(result: GoalLoopResultForRendering): GoalTaskToolRenderDetails {
|
|
170
|
+
const latestReview = [...result.state.iterations]
|
|
171
|
+
.reverse()
|
|
172
|
+
.find((iteration) => iteration.reviewerResult)?.reviewerResult;
|
|
173
|
+
return {
|
|
174
|
+
goalRunId: result.state.goalRunId,
|
|
175
|
+
goal: result.state.goal,
|
|
176
|
+
status: result.state.status,
|
|
177
|
+
currentIteration: result.state.currentIteration,
|
|
178
|
+
totalIterations: result.state.iterations.length,
|
|
179
|
+
minIterations: result.state.limits.minIterations,
|
|
180
|
+
maxIterations: result.state.limits.maxIterations,
|
|
181
|
+
resultPath: result.resultPath,
|
|
182
|
+
statePath: `${result.state.goalRunDir}/GOAL_STATE.json`,
|
|
183
|
+
tracePath: `${result.state.goalRunDir}/GOAL_TRACE.jsonl`,
|
|
184
|
+
workerCostTotal: result.workerCostTotal,
|
|
185
|
+
reviewerCostTotal: result.reviewerCostTotal,
|
|
186
|
+
totalCost: result.totalCost,
|
|
187
|
+
completionReason: result.state.completion?.reason,
|
|
188
|
+
latestReviewerDecision: latestReview?.decision,
|
|
189
|
+
remainingWork: latestReview?.remainingWork,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function renderGoalTaskToolCall(
|
|
194
|
+
args: {
|
|
195
|
+
goal?: string;
|
|
196
|
+
commit?: boolean;
|
|
197
|
+
minIterations?: number;
|
|
198
|
+
maxIterations?: number;
|
|
199
|
+
timeoutMs?: number;
|
|
200
|
+
reviewerTimeoutMs?: number;
|
|
201
|
+
},
|
|
202
|
+
theme: Theme,
|
|
203
|
+
): Text {
|
|
204
|
+
const commit = (args.commit ?? true) ? theme.fg("warning", "commit:on") : theme.fg("dim", "commit:off");
|
|
205
|
+
const goal = oneLine(args.goal ?? "");
|
|
206
|
+
const limits = [
|
|
207
|
+
args.minIterations ? `min:${args.minIterations}` : undefined,
|
|
208
|
+
args.maxIterations ? `max:${args.maxIterations}` : undefined,
|
|
209
|
+
args.timeoutMs ? `timeout:${args.timeoutMs}ms` : undefined,
|
|
210
|
+
args.reviewerTimeoutMs ? `review:${args.reviewerTimeoutMs}ms` : undefined,
|
|
211
|
+
]
|
|
212
|
+
.filter(Boolean)
|
|
213
|
+
.join(" ");
|
|
214
|
+
const limitPreview = limits ? ` ${theme.fg("muted", limits)}` : "";
|
|
215
|
+
const goalPreview = goal ? ` ${theme.fg("muted", quote(truncatePlain(goal, 80)))}` : "";
|
|
216
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("pi_goal_task"))} ${commit}${limitPreview}${goalPreview}`, 0, 0);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function renderGoalTaskToolResult(
|
|
220
|
+
result: AgentToolResult<unknown>,
|
|
221
|
+
options: ToolRenderResultOptions,
|
|
222
|
+
theme: Theme,
|
|
223
|
+
): Component {
|
|
224
|
+
const details = recordOrUndefined(result.details);
|
|
225
|
+
if (options.isPartial) {
|
|
226
|
+
return new Text(renderGoalTaskProgress(details, contentText(result), theme), 0, 0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const finalDetails = goalTaskDetails(details);
|
|
230
|
+
if (!finalDetails) {
|
|
231
|
+
return new Text(contentText(result), 0, 0);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return new Text(renderGoalTaskSummary(finalDetails, options.expanded, theme), 0, 0);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function renderGoalTaskProgress(details: Record<string, unknown> | undefined, fallback: string, theme: Theme): string {
|
|
238
|
+
const message = stringValue(details?.message) || firstLine(fallback) || "Pi Goal Task is running...";
|
|
239
|
+
const phase = stringValue(details?.phase);
|
|
240
|
+
const iteration = numberValue(details?.iteration) ?? numberValue(details?.currentIteration);
|
|
241
|
+
const minIterations = numberValue(details?.minIterations);
|
|
242
|
+
const maxIterations = numberValue(details?.maxIterations);
|
|
243
|
+
const status = stringValue(details?.status) || "running";
|
|
244
|
+
const cost = numberValue(details?.totalCost);
|
|
245
|
+
const meta = [
|
|
246
|
+
iteration ? `iteration ${iteration}${maxIterations ? `/${maxIterations}` : ""}` : undefined,
|
|
247
|
+
minIterations && iteration && iteration < minIterations ? `min:${minIterations}` : undefined,
|
|
248
|
+
status,
|
|
249
|
+
cost && cost > 0 ? formatCost(cost) : undefined,
|
|
250
|
+
]
|
|
251
|
+
.filter(Boolean)
|
|
252
|
+
.join(` ${theme.fg("dim", "·")} `);
|
|
253
|
+
const icon = phase === "complete" ? (status === "done" ? "✓" : "!") : "+";
|
|
254
|
+
const color = phase === "complete" ? statusColor(status) : "warning";
|
|
255
|
+
return `${theme.fg(color, icon)} ${theme.fg(color, "Pi Goal Task")}${meta ? ` ${theme.fg("dim", "·")} ${theme.fg("muted", meta)}` : ""}\n ${theme.fg("dim", message)}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function renderGoalTaskSummary(details: GoalTaskToolRenderDetails, expanded: boolean, theme: Theme): string {
|
|
259
|
+
const statusStyle = statusColor(details.status);
|
|
260
|
+
const icon =
|
|
261
|
+
details.status === "done" ? "✓" : details.status === "failed" ? "✗" : details.status === "cancelled" ? "×" : "!";
|
|
262
|
+
const summary = [
|
|
263
|
+
`${theme.fg(statusStyle, icon)} ${theme.fg("toolTitle", theme.bold("Pi Goal Task"))} ${theme.fg(statusStyle, details.status)}`,
|
|
264
|
+
theme.fg("muted", `${details.totalIterations}/${details.maxIterations} iterations`),
|
|
265
|
+
details.totalIterations < details.minIterations ? theme.fg("muted", `min:${details.minIterations}`) : undefined,
|
|
266
|
+
details.latestReviewerDecision ? theme.fg("muted", `review:${details.latestReviewerDecision}`) : undefined,
|
|
267
|
+
details.totalCost ? theme.fg("muted", `spend ${formatCost(details.totalCost)}`) : undefined,
|
|
268
|
+
].filter(Boolean);
|
|
269
|
+
|
|
270
|
+
if (!expanded) {
|
|
271
|
+
return summary.join(" — ");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const lines = [summary.join(" — "), theme.fg("muted", details.goal)];
|
|
275
|
+
if (details.completionReason) {
|
|
276
|
+
lines.push(theme.fg("dim", `Outcome: ${details.completionReason}`));
|
|
277
|
+
}
|
|
278
|
+
lines.push(theme.fg("dim", `Result: ${details.resultPath}`));
|
|
279
|
+
if (details.statePath) {
|
|
280
|
+
lines.push(theme.fg("dim", `State: ${details.statePath}`));
|
|
281
|
+
}
|
|
282
|
+
if (details.tracePath) {
|
|
283
|
+
lines.push(theme.fg("dim", `Trace: ${details.tracePath}`));
|
|
284
|
+
}
|
|
285
|
+
const remaining = details.remainingWork ?? [];
|
|
286
|
+
if (remaining.length > 0) {
|
|
287
|
+
lines.push(theme.fg("muted", "Remaining work:"), ...remaining.map((item) => theme.fg("dim", `- ${item}`)));
|
|
288
|
+
}
|
|
289
|
+
if (details.error) {
|
|
290
|
+
lines.push(theme.fg("error", `Error: ${details.error}`));
|
|
291
|
+
}
|
|
292
|
+
return lines.join("\n");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function goalTaskDetails(details: Record<string, unknown> | undefined): GoalTaskToolRenderDetails | undefined {
|
|
296
|
+
if (!details) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
const goalRunId = stringValue(details.goalRunId);
|
|
300
|
+
const goal = stringValue(details.goal);
|
|
301
|
+
const status = goalLoopStatus(details.status);
|
|
302
|
+
const resultPath = stringValue(details.resultPath);
|
|
303
|
+
if (!goalRunId || !goal || !status || !resultPath) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
goalRunId,
|
|
308
|
+
goal,
|
|
309
|
+
status,
|
|
310
|
+
currentIteration: numberValue(details.currentIteration) ?? 0,
|
|
311
|
+
totalIterations: numberValue(details.totalIterations) ?? 0,
|
|
312
|
+
minIterations: numberValue(details.minIterations) ?? 0,
|
|
313
|
+
maxIterations: numberValue(details.maxIterations) ?? 0,
|
|
314
|
+
resultPath,
|
|
315
|
+
statePath: stringValue(details.statePath),
|
|
316
|
+
tracePath: stringValue(details.tracePath),
|
|
317
|
+
workerCostTotal: numberValue(details.workerCostTotal),
|
|
318
|
+
reviewerCostTotal: numberValue(details.reviewerCostTotal),
|
|
319
|
+
totalCost: numberValue(details.totalCost),
|
|
320
|
+
completionReason: stringValue(details.completionReason),
|
|
321
|
+
latestReviewerDecision: stringValue(details.latestReviewerDecision),
|
|
322
|
+
remainingWork: stringArray(details.remainingWork),
|
|
323
|
+
error: stringValue(details.error),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
108
327
|
function renderLongTaskProgress(details: Record<string, unknown> | undefined, fallback: string, theme: Theme): string {
|
|
109
328
|
const message = stringValue(details?.message) || firstLine(fallback) || "Pi Long Task is running...";
|
|
110
329
|
const phase = stringValue(details?.phase);
|
|
@@ -422,11 +641,11 @@ function remainingTaskSummaries(value: unknown): CoordinatorRemainingTask[] {
|
|
|
422
641
|
});
|
|
423
642
|
}
|
|
424
643
|
|
|
425
|
-
function statusColor(status: CoordinatorStatus): "success" | "warning" | "error" {
|
|
644
|
+
function statusColor(status: CoordinatorStatus | GoalLoopStatus | string): "success" | "warning" | "error" {
|
|
426
645
|
if (status === "done") {
|
|
427
646
|
return "success";
|
|
428
647
|
}
|
|
429
|
-
if (status === "failed") {
|
|
648
|
+
if (status === "failed" || status === "cancelled") {
|
|
430
649
|
return "error";
|
|
431
650
|
}
|
|
432
651
|
return "warning";
|
|
@@ -436,6 +655,17 @@ function isCoordinatorStatus(value: string): value is CoordinatorStatus {
|
|
|
436
655
|
return value === "done" || value === "partial" || value === "blocked" || value === "failed";
|
|
437
656
|
}
|
|
438
657
|
|
|
658
|
+
function goalLoopStatus(value: unknown): GoalLoopStatus | undefined {
|
|
659
|
+
return value === "running" ||
|
|
660
|
+
value === "done" ||
|
|
661
|
+
value === "partial" ||
|
|
662
|
+
value === "blocked" ||
|
|
663
|
+
value === "failed" ||
|
|
664
|
+
value === "cancelled"
|
|
665
|
+
? value
|
|
666
|
+
: undefined;
|
|
667
|
+
}
|
|
668
|
+
|
|
439
669
|
function contentText(result: AgentToolResult<unknown>): string {
|
|
440
670
|
const content = Array.isArray(result.content) ? result.content : [];
|
|
441
671
|
return content
|
|
@@ -475,6 +705,10 @@ function nonNegativeNumberValue(value: unknown): number | undefined {
|
|
|
475
705
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
476
706
|
}
|
|
477
707
|
|
|
708
|
+
function stringArray(value: unknown): string[] {
|
|
709
|
+
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
|
|
710
|
+
}
|
|
711
|
+
|
|
478
712
|
function formatCost(value: number): string {
|
|
479
713
|
if (value === 0) {
|
|
480
714
|
return "$0";
|
package/src/todo_generator.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
coverageGoalAction,
|
|
3
|
+
coverageGoalVerification,
|
|
4
|
+
coverageGoalVerifyBullet,
|
|
5
|
+
parseCoverageGoal,
|
|
6
|
+
} from "./coverage_goal.ts";
|
|
1
7
|
import { parseTasks, TodoParseError } from "./todo_parser.ts";
|
|
2
8
|
|
|
3
9
|
const TODO_HEADING_RE = /^##\s+TODO\s+(\d+)\s+[—-]\s+(.+?)\s*$/gm;
|
|
@@ -20,21 +26,21 @@ interface ExistingTask {
|
|
|
20
26
|
body: string;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
export function todoMarkdownFromString(rawInput: string): string | undefined {
|
|
29
|
+
export function todoMarkdownFromString(rawInput: string, goal?: string): string | undefined {
|
|
24
30
|
const input = rawInput.trim();
|
|
25
31
|
if (!input) {
|
|
26
32
|
return undefined;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
if (hasTodoHeadings(input)) {
|
|
30
|
-
const markdown = normalizeExistingTodoMarkdown(input);
|
|
36
|
+
const markdown = applyGoalInstructionsToTodoMarkdown(normalizeExistingTodoMarkdown(input), goal);
|
|
31
37
|
validateTodoMarkdown(markdown);
|
|
32
38
|
return markdown;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
const listItems = simpleListItems(input);
|
|
36
42
|
if (listItems.length >= 2) {
|
|
37
|
-
const markdown = generatedTodoMarkdown(listItems);
|
|
43
|
+
const markdown = applyGoalInstructionsToTodoMarkdown(generatedTodoMarkdown(listItems), goal);
|
|
38
44
|
validateTodoMarkdown(markdown);
|
|
39
45
|
return markdown;
|
|
40
46
|
}
|
|
@@ -116,12 +122,134 @@ export function validateTodoMarkdown(markdown: string): void {
|
|
|
116
122
|
});
|
|
117
123
|
}
|
|
118
124
|
|
|
119
|
-
export function
|
|
120
|
-
|
|
125
|
+
export function applyGoalInstructionsToTodoMarkdown(markdown: string, goal?: string): string {
|
|
126
|
+
const trimmedGoal = oneLine(goal ?? "");
|
|
127
|
+
if (!trimmedGoal) {
|
|
128
|
+
return markdown;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let next = insertGlobalGoalInstructions(markdown, trimmedGoal);
|
|
132
|
+
const coverageGoal = parseCoverageGoal(trimmedGoal);
|
|
133
|
+
if (coverageGoal) {
|
|
134
|
+
next = appendCoverageVerificationToTasks(next, coverageGoalVerifyBullet(coverageGoal));
|
|
135
|
+
}
|
|
136
|
+
validateTodoMarkdown(next);
|
|
137
|
+
return next;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function insertGlobalGoalInstructions(markdown: string, goal: string): string {
|
|
141
|
+
const coverageGoal = parseCoverageGoal(goal);
|
|
142
|
+
const additions = [`- Long task goal: ${goal}`];
|
|
143
|
+
if (coverageGoal) {
|
|
144
|
+
additions.push(`- Coverage goal: ${coverageGoalAction(coverageGoal)}`);
|
|
145
|
+
additions.push(`- Coverage verification: ${coverageGoalVerification(coverageGoal)}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
149
|
+
const progressIndex = lines.findIndex((line) => /^##\s+Progress\s*$/i.test(line.trim()));
|
|
150
|
+
if (progressIndex < 0) {
|
|
151
|
+
return markdown;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const existingGlobalText = lines.slice(0, progressIndex).join("\n");
|
|
155
|
+
const missing = additions.filter((line) => !existingGlobalText.includes(line));
|
|
156
|
+
if (missing.length === 0) {
|
|
157
|
+
return markdown;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const before = trimTrailingBlankLines(lines.slice(0, progressIndex));
|
|
161
|
+
const after = trimLeadingBlankLines(lines.slice(progressIndex));
|
|
162
|
+
const hasGlobalHeading = /^Global instructions:\s*$/im.test(existingGlobalText);
|
|
163
|
+
const block = hasGlobalHeading ? missing : ["Global instructions:", ...missing];
|
|
164
|
+
return ensureTrailingNewline([...before, "", ...block, "", ...after].join("\n"));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function appendCoverageVerificationToTasks(markdown: string, verificationBullet: string): string {
|
|
168
|
+
const tasks = parseTasks(markdown);
|
|
169
|
+
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
170
|
+
for (const task of [...tasks].reverse()) {
|
|
171
|
+
if (task.section.includes(verificationBullet)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const startIndex = task.startLine - 1;
|
|
176
|
+
const endIndex = task.endLine;
|
|
177
|
+
const verifyIndex = findVerifyLineIndex(lines, startIndex, endIndex);
|
|
178
|
+
if (verifyIndex >= 0) {
|
|
179
|
+
lines.splice(verifyIndex + 1, 0, verificationBullet);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const doneIndex = findDoneWhenLineIndex(lines, startIndex, endIndex);
|
|
184
|
+
const insertIndex = doneIndex >= 0 ? doneIndex : endIndex;
|
|
185
|
+
lines.splice(insertIndex, 0, "**Verify:**", verificationBullet, "");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return ensureTrailingNewline(lines.join("\n"));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function findVerifyLineIndex(lines: readonly string[], startIndex: number, endIndex: number): number {
|
|
192
|
+
for (let idx = startIndex; idx < endIndex; idx += 1) {
|
|
193
|
+
if (/^\*\*Verify:\*\*/i.test(lines[idx].trim())) {
|
|
194
|
+
return idx;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return -1;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function findDoneWhenLineIndex(lines: readonly string[], startIndex: number, endIndex: number): number {
|
|
201
|
+
for (let idx = startIndex; idx < endIndex; idx += 1) {
|
|
202
|
+
if (/^\*\*Done when:\*\*/i.test(lines[idx].trim())) {
|
|
203
|
+
return idx;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return -1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function trimTrailingBlankLines(lines: string[]): string[] {
|
|
210
|
+
while (lines.at(-1)?.trim() === "") {
|
|
211
|
+
lines.pop();
|
|
212
|
+
}
|
|
213
|
+
return lines;
|
|
121
214
|
}
|
|
122
215
|
|
|
123
|
-
|
|
124
|
-
|
|
216
|
+
function trimLeadingBlankLines(lines: string[]): string[] {
|
|
217
|
+
while (lines[0]?.trim() === "") {
|
|
218
|
+
lines.shift();
|
|
219
|
+
}
|
|
220
|
+
return lines;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function oneLine(value: string): string {
|
|
224
|
+
return value.replace(/\s+/g, " ").trim();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function buildTodoCreationPrompt(rawInput: string, goal?: string): string {
|
|
228
|
+
const goalBlock = todoGoalPromptBlock(goal);
|
|
229
|
+
return `Convert the following raw project request into Pi Long Task-compatible TODO markdown.\n\nRequirements:\n- Output only markdown, with no commentary and no code fence.\n- Start with exactly: # Pi Long Task TODO\n- Include a ## Progress section with one unchecked line per task: - [ ] TODO N — Title\n- Include a --- separator before task sections.\n- Create sequential sections named ## TODO N — Title.\n- Each task section must include **Goal:**, **Status:** with unchecked checkbox items, **Verify:** with concrete verification guidance, and **Done when:**.\n- Preserve any global instructions or constraints that apply to all tasks above ## Progress.\n- Keep tasks focused and independently assignable to worker sessions.\n${goalBlock}\nRaw input:\n\n${rawInput.trim()}\n`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function buildTodoRepairPrompt(
|
|
233
|
+
rawInput: string,
|
|
234
|
+
invalidOutput: string,
|
|
235
|
+
validationError: string,
|
|
236
|
+
goal?: string,
|
|
237
|
+
): string {
|
|
238
|
+
const goalBlock = todoGoalPromptBlock(goal);
|
|
239
|
+
return `Your previous response was not valid Pi Long Task TODO markdown. Correct it now.\n\nValidation/extraction error:\n${validationError.trim() || "Unknown validation error."}\n\nRequirements:\n- Output only corrected markdown, with no commentary and no code fence.\n- Start with exactly: # Pi Long Task TODO\n- Include a ## Progress section with one unchecked line per task: - [ ] TODO N — Title\n- Include a --- separator before task sections.\n- Create sequential sections named ## TODO N — Title.\n- Each task section must include **Goal:**, **Status:** with unchecked checkbox items, **Verify:** with concrete verification guidance, and **Done when:**.\n- Preserve any global instructions or constraints that apply to all tasks above ## Progress.\n- Keep tasks focused and independently assignable to worker sessions.\n${goalBlock}\nOriginal raw input:\n\n${rawInput.trim()}\n\nPrevious invalid output:\n\n${invalidOutput.trim()}\n`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function todoGoalPromptBlock(goal: string | undefined): string {
|
|
243
|
+
const trimmed = goal?.trim();
|
|
244
|
+
if (!trimmed) {
|
|
245
|
+
return "";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const coverageGoal = parseCoverageGoal(trimmed);
|
|
249
|
+
const coverageBlock = coverageGoal
|
|
250
|
+
? `\nCoverage goal requirements:\n- Include global instructions that tell workers to ${coverageGoalAction(coverageGoal)}\n- Include verification guidance that tells workers to ${coverageGoalVerification(coverageGoal)}\n- Do not hardcode a fixed coverage threshold; use the requested threshold (${coverageGoal.thresholdText}%).\n`
|
|
251
|
+
: "";
|
|
252
|
+
return `\nOverall goal:\n\n${trimmed}\n${coverageBlock}`;
|
|
125
253
|
}
|
|
126
254
|
|
|
127
255
|
export function extractAndValidateTodoMarkdown(assistantText: string): string {
|