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
package/src/types.ts
CHANGED
|
@@ -1,9 +1,41 @@
|
|
|
1
1
|
import type { Static } from "typebox";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
|
|
4
|
+
import { MAX_PLANNER_DURATION_MS, type PlannerBudget } from "./planner_config.ts";
|
|
4
5
|
import type { TaskProgressModel } from "./task_progress.ts";
|
|
6
|
+
import type { WorkerCapabilityWarning } from "./worker_capabilities.ts";
|
|
5
7
|
import type { SessionOutcome } from "./worker_session.ts";
|
|
6
8
|
|
|
9
|
+
const NetworkRecoveryParams = Type.Object(
|
|
10
|
+
{
|
|
11
|
+
enabled: Type.Optional(
|
|
12
|
+
Type.Boolean({
|
|
13
|
+
description:
|
|
14
|
+
"Enable coordinator-level recovery after Pi's bounded provider retries are exhausted. Defaults to false for backward compatibility.",
|
|
15
|
+
}),
|
|
16
|
+
),
|
|
17
|
+
baseDelayMs: Type.Optional(
|
|
18
|
+
Type.Integer({
|
|
19
|
+
minimum: 1,
|
|
20
|
+
description: "Initial network-recovery delay in milliseconds. Defaults to 1000.",
|
|
21
|
+
}),
|
|
22
|
+
),
|
|
23
|
+
maxDelayMs: Type.Optional(
|
|
24
|
+
Type.Integer({
|
|
25
|
+
minimum: 1,
|
|
26
|
+
description: "Maximum network-recovery backoff delay in milliseconds. Defaults to 30000.",
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
maxOutageMs: Type.Optional(
|
|
30
|
+
Type.Union([Type.Integer({ minimum: 1 }), Type.Null()], {
|
|
31
|
+
description:
|
|
32
|
+
"Maximum continuous outage duration in milliseconds. Defaults to 300000; use null to wait indefinitely until cancelled.",
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
{ additionalProperties: false },
|
|
37
|
+
);
|
|
38
|
+
|
|
7
39
|
export const PiLongTaskParams = Type.Object(
|
|
8
40
|
{
|
|
9
41
|
inputText: Type.Optional(
|
|
@@ -21,6 +53,23 @@ export const PiLongTaskParams = Type.Object(
|
|
|
21
53
|
description: "Optional high-level goal or desired outcome for the long-task run.",
|
|
22
54
|
}),
|
|
23
55
|
),
|
|
56
|
+
todoTimeoutMs: Type.Optional(
|
|
57
|
+
Type.Integer({
|
|
58
|
+
minimum: 1,
|
|
59
|
+
maximum: MAX_PLANNER_DURATION_MS,
|
|
60
|
+
description:
|
|
61
|
+
"Explicit TODO-planner timeout in milliseconds. When omitted, the 5-minute default adapts deterministically for explicit item counts, enumerated deliverables, and separately planned tasks, up to 15 minutes.",
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
todoGracefulShutdownMs: Type.Optional(
|
|
65
|
+
Type.Integer({
|
|
66
|
+
minimum: 0,
|
|
67
|
+
maximum: MAX_PLANNER_DURATION_MS,
|
|
68
|
+
description:
|
|
69
|
+
"Grace period in milliseconds after the TODO-planner timeout. Defaults to 15000 (15 seconds); use 0 to disable the grace period.",
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
networkRecovery: Type.Optional(NetworkRecoveryParams),
|
|
24
73
|
},
|
|
25
74
|
{ additionalProperties: false },
|
|
26
75
|
);
|
|
@@ -82,6 +131,23 @@ export const PiGoalTaskParams = Type.Object(
|
|
|
82
131
|
description: "Maximum bash command timeout in milliseconds allowed in worker sessions.",
|
|
83
132
|
}),
|
|
84
133
|
),
|
|
134
|
+
todoTimeoutMs: Type.Optional(
|
|
135
|
+
Type.Integer({
|
|
136
|
+
minimum: 1,
|
|
137
|
+
maximum: MAX_PLANNER_DURATION_MS,
|
|
138
|
+
description:
|
|
139
|
+
"Explicit TODO-planner timeout in milliseconds for child long-task planning and plan revisions. When omitted, the 5-minute default adapts deterministically for explicit item counts, enumerated deliverables, and separately planned tasks, up to 15 minutes.",
|
|
140
|
+
}),
|
|
141
|
+
),
|
|
142
|
+
todoGracefulShutdownMs: Type.Optional(
|
|
143
|
+
Type.Integer({
|
|
144
|
+
minimum: 0,
|
|
145
|
+
maximum: MAX_PLANNER_DURATION_MS,
|
|
146
|
+
description:
|
|
147
|
+
"Grace period in milliseconds after a child TODO-planner timeout. Defaults to 15000 (15 seconds); use 0 to disable the grace period.",
|
|
148
|
+
}),
|
|
149
|
+
),
|
|
150
|
+
networkRecovery: Type.Optional(NetworkRecoveryParams),
|
|
85
151
|
},
|
|
86
152
|
{ additionalProperties: false },
|
|
87
153
|
);
|
|
@@ -133,6 +199,8 @@ export interface PiLongTaskResult {
|
|
|
133
199
|
}>;
|
|
134
200
|
taskProgress: TaskProgressModel;
|
|
135
201
|
workerCostTotal: number;
|
|
202
|
+
plannerBudget?: Readonly<PlannerBudget>;
|
|
203
|
+
capabilityWarnings?: readonly WorkerCapabilityWarning[];
|
|
136
204
|
commit: boolean;
|
|
137
205
|
goal?: string;
|
|
138
206
|
error?: string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export interface IsolatedWorkerCapabilities {
|
|
2
|
+
/** Tool names exposed directly to the worker session. */
|
|
3
|
+
tools: readonly string[];
|
|
4
|
+
/** Pi Long Task isolated sessions deliberately disable extension runtimes. */
|
|
5
|
+
extensionsEnabled: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type WorkerCapabilityWarningCode = "unavailable_browser_capability";
|
|
9
|
+
|
|
10
|
+
export interface WorkerCapabilityWarning {
|
|
11
|
+
code: WorkerCapabilityWarningCode;
|
|
12
|
+
requestedCapabilities: readonly string[];
|
|
13
|
+
availableTools: readonly string[];
|
|
14
|
+
message: string;
|
|
15
|
+
planningConstraint: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const BROWSER_EXTENSION_REQUIREMENT_RE =
|
|
19
|
+
/\b(?:must(?:\s+use)?|need(?:s|ed)?(?:\s+to\s+use)?|require(?:s|d)?|rely(?:ing)?\s+on|use|using|via|through|with)\s+(?:the\s+|an?\s+)?(?:google\s+)?(?:chrome|chromium|firefox|edge|browser)(?:\s+(?:browser|devtools?))?\s+(?:extension|mcp|connector)\b/i;
|
|
20
|
+
const REQUIRED_NAMED_BROWSER_EXTENSION_RE =
|
|
21
|
+
/\b(?:chrome\s+devtools\s+(?:mcp|extension|tool)|(?:chrome|browser)\s+(?:mcp|extension(?:\s+tool)?|tool\s+extension))\s+(?:is\s+(?:required|needed)|must\s+be\s+used)\b/i;
|
|
22
|
+
const IMPERATIVE_CHROME_RE =
|
|
23
|
+
/\b(?:open|launch|control|drive|browse\s+with|inspect\s+(?:with|using)|scrape\s+(?:with|using)|fetch\s+(?:with|using)|test\s+(?:with|using))\s+(?:google\s+)?chrome\b/i;
|
|
24
|
+
const EXPLICIT_BROWSER_TOOL_RE =
|
|
25
|
+
/\b(?:must(?:\s+use)?|need(?:s|ed)?(?:\s+to\s+use)?|require(?:s|d)?|use|using|via|through|with)\s+(?:the\s+)?(browser|chrome|web[_-]?fetch|playwright|puppeteer)\s+tool\b/i;
|
|
26
|
+
const EXPLICIT_CHROME_RUNTIME_RE =
|
|
27
|
+
/(?:\b(?:must\s+use|need(?:s|ed)?\s+to\s+use|use|using)\s+(?:google\s+)?chrome(?:\s+devtools?)?(?=\s+(?:to|for)\b|\s*[.,;:]|\s*$)|\b(?:via|through)\s+(?:google\s+)?chrome\b|\b(?:google\s+)?chrome(?:\s+devtools?)?\s+(?:is\s+required|must\s+be\s+used)\b)/i;
|
|
28
|
+
const NEGATED_CAPABILITY_RE =
|
|
29
|
+
/\b(?:do\s+not|don't|dont|never|avoid|without)\s+(?:use|using|rely(?:ing)?\s+on|requiring?)?\s*(?:the\s+|an?\s+)?(?:google\s+)?(?:chrome(?:\s+(?:browser|devtools?))?|chromium|firefox|edge|browser|web[_-]?fetch|playwright|puppeteer)(?:\s+(?:extension|mcp|tool(?:ing)?|connector))?/gi;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detect only explicit requests to invoke a browser capability. Merely asking
|
|
33
|
+
* workers to build or support a browser extension is implementation work and
|
|
34
|
+
* does not imply that the extension must be loaded during the run.
|
|
35
|
+
*/
|
|
36
|
+
export function detectUnavailableWorkerCapabilities(
|
|
37
|
+
requestText: string,
|
|
38
|
+
capabilities: Readonly<IsolatedWorkerCapabilities>,
|
|
39
|
+
): WorkerCapabilityWarning[] {
|
|
40
|
+
const text = requestText.replace(NEGATED_CAPABILITY_RE, " ");
|
|
41
|
+
const extensionRequested =
|
|
42
|
+
BROWSER_EXTENSION_REQUIREMENT_RE.test(text) ||
|
|
43
|
+
REQUIRED_NAMED_BROWSER_EXTENSION_RE.test(text) ||
|
|
44
|
+
IMPERATIVE_CHROME_RE.test(text);
|
|
45
|
+
const browserToolMatch = EXPLICIT_BROWSER_TOOL_RE.exec(text);
|
|
46
|
+
const requestedDirectTools = [
|
|
47
|
+
browserToolMatch?.[1],
|
|
48
|
+
EXPLICIT_CHROME_RUNTIME_RE.test(text) ? "chrome" : undefined,
|
|
49
|
+
].filter((item, index, all): item is string => Boolean(item) && all.indexOf(item) === index);
|
|
50
|
+
const unavailableDirectTools = requestedDirectTools.filter(
|
|
51
|
+
(requested) =>
|
|
52
|
+
!capabilities.tools.some((available) => canonicalToolName(available) === canonicalToolName(requested)),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if ((!extensionRequested || capabilities.extensionsEnabled) && unavailableDirectTools.length === 0) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const requestedCapabilities = [
|
|
60
|
+
extensionRequested && !capabilities.extensionsEnabled ? "Chrome/browser extension runtime" : undefined,
|
|
61
|
+
unavailableDirectTools.length > 0 ? `${unavailableDirectTools.join("/")} direct tool` : undefined,
|
|
62
|
+
].filter((item): item is string => Boolean(item));
|
|
63
|
+
const availableTools = [...capabilities.tools];
|
|
64
|
+
const toolList = availableTools.length > 0 ? availableTools.join(", ") : "none";
|
|
65
|
+
const alternatives = availableWorkerAlternatives(capabilities);
|
|
66
|
+
const alternativeText = alternatives.join(", or ");
|
|
67
|
+
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
code: "unavailable_browser_capability",
|
|
71
|
+
requestedCapabilities,
|
|
72
|
+
availableTools,
|
|
73
|
+
message:
|
|
74
|
+
`Worker capability warning: isolated Pi Long Task workers disable extensions and expose only these direct tools: ${toolList}. ` +
|
|
75
|
+
`They cannot silently use the requested ${requestedCapabilities.join(" or ")}. The run will continue, but it must use an available safe alternative when that satisfies the request: ${alternativeText}. ` +
|
|
76
|
+
"If the exact extension or browser tool is mandatory, the affected task must report blocked instead of claiming it used that capability.",
|
|
77
|
+
planningConstraint:
|
|
78
|
+
`Isolated-worker capability constraint: extensions are disabled and workers have only these direct tools: ${toolList}. ` +
|
|
79
|
+
`Do not create or execute tasks that assume the requested ${requestedCapabilities.join(" or ")} is available, and never claim it was used. ` +
|
|
80
|
+
`When equivalent, ${alternativeText}. If the exact unavailable capability is mandatory, make the affected task report blocked with the required user action.`,
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function canonicalToolName(tool: string): string {
|
|
86
|
+
return tool.toLowerCase().replace(/[-_]/g, "");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function availableWorkerAlternatives(capabilities: Readonly<IsolatedWorkerCapabilities>): string[] {
|
|
90
|
+
const alternatives: string[] = [];
|
|
91
|
+
if (capabilities.tools.includes("bash")) {
|
|
92
|
+
alternatives.push(
|
|
93
|
+
"fetch public content through a supported command-line mechanism via bash or run project-provided browser automation via bash when available",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (capabilities.tools.includes("read")) {
|
|
97
|
+
alternatives.push("supply the page or source content to the run so workers can read it locally");
|
|
98
|
+
}
|
|
99
|
+
if (alternatives.length === 0) {
|
|
100
|
+
alternatives.push("supply the needed source content to the run");
|
|
101
|
+
}
|
|
102
|
+
return alternatives;
|
|
103
|
+
}
|
package/src/worker_config.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NetworkRecoveryConfigError,
|
|
3
|
+
resolveNetworkRecoveryConfig,
|
|
4
|
+
type NetworkRecoveryConfigInput,
|
|
5
|
+
} from "./network_recovery_config.ts";
|
|
6
|
+
import { MAX_PLANNER_DURATION_MS, PlannerDurationConfigError } from "./planner_config.ts";
|
|
7
|
+
|
|
1
8
|
export interface ParsedWorkerRuntimeConfig {
|
|
2
9
|
modelName?: string;
|
|
3
10
|
maxAttemptsPerTask?: number;
|
|
4
11
|
taskTimeoutMs?: number;
|
|
12
|
+
todoTimeoutMs?: number;
|
|
13
|
+
todoGracefulShutdownMs?: number;
|
|
5
14
|
maxBashTimeoutMs?: number;
|
|
6
15
|
workerSessionReuseEnabled?: boolean;
|
|
7
16
|
workerSessionReuseContextThresholdPercent?: number;
|
|
17
|
+
networkRecovery?: NetworkRecoveryConfigInput;
|
|
8
18
|
}
|
|
9
19
|
|
|
10
20
|
type MutableWorkerRuntimeConfig = ParsedWorkerRuntimeConfig & { provider?: string; model?: string };
|
|
@@ -36,12 +46,17 @@ export function parseWorkerRuntimeConfig(text: string): ParsedWorkerRuntimeConfi
|
|
|
36
46
|
|
|
37
47
|
parseLineDirectives(text, state);
|
|
38
48
|
parseNaturalLanguageDirectives(text, state);
|
|
49
|
+
if (state.networkRecovery) {
|
|
50
|
+
resolveNetworkRecoveryConfig(state.networkRecovery);
|
|
51
|
+
}
|
|
39
52
|
|
|
40
53
|
const modelName = combineProviderAndModel(state.provider, state.model);
|
|
41
54
|
return {
|
|
42
55
|
...(modelName ? { modelName } : {}),
|
|
43
56
|
...(state.maxAttemptsPerTask !== undefined ? { maxAttemptsPerTask: state.maxAttemptsPerTask } : {}),
|
|
44
57
|
...(state.taskTimeoutMs !== undefined ? { taskTimeoutMs: state.taskTimeoutMs } : {}),
|
|
58
|
+
...(state.todoTimeoutMs !== undefined ? { todoTimeoutMs: state.todoTimeoutMs } : {}),
|
|
59
|
+
...(state.todoGracefulShutdownMs !== undefined ? { todoGracefulShutdownMs: state.todoGracefulShutdownMs } : {}),
|
|
45
60
|
...(state.maxBashTimeoutMs !== undefined ? { maxBashTimeoutMs: state.maxBashTimeoutMs } : {}),
|
|
46
61
|
...(state.workerSessionReuseEnabled !== undefined
|
|
47
62
|
? { workerSessionReuseEnabled: state.workerSessionReuseEnabled }
|
|
@@ -49,6 +64,7 @@ export function parseWorkerRuntimeConfig(text: string): ParsedWorkerRuntimeConfi
|
|
|
49
64
|
...(state.workerSessionReuseContextThresholdPercent !== undefined
|
|
50
65
|
? { workerSessionReuseContextThresholdPercent: state.workerSessionReuseContextThresholdPercent }
|
|
51
66
|
: {}),
|
|
67
|
+
...(state.networkRecovery ? { networkRecovery: { ...state.networkRecovery } } : {}),
|
|
52
68
|
};
|
|
53
69
|
}
|
|
54
70
|
|
|
@@ -124,14 +140,36 @@ function parseNaturalLanguageDirectives(text: string, state: MutableWorkerRuntim
|
|
|
124
140
|
|
|
125
141
|
captureDurations(
|
|
126
142
|
text,
|
|
127
|
-
/\b(
|
|
143
|
+
/\b(?:todo\s+)?(?:planner|planning)\s+timeout\s*(?:is|=|:|to|of)?\s*(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?)/gi,
|
|
144
|
+
(value) => {
|
|
145
|
+
state.todoTimeoutMs = value;
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
captureDurations(
|
|
149
|
+
text,
|
|
150
|
+
/\b(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h))\s+(?:todo\s+)?(?:planner|planning)\s+timeout\b/gi,
|
|
151
|
+
(value) => {
|
|
152
|
+
state.todoTimeoutMs = value;
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
captureDurations(
|
|
156
|
+
text,
|
|
157
|
+
/\b(?:todo\s+)?(?:planner|planning)\s+(?:grace(?:ful)?(?:\s+shutdown|\s+period)?|shutdown\s+grace(?:\s+period)?)\s*(?:duration\s*)?(?:is|=|:|to|of)?\s*(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?)/gi,
|
|
158
|
+
(value) => {
|
|
159
|
+
state.todoGracefulShutdownMs = value;
|
|
160
|
+
},
|
|
161
|
+
{ allowZero: true },
|
|
162
|
+
);
|
|
163
|
+
captureDurations(
|
|
164
|
+
text,
|
|
165
|
+
/\b(?<!bash\s)(?<!max\s)(?<!planner\s)(?<!planning\s)(?:worker\s+|task\s+)?timeout\s*(?:is|=|:|to|of)?\s*(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?)/gi,
|
|
128
166
|
(value) => {
|
|
129
167
|
state.taskTimeoutMs = value;
|
|
130
168
|
},
|
|
131
169
|
);
|
|
132
170
|
captureDurations(
|
|
133
171
|
text,
|
|
134
|
-
/\b(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h))\
|
|
172
|
+
/\b(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h))[ \t]+(?:worker[ \t]+|task[ \t]+)?timeout\b/gi,
|
|
135
173
|
(value) => {
|
|
136
174
|
state.taskTimeoutMs = value;
|
|
137
175
|
},
|
|
@@ -159,6 +197,11 @@ function parseNaturalLanguageDirectives(text: string, state: MutableWorkerRuntim
|
|
|
159
197
|
}
|
|
160
198
|
|
|
161
199
|
function applyDirective(key: string, value: string, state: MutableWorkerRuntimeConfig): void {
|
|
200
|
+
if (/\bnetwork\b/.test(key) && /\brecover(?:y|ies)?\b/.test(key)) {
|
|
201
|
+
applyNetworkRecoveryDirective(key, value, state);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
162
205
|
if (/\breuse\b/.test(key) && /\b(?:threshold|context)\b/.test(key)) {
|
|
163
206
|
const threshold = percentageFromText(value);
|
|
164
207
|
if (threshold !== undefined) {
|
|
@@ -199,6 +242,16 @@ function applyDirective(key: string, value: string, state: MutableWorkerRuntimeC
|
|
|
199
242
|
return;
|
|
200
243
|
}
|
|
201
244
|
|
|
245
|
+
if (/\b(?:planner|planning)\b/.test(key) && /\b(?:grace|graceful|shutdown)\b/.test(key)) {
|
|
246
|
+
state.todoGracefulShutdownMs = requiredPlannerDuration("graceful-shutdown duration", value, true);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (/\b(?:planner|planning)\b/.test(key) && /\btimeout\b/.test(key)) {
|
|
251
|
+
state.todoTimeoutMs = requiredPlannerDuration("timeout", value, false);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
202
255
|
if (/\bbash\b/.test(key) && /\btimeout\b/.test(key)) {
|
|
203
256
|
const timeout = durationMsFromText(value, { allowBareSeconds: true });
|
|
204
257
|
if (timeout !== undefined) {
|
|
@@ -215,6 +268,81 @@ function applyDirective(key: string, value: string, state: MutableWorkerRuntimeC
|
|
|
215
268
|
}
|
|
216
269
|
}
|
|
217
270
|
|
|
271
|
+
function applyNetworkRecoveryDirective(key: string, value: string, state: MutableWorkerRuntimeConfig): void {
|
|
272
|
+
const recovery = (state.networkRecovery ??= {});
|
|
273
|
+
|
|
274
|
+
if (/\bbase\b/.test(key) && /\bdelay\b/.test(key)) {
|
|
275
|
+
recovery.baseDelayMs = requiredNetworkRecoveryDuration("base delay", value);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (/\bmax(?:imum)?\b/.test(key) && /\bdelay\b/.test(key)) {
|
|
279
|
+
recovery.maxDelayMs = requiredNetworkRecoveryDuration("maximum delay", value);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (/\b(?:outage|duration|wait)\b/.test(key)) {
|
|
283
|
+
const normalized = trimDirectiveValue(value)
|
|
284
|
+
.toLowerCase()
|
|
285
|
+
.replace(/[.!]+$/g, "")
|
|
286
|
+
.trim();
|
|
287
|
+
if (/^(?:unlimited|indefinite|indefinitely|until cancelled|until canceled)$/.test(normalized)) {
|
|
288
|
+
recovery.maxOutageMs = null;
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
recovery.maxOutageMs = requiredNetworkRecoveryDuration("maximum outage", value);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (/\b(?:enabled?|enablement)\b/.test(key) || /\bnetwork recovery\b/.test(key)) {
|
|
295
|
+
const enabled = booleanSetting(value);
|
|
296
|
+
if (enabled === undefined) {
|
|
297
|
+
throw new NetworkRecoveryConfigError(
|
|
298
|
+
"Network recovery must be configured as enabled/disabled, on/off, true/false, or yes/no.",
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
recovery.enabled = enabled;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
throw new NetworkRecoveryConfigError(`Unknown network recovery configuration directive: ${key}.`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function requiredPlannerDuration(label: string, value: string, allowZero: boolean): number {
|
|
309
|
+
const trimmed = trimDirectiveValue(value)
|
|
310
|
+
.replace(/[.!]+$/g, "")
|
|
311
|
+
.trim();
|
|
312
|
+
const match = /^(\d+(?:\.\d+)?)\s*(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?$/i.exec(
|
|
313
|
+
trimmed,
|
|
314
|
+
);
|
|
315
|
+
const milliseconds = match ? durationMsFromText(trimmed, { allowBareSeconds: true, allowZero }) : undefined;
|
|
316
|
+
if (milliseconds === undefined || milliseconds > MAX_PLANNER_DURATION_MS) {
|
|
317
|
+
const minimum = allowZero ? "non-negative" : "positive";
|
|
318
|
+
throw new PlannerDurationConfigError(
|
|
319
|
+
`TODO planner ${label} must be a ${minimum} finite duration no greater than about 24.9 days (${MAX_PLANNER_DURATION_MS} milliseconds), for example 30s or 5m.`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return milliseconds;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function requiredNetworkRecoveryDuration(label: string, value: string): number {
|
|
326
|
+
const trimmed = trimDirectiveValue(value)
|
|
327
|
+
.replace(/[.!]+$/g, "")
|
|
328
|
+
.trim();
|
|
329
|
+
const match = /^(\d+(?:\.\d+)?)\s*(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?$/i.exec(
|
|
330
|
+
trimmed,
|
|
331
|
+
);
|
|
332
|
+
if (!match) {
|
|
333
|
+
throw new NetworkRecoveryConfigError(
|
|
334
|
+
`Network recovery ${label} must be a positive finite duration (for example 1000ms, 30s, or 5m).`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const milliseconds = durationMsFromText(trimmed, { allowBareSeconds: true });
|
|
338
|
+
if (milliseconds === undefined) {
|
|
339
|
+
throw new NetworkRecoveryConfigError(
|
|
340
|
+
`Network recovery ${label} must be a positive finite duration (for example 1000ms, 30s, or 5m).`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
return milliseconds;
|
|
344
|
+
}
|
|
345
|
+
|
|
218
346
|
function captureTokens(text: string, pattern: RegExp, apply: (token: string) => void): void {
|
|
219
347
|
for (const match of text.matchAll(pattern)) {
|
|
220
348
|
const token = modelToken(match[1] ?? "");
|
|
@@ -233,9 +361,17 @@ function captureNumbers(text: string, pattern: RegExp, apply: (value: number) =>
|
|
|
233
361
|
}
|
|
234
362
|
}
|
|
235
363
|
|
|
236
|
-
function captureDurations(
|
|
364
|
+
function captureDurations(
|
|
365
|
+
text: string,
|
|
366
|
+
pattern: RegExp,
|
|
367
|
+
apply: (value: number) => void,
|
|
368
|
+
options: { allowZero?: boolean } = {},
|
|
369
|
+
): void {
|
|
237
370
|
for (const match of text.matchAll(pattern)) {
|
|
238
|
-
const value = durationMsFromText(match[1] ?? "", {
|
|
371
|
+
const value = durationMsFromText(match[1] ?? "", {
|
|
372
|
+
allowBareSeconds: true,
|
|
373
|
+
allowZero: options.allowZero,
|
|
374
|
+
});
|
|
239
375
|
if (value !== undefined) {
|
|
240
376
|
apply(value);
|
|
241
377
|
}
|
|
@@ -299,7 +435,10 @@ function booleanSetting(value: string): boolean | undefined {
|
|
|
299
435
|
return undefined;
|
|
300
436
|
}
|
|
301
437
|
|
|
302
|
-
function durationMsFromText(
|
|
438
|
+
function durationMsFromText(
|
|
439
|
+
value: string,
|
|
440
|
+
options: { allowBareSeconds: boolean; allowZero?: boolean },
|
|
441
|
+
): number | undefined {
|
|
303
442
|
const match = /(\d+(?:\.\d+)?)\s*(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?\b/i.exec(
|
|
304
443
|
value,
|
|
305
444
|
);
|
|
@@ -308,7 +447,7 @@ function durationMsFromText(value: string, options: { allowBareSeconds: boolean
|
|
|
308
447
|
}
|
|
309
448
|
|
|
310
449
|
const amount = Number.parseFloat(match[1] ?? "");
|
|
311
|
-
if (!Number.isFinite(amount) || amount
|
|
450
|
+
if (!Number.isFinite(amount) || amount < 0 || (!options.allowZero && amount === 0)) {
|
|
312
451
|
return undefined;
|
|
313
452
|
}
|
|
314
453
|
|
|
@@ -319,7 +458,9 @@ function durationMsFromText(value: string, options: { allowBareSeconds: boolean
|
|
|
319
458
|
|
|
320
459
|
const multiplier = durationMultiplier(unit || "seconds");
|
|
321
460
|
const milliseconds = Math.round(amount * multiplier);
|
|
322
|
-
return Number.isSafeInteger(milliseconds) && milliseconds
|
|
461
|
+
return Number.isSafeInteger(milliseconds) && (options.allowZero ? milliseconds >= 0 : milliseconds > 0)
|
|
462
|
+
? milliseconds
|
|
463
|
+
: undefined;
|
|
323
464
|
}
|
|
324
465
|
|
|
325
466
|
function durationMultiplier(unit: string): number {
|
package/src/worker_session.ts
CHANGED
|
@@ -8,14 +8,25 @@ import {
|
|
|
8
8
|
parseCompleteTaskResult,
|
|
9
9
|
parseReportedStatus,
|
|
10
10
|
} from "./result_writer.ts";
|
|
11
|
+
import type { NetworkRecoveryConfig } from "./network_recovery_config.ts";
|
|
11
12
|
import type { Task } from "./todo_parser.ts";
|
|
12
13
|
|
|
14
|
+
export interface WorkerNetworkRecoveryContext {
|
|
15
|
+
/** One-based coordinator network retry count; this is not a task attempt. */
|
|
16
|
+
retryCount: number;
|
|
17
|
+
durableEvidencePath: string;
|
|
18
|
+
priorSessionId?: string;
|
|
19
|
+
failure: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
export interface WorkerTaskPromptOptions {
|
|
14
23
|
todoPath: string;
|
|
15
24
|
task: Pick<Task, "taskId" | "title" | "section">;
|
|
16
25
|
attempt: number;
|
|
17
26
|
commitRequested: boolean;
|
|
18
27
|
previousAttempts?: string;
|
|
28
|
+
/** Continuity supplied only when a failed transport session is replaced. */
|
|
29
|
+
networkRecoveryContext?: WorkerNetworkRecoveryContext;
|
|
19
30
|
globalInstructions?: string;
|
|
20
31
|
goal?: string;
|
|
21
32
|
maxBashTimeoutSeconds: number;
|
|
@@ -43,6 +54,18 @@ Previous attempts for this same assigned task are below. Use them only as contin
|
|
|
43
54
|
\`\`\`text
|
|
44
55
|
${previousAttempts}
|
|
45
56
|
\`\`\`
|
|
57
|
+
`
|
|
58
|
+
: "";
|
|
59
|
+
|
|
60
|
+
const recovery = options.networkRecoveryContext;
|
|
61
|
+
const recoveryText = recovery
|
|
62
|
+
? `
|
|
63
|
+
Network recovery continuation (network retry ${recovery.retryCount}, still ordinary task attempt ${options.attempt}):
|
|
64
|
+
- The prior worker session ended only after Pi's bounded provider-request retries were exhausted: ${recovery.failure}
|
|
65
|
+
- Durable interruption evidence was recorded in \`${recovery.durableEvidencePath}\`${recovery.priorSessionId ? ` for session \`${recovery.priorSessionId}\`` : ""}.
|
|
66
|
+
- The prior session may already have completed tool calls and changed the working tree. Inspect the durable evidence and current files before acting.
|
|
67
|
+
- Continue this same TODO from its current state. Never blindly replay prior edits, commands, commits, external writes, or other side effects.
|
|
68
|
+
- Report one final TASK_RESULT for the assignment only after verifying what remains.
|
|
46
69
|
`
|
|
47
70
|
: "";
|
|
48
71
|
|
|
@@ -93,7 +116,7 @@ Rules:
|
|
|
93
116
|
- Do not run bash commands with timeout greater than ${options.maxBashTimeoutSeconds.toFixed(0)} seconds. For long full-suite checks, run once with a bounded timeout and report any timeout/failure in TASK_RESULT instead of continuing indefinitely.
|
|
94
117
|
- If TODO-file global instructions restrict scope, obey them strictly. If the task appears to require out-of-scope code changes, stop and report \`status: blocked\` instead of changing those files.
|
|
95
118
|
|
|
96
|
-
${globalText}Assigned task content only:
|
|
119
|
+
${globalText}${recoveryText}Assigned task content only:
|
|
97
120
|
|
|
98
121
|
\`\`\`markdown
|
|
99
122
|
${options.task.section.trimEnd()}
|
|
@@ -289,6 +312,8 @@ export type WorkerSessionFactory = (options: CreateWorkerSessionOptions) => Prom
|
|
|
289
312
|
export interface RunWorkerTaskOptions extends WorkerTaskPromptOptions, CreateWorkerSessionOptions {
|
|
290
313
|
taskTimeoutSeconds?: number;
|
|
291
314
|
gracefulShutdownSeconds?: number;
|
|
315
|
+
/** Normalized coordinator policy for resuming this operation after transient transport failure. */
|
|
316
|
+
networkRecovery?: Readonly<NetworkRecoveryConfig>;
|
|
292
317
|
abortSignal?: AbortSignal;
|
|
293
318
|
sessionFactory?: WorkerSessionFactory;
|
|
294
319
|
onEvent?: (event: CapturedWorkerEvent) => void;
|
|
@@ -353,6 +378,8 @@ export interface SessionOutcome {
|
|
|
353
378
|
timedOut: boolean;
|
|
354
379
|
aborted: boolean;
|
|
355
380
|
error?: string;
|
|
381
|
+
/** Original provider/transport failure retained for coordinator classification. */
|
|
382
|
+
failure?: unknown;
|
|
356
383
|
}
|
|
357
384
|
|
|
358
385
|
export function buildMissingTaskResultMessage(): string {
|
|
@@ -512,6 +539,7 @@ export async function runWorkerTaskAssignment(
|
|
|
512
539
|
let timedOut = false;
|
|
513
540
|
let aborted = false;
|
|
514
541
|
let error: string | undefined;
|
|
542
|
+
let failure: unknown;
|
|
515
543
|
let finished = false;
|
|
516
544
|
let turnCount = 0;
|
|
517
545
|
let messageUsageCostTotal = 0;
|
|
@@ -637,6 +665,7 @@ export async function runWorkerTaskAssignment(
|
|
|
637
665
|
},
|
|
638
666
|
(exc: unknown) => {
|
|
639
667
|
settled = true;
|
|
668
|
+
failure ??= exc;
|
|
640
669
|
error = error ?? errorMessage(exc);
|
|
641
670
|
resolvePromptWait?.();
|
|
642
671
|
resolvePromptWait = undefined;
|
|
@@ -773,6 +802,7 @@ export async function runWorkerTaskAssignment(
|
|
|
773
802
|
assistantText = latestInvocationAssistantText(session, assistantText, invocationMessageStart, !reusedAssignment);
|
|
774
803
|
}
|
|
775
804
|
} catch (exc) {
|
|
805
|
+
failure ??= exc;
|
|
776
806
|
error = error ?? errorMessage(exc);
|
|
777
807
|
} finally {
|
|
778
808
|
finished = true;
|
|
@@ -823,6 +853,7 @@ export async function runWorkerTaskAssignment(
|
|
|
823
853
|
timedOut,
|
|
824
854
|
aborted: aborted || cancelled,
|
|
825
855
|
error,
|
|
856
|
+
...(failure === undefined ? {} : { failure }),
|
|
826
857
|
};
|
|
827
858
|
}
|
|
828
859
|
|
|
@@ -872,6 +903,7 @@ export function buildWorkerSessionCreationFailureOutcome(
|
|
|
872
903
|
timedOut: false,
|
|
873
904
|
aborted: Boolean(options.abortSignal?.aborted),
|
|
874
905
|
error: message,
|
|
906
|
+
failure: error,
|
|
875
907
|
};
|
|
876
908
|
}
|
|
877
909
|
|