@yycholla/pi-dynamic-workflows 3.3.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/LICENSE +22 -0
- package/README.md +325 -0
- package/assets/readme/hero.png +0 -0
- package/assets/readme/hero.svg +70 -0
- package/assets/readme/package-cover.png +0 -0
- package/assets/readme/package-cover.svg +66 -0
- package/assets/readme/workflow.png +0 -0
- package/assets/readme/workflow.svg +77 -0
- package/dist/accept-workflow-guidance.d.ts +14 -0
- package/dist/accept-workflow-guidance.js +53 -0
- package/dist/adversarial-review.d.ts +30 -0
- package/dist/adversarial-review.js +107 -0
- package/dist/agent-history.d.ts +20 -0
- package/dist/agent-history.js +122 -0
- package/dist/agent-registry.d.ts +83 -0
- package/dist/agent-registry.js +190 -0
- package/dist/agent.d.ts +364 -0
- package/dist/agent.js +714 -0
- package/dist/builtin-commands.d.ts +19 -0
- package/dist/builtin-commands.js +251 -0
- package/dist/builtin-workflows.d.ts +45 -0
- package/dist/builtin-workflows.js +121 -0
- package/dist/code-review.d.ts +26 -0
- package/dist/code-review.js +181 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.js +44 -0
- package/dist/deep-research.d.ts +30 -0
- package/dist/deep-research.js +124 -0
- package/dist/display.d.ts +134 -0
- package/dist/display.js +248 -0
- package/dist/effort-command.d.ts +28 -0
- package/dist/effort-command.js +68 -0
- package/dist/enums.d.ts +69 -0
- package/dist/enums.js +78 -0
- package/dist/errors.d.ts +113 -0
- package/dist/errors.js +140 -0
- package/dist/extension-reload.d.ts +37 -0
- package/dist/extension-reload.js +78 -0
- package/dist/fs-persistence.d.ts +63 -0
- package/dist/fs-persistence.js +102 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +35 -0
- package/dist/logger.d.ts +21 -0
- package/dist/logger.js +66 -0
- package/dist/model-routing.d.ts +30 -0
- package/dist/model-routing.js +50 -0
- package/dist/model-spec.d.ts +29 -0
- package/dist/model-spec.js +252 -0
- package/dist/model-tier-config.d.ts +133 -0
- package/dist/model-tier-config.js +249 -0
- package/dist/run-persistence.d.ts +180 -0
- package/dist/run-persistence.js +294 -0
- package/dist/saved-commands.d.ts +28 -0
- package/dist/saved-commands.js +100 -0
- package/dist/shared-store.d.ts +98 -0
- package/dist/shared-store.js +212 -0
- package/dist/structured-output.d.ts +19 -0
- package/dist/structured-output.js +30 -0
- package/dist/task-panel.d.ts +61 -0
- package/dist/task-panel.js +422 -0
- package/dist/usage-limit-scheduler.d.ts +145 -0
- package/dist/usage-limit-scheduler.js +368 -0
- package/dist/web-tools.d.ts +20 -0
- package/dist/web-tools.js +120 -0
- package/dist/workflow-authoring-coverage.d.ts +70 -0
- package/dist/workflow-authoring-coverage.js +421 -0
- package/dist/workflow-authoring-reference.d.ts +20 -0
- package/dist/workflow-authoring-reference.js +156 -0
- package/dist/workflow-capability-contract.d.ts +131 -0
- package/dist/workflow-capability-contract.js +604 -0
- package/dist/workflow-commands.d.ts +18 -0
- package/dist/workflow-commands.js +260 -0
- package/dist/workflow-comprehension.d.ts +133 -0
- package/dist/workflow-comprehension.js +1321 -0
- package/dist/workflow-context-measurement.d.ts +72 -0
- package/dist/workflow-context-measurement.js +213 -0
- package/dist/workflow-control-tool.d.ts +30 -0
- package/dist/workflow-control-tool.js +176 -0
- package/dist/workflow-delivery-choice.d.ts +20 -0
- package/dist/workflow-delivery-choice.js +48 -0
- package/dist/workflow-editor.d.ts +93 -0
- package/dist/workflow-editor.js +363 -0
- package/dist/workflow-manager.d.ts +492 -0
- package/dist/workflow-manager.js +1124 -0
- package/dist/workflow-paths.d.ts +22 -0
- package/dist/workflow-paths.js +46 -0
- package/dist/workflow-release-gate.d.ts +39 -0
- package/dist/workflow-release-gate.js +309 -0
- package/dist/workflow-saved.d.ts +38 -0
- package/dist/workflow-saved.js +126 -0
- package/dist/workflow-settings.d.ts +70 -0
- package/dist/workflow-settings.js +131 -0
- package/dist/workflow-tool.d.ts +71 -0
- package/dist/workflow-tool.js +367 -0
- package/dist/workflow-ui.d.ts +182 -0
- package/dist/workflow-ui.js +1587 -0
- package/dist/workflow.d.ts +333 -0
- package/dist/workflow.js +1151 -0
- package/dist/workflows-models-command.d.ts +31 -0
- package/dist/workflows-models-command.js +156 -0
- package/dist/worktree.d.ts +25 -0
- package/dist/worktree.js +61 -0
- package/extensions/workflow.ts +151 -0
- package/package.json +104 -0
- package/skills/workflow-authoring/SKILL.md +30 -0
- package/skills/workflow-authoring/examples/adversarial-verification.js +63 -0
- package/skills/workflow-authoring/examples/bounded-semantic-retry.js +56 -0
- package/skills/workflow-authoring/examples/classify-and-act.js +67 -0
- package/skills/workflow-authoring/examples/fan-out-and-synthesize.js +47 -0
- package/skills/workflow-authoring/examples/generate-and-filter.js +68 -0
- package/skills/workflow-authoring/examples/loop-until-done.js +68 -0
- package/skills/workflow-authoring/examples/phased-budgets.js +75 -0
- package/skills/workflow-authoring/examples/saved-nested-workflows.js +44 -0
- package/skills/workflow-authoring/examples/structured-output.js +37 -0
- package/skills/workflow-authoring/examples/tournament.js +81 -0
- package/skills/workflow-authoring/examples/validated-gate.js +63 -0
- package/skills/workflow-authoring/references/capabilities.md +41 -0
- package/skills/workflow-authoring/references/capability-details.md +357 -0
- package/skills/workflow-authoring/references/common-helpers.md +4 -0
- package/skills/workflow-authoring/references/debugging.md +27 -0
- package/skills/workflow-authoring/references/focused-recipes.md +11 -0
- package/skills/workflow-authoring/references/helpers.md +9 -0
- package/skills/workflow-authoring/references/lifecycle.md +33 -0
- package/skills/workflow-authoring/references/pattern-selection.md +15 -0
- package/skills/workflow-authoring/references/quality-helpers.md +8 -0
- package/skills/workflow-authoring/references/registry-ownership.md +15 -0
- package/skills/workflow-authoring/references/retry-helper.md +5 -0
- package/skills/workflow-authoring/references/review.md +43 -0
- package/skills/workflow-authoring/references/runtime.md +27 -0
- package/skills/workflow-authoring/references/specialized-helpers.md +19 -0
- package/skills/workflow-authoring/references/versions.md +13 -0
- package/skills/workflow-patterns/SKILL.md +51 -0
- package/src/accept-workflow-guidance.ts +71 -0
- package/src/adversarial-review.ts +120 -0
- package/src/agent-history.ts +157 -0
- package/src/agent-registry.ts +221 -0
- package/src/agent.ts +929 -0
- package/src/builtin-commands.ts +286 -0
- package/src/builtin-workflows.ts +155 -0
- package/src/code-review.ts +183 -0
- package/src/config.ts +55 -0
- package/src/deep-research.ts +135 -0
- package/src/display.ts +367 -0
- package/src/effort-command.ts +87 -0
- package/src/enums.ts +77 -0
- package/src/errors.ts +199 -0
- package/src/extension-reload.ts +100 -0
- package/src/fs-persistence.ts +124 -0
- package/src/index.ts +176 -0
- package/src/logger.ts +88 -0
- package/src/model-routing.ts +73 -0
- package/src/model-spec.ts +309 -0
- package/src/model-tier-config.ts +296 -0
- package/src/run-persistence.ts +484 -0
- package/src/saved-commands.ts +115 -0
- package/src/shared-store.ts +228 -0
- package/src/structured-output.ts +47 -0
- package/src/task-panel.ts +490 -0
- package/src/usage-limit-scheduler.ts +432 -0
- package/src/web-tools.ts +124 -0
- package/src/workflow-authoring-coverage.ts +486 -0
- package/src/workflow-authoring-reference.ts +186 -0
- package/src/workflow-capability-contract.ts +806 -0
- package/src/workflow-commands.ts +287 -0
- package/src/workflow-comprehension.ts +1673 -0
- package/src/workflow-context-measurement.ts +262 -0
- package/src/workflow-control-tool.ts +238 -0
- package/src/workflow-delivery-choice.ts +69 -0
- package/src/workflow-editor.ts +444 -0
- package/src/workflow-manager.ts +1405 -0
- package/src/workflow-paths.ts +63 -0
- package/src/workflow-release-gate.ts +529 -0
- package/src/workflow-saved.ts +180 -0
- package/src/workflow-settings.ts +194 -0
- package/src/workflow-tool.ts +464 -0
- package/src/workflow-ui.ts +1789 -0
- package/src/workflow.ts +1615 -0
- package/src/workflows-models-command.ts +211 -0
- package/src/worktree.ts +76 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-resume for runs paused on a provider usage limit.
|
|
3
|
+
*
|
|
4
|
+
* A workflow run pauses (does not fail) when a provider quota/usage limit is hit
|
|
5
|
+
* (see errors.ts PROVIDER_USAGE_LIMIT, workflow-manager.ts executeRun()'s catch
|
|
6
|
+
* block). Left alone, the run just sits there until a human runs /workflows and
|
|
7
|
+
* hits resume. This module watches the manager's public event stream and, for
|
|
8
|
+
* runs that are auto-resume-eligible, arms a timer to call manager.resume() once
|
|
9
|
+
* the provider's quota is likely to have refilled — with exponential backoff if
|
|
10
|
+
* it keeps hitting the wall, and a hard attempt cap so it never retries forever.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately standalone: it consumes ONLY WorkflowManager's public surface
|
|
13
|
+
* (on/off, listAllRuns, resume, getPersistence) so it stays decoupled from
|
|
14
|
+
* manager/persistence internals. It owns its own timers and its own bookkeeping
|
|
15
|
+
* (in-memory, best-effort persisted) — it does not rely on manager.stop(), which
|
|
16
|
+
* only operates on in-memory runs.
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_MAX_ATTEMPTS = 5;
|
|
19
|
+
const DEFAULT_MIN_DELAY_MS = 60_000;
|
|
20
|
+
const DEFAULT_FALLBACK_DELAY_MS = 300_000;
|
|
21
|
+
const DEFAULT_MAX_DELAY_MS = 6 * 60 * 60 * 1000;
|
|
22
|
+
/**
|
|
23
|
+
* Best-effort parse of a provider's human reset hint ("Resets in ~3h",
|
|
24
|
+
* "resets in 5m", "in 90s", "1h30m") into milliseconds. Sums every
|
|
25
|
+
* (number, unit) pair found, so combined forms like "1h30m" work for free.
|
|
26
|
+
* Returns undefined when nothing recognizable is found — callers should fall
|
|
27
|
+
* back to a fixed delay rather than guess.
|
|
28
|
+
*/
|
|
29
|
+
export function parseResetHintMs(hint) {
|
|
30
|
+
if (!hint)
|
|
31
|
+
return undefined;
|
|
32
|
+
// No trailing \b: combined forms like "1h30m" have a digit right after the
|
|
33
|
+
// unit letter, which is itself a word character, so \b would never match
|
|
34
|
+
// there. A negative lookahead for another letter is the correct boundary —
|
|
35
|
+
// it still stops "hours" from partially matching as bare "h" mid-word while
|
|
36
|
+
// allowing a unit to be followed immediately by the next (digit, unit) pair.
|
|
37
|
+
const re = /(\d+(?:\.\d+)?)\s*(hours?|hrs?|h|minutes?|mins?|m|seconds?|secs?|s)(?![a-z])/gi;
|
|
38
|
+
let match;
|
|
39
|
+
let totalMs = 0;
|
|
40
|
+
let found = false;
|
|
41
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex-exec loop
|
|
42
|
+
while ((match = re.exec(hint)) !== null) {
|
|
43
|
+
const value = Number.parseFloat(match[1]);
|
|
44
|
+
if (!Number.isFinite(value))
|
|
45
|
+
continue;
|
|
46
|
+
const unit = match[2].toLowerCase();
|
|
47
|
+
found = true;
|
|
48
|
+
if (unit.startsWith("h"))
|
|
49
|
+
totalMs += value * 3_600_000;
|
|
50
|
+
else if (unit.startsWith("m"))
|
|
51
|
+
totalMs += value * 60_000;
|
|
52
|
+
else if (unit.startsWith("s"))
|
|
53
|
+
totalMs += value * 1_000;
|
|
54
|
+
}
|
|
55
|
+
return found ? totalMs : undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* delay = clamp(minDelayMs, remaining * 2^(attempts-1), maxDelayMs), where
|
|
59
|
+
* remaining = parsed(resetHint) ?? fallbackDelayMs, minus time already elapsed.
|
|
60
|
+
* The exponent is capped defensively so a pathological attempt count can't
|
|
61
|
+
* overflow the multiplication to Infinity/NaN before the maxDelayMs clamp runs.
|
|
62
|
+
*/
|
|
63
|
+
export function computeAutoResumeDelayMs(params) {
|
|
64
|
+
const base = parseResetHintMs(params.resetHint) ?? params.fallbackDelayMs;
|
|
65
|
+
const remaining = base - params.elapsedMs;
|
|
66
|
+
const exponent = Math.min(Math.max(params.attempts - 1, 0), 30);
|
|
67
|
+
const backoff = remaining * 2 ** exponent;
|
|
68
|
+
return Math.min(params.maxDelayMs, Math.max(params.minDelayMs, backoff));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Watches a WorkflowManager for usage-limit pauses and auto-resumes eligible
|
|
72
|
+
* runs once the provider's quota is likely to have refilled.
|
|
73
|
+
*
|
|
74
|
+
* Event-driven "fire and watch": an attempt is consumed when a run ENTERS a
|
|
75
|
+
* usage_limit pause (live via the "paused" event, or once at cold start for a
|
|
76
|
+
* run that was already paused), never when a resume is merely fired. When an
|
|
77
|
+
* armed timer fires, resume() is called; if it returns false (lease busy, run
|
|
78
|
+
* already gone, etc.) no attempt is consumed and a short un-backed-off retry is
|
|
79
|
+
* armed instead, unless the run has reached a terminal state on disk. If resume()
|
|
80
|
+
* returns true, this scheduler steps back — the existing "paused" subscription
|
|
81
|
+
* re-arms with backoff if the run hits the wall again, and "complete"/"error"/
|
|
82
|
+
* "stopped" clean up its timer.
|
|
83
|
+
*/
|
|
84
|
+
export class UsageLimitScheduler {
|
|
85
|
+
manager;
|
|
86
|
+
now;
|
|
87
|
+
setTimer;
|
|
88
|
+
clearTimer;
|
|
89
|
+
maxAttempts;
|
|
90
|
+
minDelayMs;
|
|
91
|
+
fallbackDelayMs;
|
|
92
|
+
maxDelayMs;
|
|
93
|
+
diagnostic;
|
|
94
|
+
state = new Map();
|
|
95
|
+
disposed = false;
|
|
96
|
+
/**
|
|
97
|
+
* Runs this scheduler is currently auto-resuming (its own timer fired). Used to
|
|
98
|
+
* tell an auto-resume's "resumed" event apart from a manual one: an auto-resume
|
|
99
|
+
* must keep the backoff counter (it IS the backoff), a manual resume resets it.
|
|
100
|
+
*/
|
|
101
|
+
autoResumingRunIds = new Set();
|
|
102
|
+
onPaused = (event) => {
|
|
103
|
+
this.safe(() => this.handlePaused(event));
|
|
104
|
+
};
|
|
105
|
+
onTerminal = (event) => {
|
|
106
|
+
this.safe(() => this.cleanup(event?.runId));
|
|
107
|
+
};
|
|
108
|
+
onResumed = (event) => {
|
|
109
|
+
this.safe(() => this.handleResumed(event));
|
|
110
|
+
};
|
|
111
|
+
constructor(manager, options = {}) {
|
|
112
|
+
this.manager = manager;
|
|
113
|
+
this.now = options.now ?? Date.now;
|
|
114
|
+
this.setTimer = options.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
115
|
+
this.clearTimer = options.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
116
|
+
this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
117
|
+
this.minDelayMs = options.minDelayMs ?? DEFAULT_MIN_DELAY_MS;
|
|
118
|
+
this.fallbackDelayMs = options.fallbackDelayMs ?? DEFAULT_FALLBACK_DELAY_MS;
|
|
119
|
+
this.maxDelayMs = options.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
120
|
+
this.diagnostic =
|
|
121
|
+
options.onDiagnostic ??
|
|
122
|
+
((message, detail) => {
|
|
123
|
+
console.warn(message, detail ?? "");
|
|
124
|
+
});
|
|
125
|
+
this.manager.on("paused", this.onPaused);
|
|
126
|
+
this.manager.on("resumed", this.onResumed);
|
|
127
|
+
this.manager.on("complete", this.onTerminal);
|
|
128
|
+
this.manager.on("error", this.onTerminal);
|
|
129
|
+
this.manager.on("stopped", this.onTerminal);
|
|
130
|
+
// Cold-start re-arm: pick up any run that was already paused-on-usage_limit
|
|
131
|
+
// before this process (and thus this scheduler instance) existed.
|
|
132
|
+
this.safe(() => this.coldStartRearm());
|
|
133
|
+
}
|
|
134
|
+
/** Clear every armed timer and unsubscribe from the manager. Idempotent. */
|
|
135
|
+
dispose() {
|
|
136
|
+
if (this.disposed)
|
|
137
|
+
return;
|
|
138
|
+
this.disposed = true;
|
|
139
|
+
this.manager.off("paused", this.onPaused);
|
|
140
|
+
this.manager.off("resumed", this.onResumed);
|
|
141
|
+
this.manager.off("complete", this.onTerminal);
|
|
142
|
+
this.manager.off("error", this.onTerminal);
|
|
143
|
+
this.manager.off("stopped", this.onTerminal);
|
|
144
|
+
for (const entry of this.state.values()) {
|
|
145
|
+
if (entry.timer !== undefined)
|
|
146
|
+
this.clearTimer(entry.timer);
|
|
147
|
+
}
|
|
148
|
+
this.state.clear();
|
|
149
|
+
}
|
|
150
|
+
/** Test/diagnostic helper: in-memory attempt count tracked for a run, if any. */
|
|
151
|
+
getAttemptCount(runId) {
|
|
152
|
+
return this.state.get(runId)?.attempts;
|
|
153
|
+
}
|
|
154
|
+
/** Test/diagnostic helper: whether a resume timer is currently armed for a run. */
|
|
155
|
+
hasArmedTimer(runId) {
|
|
156
|
+
return this.state.get(runId)?.timer !== undefined;
|
|
157
|
+
}
|
|
158
|
+
// ---- event handlers -----------------------------------------------------
|
|
159
|
+
handlePaused(event) {
|
|
160
|
+
if (this.disposed || !event?.runId || event.reason !== "usage_limit")
|
|
161
|
+
return;
|
|
162
|
+
const runId = event.runId;
|
|
163
|
+
// The "paused" event fires BEFORE the manager's own persistRun() write for
|
|
164
|
+
// this pause (see executeRun()'s catch block: emit then persist). A disk
|
|
165
|
+
// read here can therefore be stale for fields this exact pause is about to
|
|
166
|
+
// set (status/pauseReason/resetHint) — but NOT for `autoResume`, which is
|
|
167
|
+
// fixed at run-start and persisted on every persistRun() call since, so a
|
|
168
|
+
// stale read of it is still correct. resetHint comes off the event itself,
|
|
169
|
+
// not disk, to avoid that race.
|
|
170
|
+
const persisted = this.safeLoad(runId);
|
|
171
|
+
if (persisted?.autoResume === false) {
|
|
172
|
+
this.diagnostic(`[usage-limit-scheduler] ${runId}: autoResume is disabled for this run, not arming`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const priorAttempts = this.state.get(runId)?.attempts ?? persisted?.autoResumeAttempts ?? 0;
|
|
176
|
+
this.arm(runId, {
|
|
177
|
+
attempts: priorAttempts + 1,
|
|
178
|
+
resetHint: event.resetHint ?? persisted?.resetHint,
|
|
179
|
+
elapsedMs: 0,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
cleanup(runId) {
|
|
183
|
+
if (!runId)
|
|
184
|
+
return;
|
|
185
|
+
const entry = this.state.get(runId);
|
|
186
|
+
if (entry?.timer !== undefined)
|
|
187
|
+
this.clearTimer(entry.timer);
|
|
188
|
+
this.state.delete(runId);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* A run was resumed. If WE resumed it (auto-resume timer fired), leave the
|
|
192
|
+
* backoff counter alone — that's the sequence doing its job, and it must still
|
|
193
|
+
* be able to reach the cap. If a human resumed it (via /workflows), treat that
|
|
194
|
+
* as a deliberate fresh start: drop the in-memory given-up state and reset the
|
|
195
|
+
* persisted counter so a later pause re-enters the normal backoff from attempt 1
|
|
196
|
+
* instead of staying silently given-up forever.
|
|
197
|
+
*/
|
|
198
|
+
handleResumed(event) {
|
|
199
|
+
if (this.disposed || !event?.runId)
|
|
200
|
+
return;
|
|
201
|
+
if (this.autoResumingRunIds.has(event.runId))
|
|
202
|
+
return;
|
|
203
|
+
this.cleanup(event.runId);
|
|
204
|
+
this.persistAttempts(event.runId, 0);
|
|
205
|
+
}
|
|
206
|
+
coldStartRearm() {
|
|
207
|
+
const runs = this.manager.listAllRuns();
|
|
208
|
+
for (const run of runs) {
|
|
209
|
+
if (run.status !== "paused" || run.pauseReason !== "usage_limit")
|
|
210
|
+
continue;
|
|
211
|
+
if (run.autoResume === false)
|
|
212
|
+
continue;
|
|
213
|
+
if (this.state.has(run.runId))
|
|
214
|
+
continue;
|
|
215
|
+
const priorAttempts = run.autoResumeAttempts ?? 0;
|
|
216
|
+
const updatedAtMs = Date.parse(run.updatedAt);
|
|
217
|
+
const elapsedMs = Number.isFinite(updatedAtMs) ? Math.max(0, this.now() - updatedAtMs) : 0;
|
|
218
|
+
this.arm(run.runId, {
|
|
219
|
+
attempts: priorAttempts + 1,
|
|
220
|
+
resetHint: run.resetHint,
|
|
221
|
+
elapsedMs,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ---- arming / firing ------------------------------------------------------
|
|
226
|
+
arm(runId, params) {
|
|
227
|
+
const existing = this.state.get(runId);
|
|
228
|
+
if (existing?.timer !== undefined)
|
|
229
|
+
this.clearTimer(existing.timer);
|
|
230
|
+
if (params.attempts > this.maxAttempts) {
|
|
231
|
+
const alreadyLogged = existing?.gaveUp === true;
|
|
232
|
+
// Freeze the counter at a single sentinel (maxAttempts + 1) instead of
|
|
233
|
+
// storing the raw overflow. coldStartRearm() reads the persisted count and
|
|
234
|
+
// adds 1 on every restart; without this clamp a given-up run's counter
|
|
235
|
+
// grew without bound (…6, 7, 8… → "giving up after 23") across cold starts
|
|
236
|
+
// (#106). Clamping makes the persisted value idempotent — a rearm of an
|
|
237
|
+
// already-given-up run rewrites the same 6.
|
|
238
|
+
const frozen = this.maxAttempts + 1;
|
|
239
|
+
this.state.set(runId, { attempts: frozen, gaveUp: true });
|
|
240
|
+
this.persistAttempts(runId, frozen);
|
|
241
|
+
// Log the give-up exactly once per crossing. In-process the gaveUp flag
|
|
242
|
+
// guards it; across restarts a fresh scheduler has no memory, so also
|
|
243
|
+
// suppress when this arm is merely re-giving-up an already-capped run
|
|
244
|
+
// (params.attempts already past the sentinel, i.e. prior was ≥ frozen).
|
|
245
|
+
if (!alreadyLogged && params.attempts <= frozen) {
|
|
246
|
+
this.diagnostic(`[usage-limit-scheduler] ${runId}: giving up after ${this.maxAttempts} auto-resume attempt(s) ` +
|
|
247
|
+
`(max ${this.maxAttempts}); leaving paused for manual resume`);
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const delay = computeAutoResumeDelayMs({
|
|
252
|
+
resetHint: params.resetHint,
|
|
253
|
+
attempts: params.attempts,
|
|
254
|
+
elapsedMs: params.elapsedMs,
|
|
255
|
+
minDelayMs: this.minDelayMs,
|
|
256
|
+
fallbackDelayMs: this.fallbackDelayMs,
|
|
257
|
+
maxDelayMs: this.maxDelayMs,
|
|
258
|
+
});
|
|
259
|
+
const timer = this.setTimer(() => this.safe(() => this.onTimerFire(runId)), delay);
|
|
260
|
+
this.state.set(runId, { attempts: params.attempts, timer });
|
|
261
|
+
this.persistAttempts(runId, params.attempts);
|
|
262
|
+
}
|
|
263
|
+
async onTimerFire(runId) {
|
|
264
|
+
if (this.disposed)
|
|
265
|
+
return;
|
|
266
|
+
const entry = this.state.get(runId);
|
|
267
|
+
if (!entry || entry.gaveUp)
|
|
268
|
+
return;
|
|
269
|
+
// The timer that just fired is spent; clear its handle while we await.
|
|
270
|
+
this.state.set(runId, { ...entry, timer: undefined });
|
|
271
|
+
let resumed = false;
|
|
272
|
+
// Mark this as OUR resume so handleResumed() (fired synchronously inside
|
|
273
|
+
// resume(), before it returns) doesn't mistake it for a manual resume and
|
|
274
|
+
// reset the backoff counter mid-sequence.
|
|
275
|
+
this.autoResumingRunIds.add(runId);
|
|
276
|
+
try {
|
|
277
|
+
resumed = await this.manager.resume(runId);
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
this.diagnostic(`[usage-limit-scheduler] ${runId}: resume() threw`, err);
|
|
281
|
+
resumed = false;
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
this.autoResumingRunIds.delete(runId);
|
|
285
|
+
}
|
|
286
|
+
if (this.disposed)
|
|
287
|
+
return;
|
|
288
|
+
if (resumed) {
|
|
289
|
+
// Don't consume/advance anything further here — the existing "paused"
|
|
290
|
+
// subscription re-arms (with backoff) if this run hits the wall again,
|
|
291
|
+
// and "complete"/"error"/"stopped" clean up on any terminal outcome.
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
// resume() returned false without throwing: it refused for a structural
|
|
295
|
+
// reason (already running/aborted, no persisted script, or the lease is
|
|
296
|
+
// held elsewhere) rather than a real failed attempt. Per the fix for bug
|
|
297
|
+
// (a), that must NOT consume an attempt. Distinguish "gone for good" from
|
|
298
|
+
// "try again shortly":
|
|
299
|
+
const status = this.safeStatus(runId);
|
|
300
|
+
if (status === undefined || status === "completed" || status === "aborted") {
|
|
301
|
+
this.cleanup(runId);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const current = this.state.get(runId) ?? entry;
|
|
305
|
+
const timer = this.setTimer(() => this.safe(() => this.onTimerFire(runId)), this.minDelayMs);
|
|
306
|
+
this.state.set(runId, { attempts: current.attempts, timer });
|
|
307
|
+
}
|
|
308
|
+
// ---- helpers --------------------------------------------------------------
|
|
309
|
+
safeLoad(runId) {
|
|
310
|
+
try {
|
|
311
|
+
return this.manager.getPersistence().load(runId) ?? undefined;
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
this.diagnostic(`[usage-limit-scheduler] ${runId}: persistence load failed`, err);
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
safeStatus(runId) {
|
|
319
|
+
try {
|
|
320
|
+
return this.manager.listAllRuns().find((r) => r.runId === runId)?.status;
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
this.diagnostic(`[usage-limit-scheduler] ${runId}: listAllRuns() failed`, err);
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Best-effort persist of the in-memory attempt counter, so a cold start after
|
|
329
|
+
* a crash can approximately resume the backoff sequence instead of restarting
|
|
330
|
+
* it. Deferred to a microtask so it lands AFTER the manager's own persistRun()
|
|
331
|
+
* write for this same pause (which happens synchronously, right after the
|
|
332
|
+
* "paused" event we're reacting to returns control to executeRun()) — writing
|
|
333
|
+
* synchronously here would just get clobbered, since persistRun() writes a
|
|
334
|
+
* fresh PersistedRunState object literal that doesn't know about this field.
|
|
335
|
+
* This is still inherently racy across process crashes (see class docs); it
|
|
336
|
+
* is a best-effort durability aid, not a correctness requirement for the live
|
|
337
|
+
* (in-memory) path.
|
|
338
|
+
*/
|
|
339
|
+
persistAttempts(runId, attempts) {
|
|
340
|
+
queueMicrotask(() => {
|
|
341
|
+
if (this.disposed)
|
|
342
|
+
return;
|
|
343
|
+
try {
|
|
344
|
+
const persistence = this.manager.getPersistence();
|
|
345
|
+
const current = persistence.load(runId);
|
|
346
|
+
if (!current)
|
|
347
|
+
return;
|
|
348
|
+
persistence.save({ ...current, autoResumeAttempts: attempts });
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
this.diagnostic(`[usage-limit-scheduler] ${runId}: failed to persist autoResumeAttempts`, err);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
safe(fn) {
|
|
356
|
+
try {
|
|
357
|
+
const result = fn();
|
|
358
|
+
if (result && typeof result.catch === "function") {
|
|
359
|
+
result.catch((err) => {
|
|
360
|
+
this.diagnostic("[usage-limit-scheduler] async handler error", err);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
this.diagnostic("[usage-limit-scheduler] handler error", err);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real web tools for research workflows. These execute in the extension host
|
|
3
|
+
* process (which has network access), not in a subagent sandbox, so they perform
|
|
4
|
+
* genuine HTTP requests via Node's fetch.
|
|
5
|
+
*
|
|
6
|
+
* - web_search: best-effort Bing HTML scrape -> result {url, title}
|
|
7
|
+
* - web_fetch: fetch a URL and return readable text (HTML stripped, truncated)
|
|
8
|
+
*/
|
|
9
|
+
import { type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
export declare function htmlToText(html: string): string;
|
|
11
|
+
export declare function parseBingResults(html: string, limit: number): Array<{
|
|
12
|
+
url: string;
|
|
13
|
+
title: string;
|
|
14
|
+
}>;
|
|
15
|
+
/** A tool that searches the web (best-effort) and returns result URLs + titles. */
|
|
16
|
+
export declare function createWebSearchTool(): ToolDefinition;
|
|
17
|
+
/** A tool that fetches a URL and returns readable text. */
|
|
18
|
+
export declare function createWebFetchTool(maxChars?: number): ToolDefinition;
|
|
19
|
+
/** Both web tools, for injecting into a research workflow's agents. */
|
|
20
|
+
export declare function createWebTools(): ToolDefinition[];
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real web tools for research workflows. These execute in the extension host
|
|
3
|
+
* process (which has network access), not in a subagent sandbox, so they perform
|
|
4
|
+
* genuine HTTP requests via Node's fetch.
|
|
5
|
+
*
|
|
6
|
+
* - web_search: best-effort Bing HTML scrape -> result {url, title}
|
|
7
|
+
* - web_fetch: fetch a URL and return readable text (HTML stripped, truncated)
|
|
8
|
+
*/
|
|
9
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { Type } from "typebox";
|
|
11
|
+
const UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36";
|
|
12
|
+
async function fetchText(url, timeoutMs = 15000) {
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(url, { headers: { "user-agent": UA }, signal: controller.signal, redirect: "follow" });
|
|
17
|
+
return { status: res.status, body: await res.text() };
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function htmlToText(html) {
|
|
24
|
+
return html
|
|
25
|
+
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
26
|
+
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
27
|
+
.replace(/<\/(p|div|li|h[1-6]|tr|br)>/gi, "\n")
|
|
28
|
+
.replace(/<[^>]+>/g, " ")
|
|
29
|
+
.replace(/ /g, " ")
|
|
30
|
+
.replace(/&/g, "&")
|
|
31
|
+
.replace(/</g, "<")
|
|
32
|
+
.replace(/>/g, ">")
|
|
33
|
+
.replace(/'|'/g, "'")
|
|
34
|
+
.replace(/"/g, '"')
|
|
35
|
+
.replace(/[ \t]+/g, " ")
|
|
36
|
+
.replace(/\n +/g, "\n")
|
|
37
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
38
|
+
.trim();
|
|
39
|
+
}
|
|
40
|
+
export function parseBingResults(html, limit) {
|
|
41
|
+
const out = [];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
for (const m of html.matchAll(/<h2[^>]*>\s*<a[^>]+href="(https?:\/\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/g)) {
|
|
44
|
+
const url = m[1];
|
|
45
|
+
if (/\.bing\.com|go\.microsoft\.com/.test(url) || seen.has(url))
|
|
46
|
+
continue;
|
|
47
|
+
seen.add(url);
|
|
48
|
+
out.push({ url, title: m[2].replace(/<[^>]+>/g, "").trim() });
|
|
49
|
+
if (out.length >= limit)
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/** A tool that searches the web (best-effort) and returns result URLs + titles. */
|
|
55
|
+
export function createWebSearchTool() {
|
|
56
|
+
return defineTool({
|
|
57
|
+
name: "web_search",
|
|
58
|
+
label: "Web Search",
|
|
59
|
+
description: "Search the web and return a list of result URLs and titles. Use before web_fetch to find sources.",
|
|
60
|
+
promptSnippet: "Search the web for sources",
|
|
61
|
+
parameters: Type.Object({
|
|
62
|
+
query: Type.String({ description: "The search query." }),
|
|
63
|
+
count: Type.Optional(Type.Number({ description: "Max results (default 6)." })),
|
|
64
|
+
}),
|
|
65
|
+
async execute(_id, params) {
|
|
66
|
+
const limit = Math.min(Math.max(params.count ?? 6, 1), 10);
|
|
67
|
+
try {
|
|
68
|
+
const { status, body } = await fetchText(`https://www.bing.com/search?q=${encodeURIComponent(params.query)}`);
|
|
69
|
+
const results = parseBingResults(body, limit);
|
|
70
|
+
const text = results.length
|
|
71
|
+
? results.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}`).join("\n")
|
|
72
|
+
: `No results parsed (HTTP ${status}). Try a different query or fetch a known URL directly.`;
|
|
73
|
+
return { content: [{ type: "text", text }], details: { results } };
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: "text", text: `web_search failed: ${error instanceof Error ? error.message : error}` }],
|
|
78
|
+
details: { results: [] },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/** A tool that fetches a URL and returns readable text. */
|
|
85
|
+
export function createWebFetchTool(maxChars = 6000) {
|
|
86
|
+
return defineTool({
|
|
87
|
+
name: "web_fetch",
|
|
88
|
+
label: "Web Fetch",
|
|
89
|
+
description: "Fetch a URL and return its readable text content (HTML stripped, truncated).",
|
|
90
|
+
promptSnippet: "Fetch a URL's text",
|
|
91
|
+
parameters: Type.Object({
|
|
92
|
+
url: Type.String({ description: "The absolute URL to fetch." }),
|
|
93
|
+
}),
|
|
94
|
+
async execute(_id, params) {
|
|
95
|
+
try {
|
|
96
|
+
const { status, body } = await fetchText(params.url);
|
|
97
|
+
const text = htmlToText(body).slice(0, maxChars);
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: "text", text: `HTTP ${status} ${params.url}\n\n${text}` }],
|
|
100
|
+
details: { status, url: params.url },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: "text",
|
|
108
|
+
text: `web_fetch failed for ${params.url}: ${error instanceof Error ? error.message : error}`,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
details: { status: 0, url: params.url },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** Both web tools, for injecting into a research workflow's agents. */
|
|
118
|
+
export function createWebTools() {
|
|
119
|
+
return [createWebSearchTool(), createWebFetchTool()];
|
|
120
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { WorkflowAuthoringProtection } from "./enums.js";
|
|
2
|
+
/** Exact installed guidance location retained for an authoring surface without model evidence. */
|
|
3
|
+
export interface ProtectedGuidanceSurface {
|
|
4
|
+
path: string;
|
|
5
|
+
anchor?: string;
|
|
6
|
+
requiredText?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Evidence and optimization policy for one stable workflow authoring surface. */
|
|
9
|
+
export interface WorkflowAuthoringCoverageEntry {
|
|
10
|
+
id: string;
|
|
11
|
+
kind: string;
|
|
12
|
+
reference: {
|
|
13
|
+
path: string;
|
|
14
|
+
anchor?: string;
|
|
15
|
+
};
|
|
16
|
+
example?: string;
|
|
17
|
+
behaviorEvidence: readonly string[];
|
|
18
|
+
comprehensionScenarios: readonly string[];
|
|
19
|
+
protection: WorkflowAuthoringProtection;
|
|
20
|
+
protectedGuidance: readonly ProtectedGuidanceSurface[];
|
|
21
|
+
}
|
|
22
|
+
/** Scenario identifiers that release checks may accept as provider-backed evidence. */
|
|
23
|
+
export declare const WORKFLOW_COMPREHENSION_SCENARIO_IDS: string[];
|
|
24
|
+
/** Mixed guidance files that require explicit acceptance while behavioral coverage remains partial. */
|
|
25
|
+
export declare const WORKFLOW_AUTHORING_FROZEN_FILES: readonly [{
|
|
26
|
+
readonly path: "skills/workflow-authoring/SKILL.md";
|
|
27
|
+
readonly sha256: "d2843c4d928b6f622271c65bf4cf5d54cb219efca0ce92d8c5f4d8204365bf91";
|
|
28
|
+
}, {
|
|
29
|
+
readonly path: "skills/workflow-authoring/references/runtime.md";
|
|
30
|
+
readonly sha256: "14f1c4496c523d2e37316a7c96041a22630a65d342d08fdd77aeca2d325e22a3";
|
|
31
|
+
}, {
|
|
32
|
+
readonly path: "skills/workflow-authoring/references/helpers.md";
|
|
33
|
+
readonly sha256: "1c8d253649f00412511f17ffc08c6156797b99de72ae037e14f2ea92ac33a11e";
|
|
34
|
+
}, {
|
|
35
|
+
readonly path: "skills/workflow-authoring/references/specialized-helpers.md";
|
|
36
|
+
readonly sha256: "7597c94bbacea885697fb2d05a96ed9ec39403ca6d3a94547bf8ce5e233b2c76";
|
|
37
|
+
}, {
|
|
38
|
+
readonly path: "skills/workflow-authoring/references/lifecycle.md";
|
|
39
|
+
readonly sha256: "e2f187a7b633beef0ca257e6782f72a140fc06b37a58aac423432d36d1dc19ee";
|
|
40
|
+
}, {
|
|
41
|
+
readonly path: "skills/workflow-authoring/references/pattern-selection.md";
|
|
42
|
+
readonly sha256: "923988a1b4d506a7b330bf5e4b8ab47cf8456edcfe6674b5d8d8848264633c3d";
|
|
43
|
+
}, {
|
|
44
|
+
readonly path: "skills/workflow-authoring/references/focused-recipes.md";
|
|
45
|
+
readonly sha256: "30906054232f67029e31f71b3b093f9949f6de9116e4381f433009c401f2c5c7";
|
|
46
|
+
}, {
|
|
47
|
+
readonly path: "skills/workflow-authoring/references/registry-ownership.md";
|
|
48
|
+
readonly sha256: "daf324448be16732c6796fd6359d1b1b842fa550ed616a6154f556d6ec1ef0b9";
|
|
49
|
+
}, {
|
|
50
|
+
readonly path: "skills/workflow-authoring/references/review.md";
|
|
51
|
+
readonly sha256: "2bd97acb87a8f6e9514892cdf5c431305b3d8952ba9761c1c203c217b08c9e7d";
|
|
52
|
+
}, {
|
|
53
|
+
readonly path: "skills/workflow-authoring/references/debugging.md";
|
|
54
|
+
readonly sha256: "2938e635f5856f2934e42c9cc3b7035a66d53176f4ab2beadfeabb9abf42e6cc";
|
|
55
|
+
}, {
|
|
56
|
+
readonly path: "skills/workflow-authoring/examples/classify-and-act.js";
|
|
57
|
+
readonly sha256: "23d0d9f37ee8648cd29ca526b0b23cf55bd3ac57efd02e1b93e227bcd0c18603";
|
|
58
|
+
}, {
|
|
59
|
+
readonly path: "skills/workflow-authoring/examples/tournament.js";
|
|
60
|
+
readonly sha256: "3a90bd3055c5e38e13fd8d7447173fc2e6a141fbc33b9bcc8a84723b7ab9d2e6";
|
|
61
|
+
}, {
|
|
62
|
+
readonly path: "skills/workflow-authoring/examples/validated-gate.js";
|
|
63
|
+
readonly sha256: "1cb4b3941ae61ebd1e12ada899f7d04678fe858a307c7fabc408603a4b9ba889";
|
|
64
|
+
}];
|
|
65
|
+
/** Stable orchestration-pattern identifiers covered by the authoring inventory. */
|
|
66
|
+
export declare const WORKFLOW_AUTHORING_PATTERN_IDS: readonly ["workflow.pattern.classify-and-act", "workflow.pattern.fan-out-and-synthesize", "workflow.pattern.adversarial-verification", "workflow.pattern.generate-and-filter", "workflow.pattern.tournament", "workflow.pattern.loop-until-done"];
|
|
67
|
+
/** Stable focused-recipe identifiers covered by the authoring inventory. */
|
|
68
|
+
export declare const WORKFLOW_AUTHORING_RECIPE_IDS: readonly ["workflow.recipe.phased-budgets", "workflow.recipe.saved-nested-workflows", "workflow.recipe.bounded-semantic-retry", "workflow.recipe.validator-feedback", "workflow.recipe.structured-output"];
|
|
69
|
+
/** Complete release-gated inventory of behavioral coverage and frozen authoring guidance. */
|
|
70
|
+
export declare const WORKFLOW_AUTHORING_COVERAGE: readonly WorkflowAuthoringCoverageEntry[];
|