pi-workflow-engine 0.13.0 → 1.1.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/.pi/extensions/pi-workflow-engine/index.ts +152 -416
- package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +17 -14
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +7 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +48 -62
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +46 -67
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +14 -37
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +12 -13
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +15 -21
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +13 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +17 -5
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +131 -117
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +4 -18
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +3 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +33 -86
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/classify.ts +107 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +25 -80
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +16 -23
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +58 -117
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +41 -80
- package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +2 -10
- package/.pi/extensions/pi-workflow-engine/src/git.ts +55 -0
- package/.pi/extensions/pi-workflow-engine/src/guards.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/host-tools.ts +76 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +7 -19
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +171 -106
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +0 -12
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +37 -8
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +15 -66
- package/.pi/extensions/pi-workflow-engine/src/model-ref.ts +19 -0
- package/.pi/extensions/pi-workflow-engine/src/options.ts +97 -16
- package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +2 -2
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +14 -4
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +79 -80
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +28 -20
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +2 -7
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +0 -1
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +29 -31
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +75 -139
- package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +14 -31
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +3 -4
- package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-diff-lines.ts +54 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +7 -9
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -117
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +8 -13
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +3 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +158 -241
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +28 -38
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +35 -44
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +13 -14
- package/.pi/extensions/pi-workflow-engine/src/text.ts +29 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +17 -4
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +16 -28
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +34 -14
- package/.pi/extensions/pi-workflow-engine/src/types.ts +30 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +193 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/research-result-renderer.ts +177 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/result-layout.ts +209 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +17 -44
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +703 -235
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-picker.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +239 -129
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +461 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +91 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +316 -98
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +41 -34
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +162 -22
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +19 -24
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +17 -37
- package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +79 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +4 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +77 -63
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +42 -29
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +15 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +27 -31
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +3 -6
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +48 -110
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +64 -93
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +25 -67
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +19 -53
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +21 -49
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
- package/README.md +1 -1
- package/USAGE.md +79 -20
- package/package.json +1 -2
- package/skills/workflow-code-review-actions/SKILL.md +12 -23
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +0 -81
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -24
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
- package/.pi/settings.json +0 -3
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { throwIfAborted } from "../cancellation.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { isGitObjectId, runGit } from "../git.ts";
|
|
4
4
|
import {
|
|
5
|
+
GIT_DIFF_MACHINE_FORMAT,
|
|
6
|
+
reviewDiffCommand,
|
|
5
7
|
reviewGitDiffBaseline,
|
|
6
8
|
type GitReviewDiffTarget,
|
|
7
9
|
type PullRequestReviewDiffTarget,
|
|
8
10
|
type ReviewDiffTarget,
|
|
9
11
|
} from "../review-diff-target.ts";
|
|
10
|
-
import {
|
|
12
|
+
import type { BoundedProcessResult } from "../process-runner.ts";
|
|
11
13
|
import { unknownErrorMessage } from "../unknown-error.ts";
|
|
12
14
|
import type { WorktreeBaseline } from "../worktree.ts";
|
|
13
15
|
import type { ReviewContext, ReviewSnapshotIdentity } from "./review-report.ts";
|
|
@@ -39,7 +41,6 @@ export interface CapturedReviewMaterial {
|
|
|
39
41
|
export interface ReviewMaterialCaptureFailure {
|
|
40
42
|
readonly ok: false;
|
|
41
43
|
readonly error: string;
|
|
42
|
-
readonly failure: DiffCaptureFailure;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export type ReviewMaterialCaptureResult = CapturedReviewMaterial | ReviewMaterialCaptureFailure;
|
|
@@ -64,36 +65,34 @@ export async function captureReviewMaterial(
|
|
|
64
65
|
if (!before.ok) {
|
|
65
66
|
throwIfAborted(signal);
|
|
66
67
|
return latestDiff === undefined
|
|
67
|
-
? { ok: false, error: before.error
|
|
68
|
+
? { ok: false, error: before.error }
|
|
68
69
|
: capturedWithoutSnapshot(latestDiff, `review diff could not be recaptured: ${before.error}`);
|
|
69
70
|
}
|
|
70
|
-
latestDiff = before.
|
|
71
|
+
latestDiff = before.stdout;
|
|
71
72
|
|
|
72
73
|
let baseline: WorktreeBaseline;
|
|
73
74
|
try {
|
|
74
75
|
baseline = await captureReviewWorktreeBaseline(target, cwd, signal);
|
|
75
76
|
} catch (error) {
|
|
76
77
|
throwIfAborted(signal);
|
|
77
|
-
return capturedWithoutSnapshot(before.
|
|
78
|
+
return capturedWithoutSnapshot(before.stdout, `review baseline could not be captured: ${unknownErrorMessage(error)}`);
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
const after = await captureReviewDiff(target, cwd, signal);
|
|
81
82
|
if (!after.ok) {
|
|
82
83
|
throwIfAborted(signal);
|
|
83
|
-
return capturedWithoutSnapshot(before.
|
|
84
|
+
return capturedWithoutSnapshot(before.stdout, `review diff could not be recaptured: ${after.error}`);
|
|
84
85
|
}
|
|
85
|
-
latestDiff = after.
|
|
86
|
+
latestDiff = after.stdout;
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
const afterFingerprint = fingerprintReviewDiff(after.diff);
|
|
89
|
-
if (beforeFingerprint === afterFingerprint) {
|
|
88
|
+
if (before.stdout === after.stdout) {
|
|
90
89
|
return {
|
|
91
90
|
ok: true,
|
|
92
|
-
diff: after.
|
|
91
|
+
diff: after.stdout,
|
|
93
92
|
snapshot: {
|
|
94
93
|
status: "verified",
|
|
95
94
|
identity: {
|
|
96
|
-
diffFingerprint:
|
|
95
|
+
diffFingerprint: createHash("sha256").update(after.stdout).digest("hex"),
|
|
97
96
|
baselineFingerprint: fingerprintReviewWorktreeBaseline(baseline),
|
|
98
97
|
},
|
|
99
98
|
baseline,
|
|
@@ -138,20 +137,15 @@ export function fingerprintReviewWorktreeBaseline(baseline: WorktreeBaseline): s
|
|
|
138
137
|
.digest("hex");
|
|
139
138
|
}
|
|
140
139
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
signal: AbortSignal | undefined,
|
|
145
|
-
): Promise<{ readonly ok: true; readonly diff: string } | { readonly ok: false; readonly error: string; readonly failure: DiffCaptureFailure }> {
|
|
146
|
-
const captured = await captureDiffTarget(target, {
|
|
140
|
+
function captureReviewDiff(target: ReviewDiffTarget, cwd: string, signal: AbortSignal | undefined): Promise<BoundedProcessResult> {
|
|
141
|
+
return runGit({
|
|
142
|
+
...reviewDiffCommand(target),
|
|
147
143
|
cwd,
|
|
144
|
+
label: "diff capture",
|
|
148
145
|
signal,
|
|
149
146
|
timeoutMs: REVIEW_SNAPSHOT_TIMEOUT_MS,
|
|
150
147
|
maxBufferBytes: REVIEW_SNAPSHOT_MAX_BYTES,
|
|
151
148
|
});
|
|
152
|
-
return captured.ok
|
|
153
|
-
? { ok: true, diff: captured.stdout }
|
|
154
|
-
: { ok: false, error: captured.error, failure: captured.failure };
|
|
155
149
|
}
|
|
156
150
|
|
|
157
151
|
async function captureReviewWorktreeBaseline(
|
|
@@ -170,29 +164,31 @@ async function resolvePullRequestBaseline(
|
|
|
170
164
|
signal: AbortSignal | undefined,
|
|
171
165
|
): Promise<WorktreeBaseline> {
|
|
172
166
|
const number = String(target.number);
|
|
173
|
-
const viewed = await runReviewCommand("gh", ["pr", "view", number, "--json", "headRefOid,headRefName,headRepository"], cwd, signal);
|
|
167
|
+
const viewed = await runReviewCommand("gh", ["pr", "view", number, "--json", "headRefOid,headRefName,headRepository,url"], cwd, signal);
|
|
174
168
|
const details = viewed.ok ? parsePullRequestHead(viewed.stdout) : undefined;
|
|
175
169
|
if (!details) {
|
|
176
|
-
throw new Error(`pull request head could not be resolved: ${viewed.
|
|
170
|
+
throw new Error(`pull request head could not be resolved: ${viewed.ok ? "invalid head commit" : viewed.error}`);
|
|
177
171
|
}
|
|
178
172
|
|
|
179
173
|
if (!(await commitExists(details.head, cwd, signal))) {
|
|
180
174
|
const fetched = await runReviewCommand(
|
|
181
175
|
"git",
|
|
182
|
-
["fetch", "--no-tags", "--quiet",
|
|
176
|
+
["fetch", "--no-tags", "--quiet", `${details.origin}/${details.repository}.git`, details.branch],
|
|
183
177
|
cwd,
|
|
184
178
|
signal,
|
|
185
179
|
);
|
|
186
180
|
if (!fetched.ok || !(await commitExists(details.head, cwd, signal))) {
|
|
187
181
|
throw new Error(
|
|
188
|
-
`pull request ${number} head ${details.head} is not available locally: ${fetched.
|
|
182
|
+
`pull request ${number} head ${details.head} is not available locally: ${fetched.ok ? "fetch did not provide the commit" : fetched.error}`,
|
|
189
183
|
);
|
|
190
184
|
}
|
|
191
185
|
}
|
|
192
186
|
return { ref: details.head };
|
|
193
187
|
}
|
|
194
188
|
|
|
195
|
-
function parsePullRequestHead(
|
|
189
|
+
function parsePullRequestHead(
|
|
190
|
+
value: string,
|
|
191
|
+
): { readonly head: string; readonly branch: string; readonly repository: string; readonly origin: string } | undefined {
|
|
196
192
|
let parsed: unknown;
|
|
197
193
|
try {
|
|
198
194
|
parsed = JSON.parse(value);
|
|
@@ -204,12 +200,17 @@ function parsePullRequestHead(value: string): { readonly head: string; readonly
|
|
|
204
200
|
readonly headRefOid?: unknown;
|
|
205
201
|
readonly headRefName?: unknown;
|
|
206
202
|
readonly headRepository?: { readonly nameWithOwner?: unknown } | null;
|
|
203
|
+
readonly url?: unknown;
|
|
207
204
|
};
|
|
208
|
-
if (typeof candidate.headRefOid !== "string" ||
|
|
205
|
+
if (typeof candidate.headRefOid !== "string" || !isGitObjectId(candidate.headRefOid)) return undefined;
|
|
209
206
|
if (typeof candidate.headRefName !== "string" || candidate.headRefName.length === 0 || candidate.headRefName.includes("\0")) return undefined;
|
|
210
207
|
const repository = candidate.headRepository?.nameWithOwner;
|
|
211
208
|
if (typeof repository !== "string" || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) return undefined;
|
|
212
|
-
|
|
209
|
+
// The PR URL carries the host gh resolved the PR on, which is not github.com for GitHub Enterprise.
|
|
210
|
+
if (typeof candidate.url !== "string" || !URL.canParse(candidate.url)) return undefined;
|
|
211
|
+
const url = new URL(candidate.url);
|
|
212
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return undefined;
|
|
213
|
+
return { head: candidate.headRefOid, branch: candidate.headRefName, repository, origin: url.origin };
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
async function resolveGitDiffBaseline(
|
|
@@ -236,9 +237,7 @@ async function captureMutableGitBaseline(
|
|
|
236
237
|
const head = await tryResolveCommit("HEAD", cwd, signal);
|
|
237
238
|
if (!head) throw new Error("the reviewed repository has no committed HEAD baseline");
|
|
238
239
|
|
|
239
|
-
const args = staged
|
|
240
|
-
? ["diff", "--no-ext-diff", "--binary", "--cached", "HEAD"]
|
|
241
|
-
: ["diff", "--no-ext-diff", "--binary", "HEAD"];
|
|
240
|
+
const args = ["diff", "--no-ext-diff", ...GIT_DIFF_MACHINE_FORMAT, "--binary", ...(staged ? ["--cached"] : []), "HEAD"];
|
|
242
241
|
const snapshot = await runReviewCommand("git", args, cwd, signal, REVIEW_SNAPSHOT_MAX_BYTES);
|
|
243
242
|
if (!snapshot.ok) {
|
|
244
243
|
throw new Error(`the reviewed working state could not be captured: ${snapshot.error}`);
|
|
@@ -255,7 +254,7 @@ async function resolveCommit(ref: string, cwd: string, signal: AbortSignal | und
|
|
|
255
254
|
async function tryResolveCommit(ref: string, cwd: string, signal: AbortSignal | undefined): Promise<string | undefined> {
|
|
256
255
|
const result = await runReviewCommand("git", ["rev-parse", "--verify", `${ref}^{commit}`], cwd, signal);
|
|
257
256
|
const commit = result.stdout.trim();
|
|
258
|
-
return result.ok &&
|
|
257
|
+
return result.ok && isGitObjectId(commit) ? commit : undefined;
|
|
259
258
|
}
|
|
260
259
|
|
|
261
260
|
async function commitExists(ref: string, cwd: string, signal: AbortSignal | undefined): Promise<boolean> {
|
|
@@ -270,25 +269,17 @@ async function runReviewCommand(
|
|
|
270
269
|
signal: AbortSignal | undefined,
|
|
271
270
|
maxBufferBytes = REVIEW_COMMAND_MAX_BYTES,
|
|
272
271
|
) {
|
|
273
|
-
return await
|
|
272
|
+
return await runGit({
|
|
274
273
|
file,
|
|
275
|
-
args,
|
|
276
274
|
cwd,
|
|
277
|
-
|
|
275
|
+
args,
|
|
276
|
+
label: `${file} review snapshot command`,
|
|
278
277
|
signal,
|
|
279
278
|
timeoutMs: REVIEW_SNAPSHOT_TIMEOUT_MS,
|
|
280
279
|
maxBufferBytes,
|
|
281
|
-
abortError: `${file} review snapshot command aborted`,
|
|
282
|
-
timeoutError: `${file} review snapshot command timed out after ${REVIEW_SNAPSHOT_TIMEOUT_MS}ms`,
|
|
283
|
-
maxBufferError: `${file} review snapshot command exceeded output limit`,
|
|
284
|
-
exitError: (stderr, code, processSignal) => stderr.trim() || `${file} exited with code ${code ?? `signal ${processSignal ?? "unknown"}`}`,
|
|
285
280
|
});
|
|
286
281
|
}
|
|
287
282
|
|
|
288
283
|
function capturedWithoutSnapshot(diff: string, reason: string): CapturedReviewMaterial {
|
|
289
284
|
return { ok: true, diff, snapshot: { status: "unavailable", reason } };
|
|
290
285
|
}
|
|
291
|
-
|
|
292
|
-
function fingerprintReviewDiff(diff: string): string {
|
|
293
|
-
return createHash("sha256").update(diff).digest("hex");
|
|
294
|
-
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { Type, type Static } from "typebox";
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Pins the git diff output that the engine parses or applies, so user config such as
|
|
6
|
+
* diff.noprefix, diff.mnemonicPrefix, diff.srcPrefix or color.ui cannot change it.
|
|
7
|
+
*/
|
|
8
|
+
export const GIT_DIFF_MACHINE_FORMAT = ["--no-color", "--src-prefix=a/", "--dst-prefix=b/"] as const;
|
|
9
|
+
|
|
4
10
|
export type GitDiffBaselineTarget =
|
|
5
11
|
| { readonly kind: "working-tree" }
|
|
6
12
|
| { readonly kind: "index" }
|
|
@@ -96,15 +102,16 @@ function isAllowedGitDiffFlag(token: string): boolean {
|
|
|
96
102
|
return SAFE_GIT_DIFF_FLAGS.has(token) || /^-U\d+$/.test(token) || /^--unified=\d+$/.test(token) || /^--inter-hunk-context=\d+$/.test(token);
|
|
97
103
|
}
|
|
98
104
|
|
|
105
|
+
/** Executable argv. Git targets gain the machine-format pins, which stay out of the persisted and displayed target. */
|
|
99
106
|
export function reviewDiffCommand(target: ReviewDiffTarget): { readonly file: "git" | "gh"; readonly args: readonly string[] } {
|
|
100
107
|
return target.kind === "pull-request"
|
|
101
108
|
? { file: "gh", args: ["pr", "diff", String(target.number), "--color=never"] }
|
|
102
|
-
: { file: "git", args: target.args };
|
|
109
|
+
: { file: "git", args: ["diff", ...GIT_DIFF_MACHINE_FORMAT, ...target.args.slice(1)] };
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
/** Canonical display form. Never execute this string directly; use reviewDiffCommand(). */
|
|
106
113
|
export function formatReviewDiffTarget(target: ReviewDiffTarget): string {
|
|
107
|
-
const command = reviewDiffCommand(target);
|
|
114
|
+
const command = target.kind === "git" ? { file: "git", args: target.args } : reviewDiffCommand(target);
|
|
108
115
|
return [command.file, ...command.args].join(" ");
|
|
109
116
|
}
|
|
110
117
|
|
|
@@ -115,16 +122,8 @@ export function reviewGitDiffBaseline(target: GitReviewDiffTarget): GitDiffBasel
|
|
|
115
122
|
return parsed.baseline;
|
|
116
123
|
}
|
|
117
124
|
|
|
118
|
-
/** Validate persisted
|
|
119
|
-
export function
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return !("error" in parsed) && sameReviewDiffTarget(parsed, value);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function sameReviewDiffTarget(left: ReviewDiffTarget, right: ReviewDiffTarget): boolean {
|
|
126
|
-
if (left.kind !== right.kind) return false;
|
|
127
|
-
if (left.kind === "pull-request") return right.kind === "pull-request" && left.number === right.number;
|
|
128
|
-
if (right.kind !== "git" || left.args.length !== right.args.length) return false;
|
|
129
|
-
return left.args.every((arg, index) => arg === right.args[index]);
|
|
125
|
+
/** Validate a schema-checked persisted target by round-tripping it through the single command allowlist parser. */
|
|
126
|
+
export function isAllowedReviewDiffTarget(target: ReviewDiffTarget): boolean {
|
|
127
|
+
const parsed = parseAllowedDiffCommand(formatReviewDiffTarget(target));
|
|
128
|
+
return !("error" in parsed) && Value.Equal(parsed, target);
|
|
130
129
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Cap text at `limit` characters, ending a cut value with an ellipsis. */
|
|
2
|
+
export function truncateText(value: string, limit: number): string {
|
|
3
|
+
return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Pretty-printed JSON, or undefined when the value has no JSON form or cannot be serialized (cycles, BigInt). */
|
|
7
|
+
export function prettyJson(value: unknown): string | undefined {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.stringify(value, null, 2);
|
|
10
|
+
} catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Compact count for display: 999, 1.2k, 12.3k, 123k, 1.2M. */
|
|
16
|
+
export function formatCount(n: number): string {
|
|
17
|
+
if (!Number.isFinite(n)) return "0";
|
|
18
|
+
const sign = n < 0 ? "-" : "";
|
|
19
|
+
const value = Math.abs(n);
|
|
20
|
+
if (value < 1_000) return `${Math.trunc(n)}`;
|
|
21
|
+
if (value < 1_000_000) return `${sign}${formatCompact(value / 1_000)}k`;
|
|
22
|
+
return `${sign}${formatCompact(value / 1_000_000)}M`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatCompact(value: number): string {
|
|
26
|
+
if (value >= 100) return `${Math.round(value)}`;
|
|
27
|
+
const rounded = Math.round(value * 10) / 10;
|
|
28
|
+
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(1);
|
|
29
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ToolInfo } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { isMcpToolName } from "./host-tools.ts";
|
|
2
3
|
import type { AgentToolHint } from "./types.ts";
|
|
3
4
|
|
|
4
5
|
/** Raised before prompting when required semantic tool capabilities are unavailable. */
|
|
@@ -6,8 +7,12 @@ export class WorkflowToolHintUnavailableError extends Error {
|
|
|
6
7
|
readonly code = "WORKFLOW_TOOL_HINT_UNAVAILABLE";
|
|
7
8
|
readonly hints: readonly AgentToolHint[];
|
|
8
9
|
|
|
9
|
-
constructor(hints: readonly AgentToolHint[]) {
|
|
10
|
-
|
|
10
|
+
constructor(hints: readonly AgentToolHint[], options: { readonly hostToolsReachable?: boolean } = {}) {
|
|
11
|
+
// Host MCP tools only count in runs that can reach them; say so rather than imply none are installed.
|
|
12
|
+
const mcpNote = hints.includes("external-search") && options.hostToolsReachable === false
|
|
13
|
+
? " (MCP tools reach subagents only in runs started by the workflow tool)"
|
|
14
|
+
: "";
|
|
15
|
+
super(`Required workflow tool capability unavailable: ${hints.join(", ")}${mcpNote}`);
|
|
11
16
|
this.name = "WorkflowToolHintUnavailableError";
|
|
12
17
|
this.hints = [...hints];
|
|
13
18
|
}
|
|
@@ -18,7 +23,9 @@ export function matchesAgentToolHint(tool: ToolInfo, hint: AgentToolHint): boole
|
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
function isExternalSearchLikeTool(tool: ToolInfo): boolean {
|
|
21
|
-
if (isMutationLikeToolName(tool.name) || tool
|
|
26
|
+
if (isMutationLikeToolName(tool.name) || isPiLocalTool(tool)) return false;
|
|
27
|
+
// MCP servers declare these hints: a tool that changes its environment or stays in a closed domain is not web research.
|
|
28
|
+
if (tool.annotations?.readOnlyHint === false || tool.annotations?.openWorldHint === false) return false;
|
|
22
29
|
const name = tool.name.toLowerCase();
|
|
23
30
|
const description = [tool.description, ...(tool.promptGuidelines ?? [])].join(" ").toLowerCase();
|
|
24
31
|
const text = `${name} ${description}`;
|
|
@@ -33,12 +40,18 @@ function isExternalSearchLikeTool(tool: ToolInfo): boolean {
|
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
function isSearchLikeTool(tool: ToolInfo): boolean {
|
|
36
|
-
|
|
43
|
+
// Local code search must not hand repository context to web search tools.
|
|
44
|
+
if (isMutationLikeToolName(tool.name) || isExternalSearchLikeTool(tool)) return false;
|
|
37
45
|
const name = tool.name.toLowerCase();
|
|
38
46
|
if (name.includes("grep") || name.includes("find") || name.includes("search") || name === "rg" || name.includes("ripgrep")) return true;
|
|
39
47
|
return /\b(?:grep|find|search|ripgrep|rg|structural search|code search)\b/.test(tool.description.toLowerCase());
|
|
40
48
|
}
|
|
41
49
|
|
|
50
|
+
/** pi's own tools, such as read and bash. MCP tools also come from a built-in extension but reach outside it. */
|
|
51
|
+
function isPiLocalTool(tool: ToolInfo): boolean {
|
|
52
|
+
return tool.sourceInfo.source === "builtin" && !isMcpToolName(tool.name);
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
function isMutationLikeToolName(name: string): boolean {
|
|
43
56
|
return /(?:edit|write|replace|patch|apply|delete|remove|move|rename|create|commit|push)/.test(name.toLowerCase());
|
|
44
57
|
}
|
|
@@ -2,8 +2,15 @@ import { lstat } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
3
|
import { isMissingPathError } from "./filesystem-error.ts";
|
|
4
4
|
import { hashIdentity } from "./identity-fingerprint.ts";
|
|
5
|
-
import { logicalWorkspacePath
|
|
6
|
-
import {
|
|
5
|
+
import { logicalWorkspacePath } from "./replay-path-identity.ts";
|
|
6
|
+
import {
|
|
7
|
+
captureSourceTreeFingerprint,
|
|
8
|
+
FINGERPRINT_EXCLUDED_RELATIVE_PATHS,
|
|
9
|
+
isPathWithin,
|
|
10
|
+
portableRelativePath,
|
|
11
|
+
validateTreeFile,
|
|
12
|
+
type FingerprintCapture,
|
|
13
|
+
} from "./tree-fingerprint.ts";
|
|
7
14
|
|
|
8
15
|
export interface EffectiveToolSourceInfoLike {
|
|
9
16
|
readonly path: string;
|
|
@@ -32,10 +39,6 @@ export interface ToolSourceIdentityOptions {
|
|
|
32
39
|
readonly signal?: AbortSignal;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
const SOURCE_FINGERPRINT_MAX_BYTES = 32 << 20;
|
|
36
|
-
const SOURCE_FINGERPRINT_MAX_FILES = 4096;
|
|
37
|
-
const SOURCE_FINGERPRINT_EXCLUSIONS = new Set([".git", ".pi/.workflow-runs"]);
|
|
38
|
-
|
|
39
42
|
export async function captureEffectiveToolSourceIdentity(
|
|
40
43
|
sourceInfo: EffectiveToolSourceInfoLike,
|
|
41
44
|
options: ToolSourceIdentityOptions,
|
|
@@ -58,26 +61,20 @@ export async function captureEffectiveToolSourceIdentity(
|
|
|
58
61
|
};
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
const baseDir = declaredBaseDir === undefined ? undefined :
|
|
64
|
+
const baseDir = declaredBaseDir === undefined ? undefined : resolve(options.sessionCwd, declaredBaseDir);
|
|
62
65
|
const sourcePath = await resolveExistingSourcePath(path, baseDir, options.sessionCwd, options.workspaceRoot);
|
|
63
66
|
const sourceRoot = baseDir && isPathWithin(baseDir, sourcePath) ? baseDir : dirname(sourcePath);
|
|
64
67
|
const validation = await validateTreeFile({
|
|
65
68
|
root: sourceRoot,
|
|
66
69
|
path: sourcePath,
|
|
67
|
-
excludedRelativePaths:
|
|
70
|
+
excludedRelativePaths: FINGERPRINT_EXCLUDED_RELATIVE_PATHS,
|
|
68
71
|
signal: options.signal,
|
|
69
72
|
});
|
|
70
73
|
if (validation.kind === "unverifiable") throw new Error(`tool source "${path}" is not fingerprintable: ${validation.reason}`);
|
|
71
74
|
|
|
72
75
|
let capture = options.cache.get(sourceRoot);
|
|
73
76
|
if (!capture) {
|
|
74
|
-
capture =
|
|
75
|
-
root: sourceRoot,
|
|
76
|
-
excludedRelativePaths: SOURCE_FINGERPRINT_EXCLUSIONS,
|
|
77
|
-
maxBytes: SOURCE_FINGERPRINT_MAX_BYTES,
|
|
78
|
-
maxFiles: SOURCE_FINGERPRINT_MAX_FILES,
|
|
79
|
-
signal: options.signal,
|
|
80
|
-
});
|
|
77
|
+
capture = captureSourceTreeFingerprint(sourceRoot, options.signal);
|
|
81
78
|
options.cache.set(sourceRoot, capture);
|
|
82
79
|
}
|
|
83
80
|
const fingerprint = await capture;
|
|
@@ -103,11 +100,11 @@ async function resolveExistingSourcePath(
|
|
|
103
100
|
): Promise<string> {
|
|
104
101
|
const candidates = isAbsolute(path)
|
|
105
102
|
? [resolve(path)]
|
|
106
|
-
:
|
|
103
|
+
: [...new Set([
|
|
107
104
|
...(baseDir ? [resolve(baseDir, path), resolve(baseDir, path.split(/[\\/]/).at(-1) ?? path)] : []),
|
|
108
105
|
resolve(sessionCwd, path),
|
|
109
106
|
resolve(workspaceRoot, path),
|
|
110
|
-
]);
|
|
107
|
+
])];
|
|
111
108
|
|
|
112
109
|
for (const candidate of candidates) {
|
|
113
110
|
try {
|
|
@@ -119,13 +116,8 @@ async function resolveExistingSourcePath(
|
|
|
119
116
|
throw new Error(`tool source path does not identify a regular file: ${path}`);
|
|
120
117
|
}
|
|
121
118
|
|
|
122
|
-
function resolveDeclaredPath(path: string, sessionCwd: string): string {
|
|
123
|
-
return isAbsolute(path) ? resolve(path) : resolve(sessionCwd, path);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
119
|
function normalizeUnresolvedBaseDir(path: string, sessionCwd: string, workspaceRoot: string): string {
|
|
127
|
-
|
|
128
|
-
return logicalWorkspacePath(resolvedPath, { sessionCwd, workspaceRoot }) ?? "source-root:.";
|
|
120
|
+
return logicalWorkspacePath(resolve(sessionCwd, path), { sessionCwd, workspaceRoot }) ?? "source-root:.";
|
|
129
121
|
}
|
|
130
122
|
|
|
131
123
|
function logicalSourcePath(path: string, sourceRoot: string, sessionCwd: string, workspaceRoot: string): string {
|
|
@@ -139,11 +131,7 @@ function isSyntheticSource(path: string, source: string): boolean {
|
|
|
139
131
|
return (path.startsWith("<") && path.endsWith(">")) || source === "builtin" || source === "sdk" || /^(?:builtin|sdk):/.test(path);
|
|
140
132
|
}
|
|
141
133
|
|
|
142
|
-
function
|
|
143
|
-
return [...new Set(paths)];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function nonEmptyString(value: string, label: string): string {
|
|
134
|
+
export function nonEmptyString(value: string, label: string): string {
|
|
147
135
|
if (typeof value !== "string") throw new Error(`${label} is not a string`);
|
|
148
136
|
if (value.length === 0) throw new Error(`${label} is empty`);
|
|
149
137
|
return value;
|
|
@@ -6,6 +6,15 @@ import { throwIfAborted } from "./cancellation.ts";
|
|
|
6
6
|
import { isMissingPathError } from "./filesystem-error.ts";
|
|
7
7
|
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
8
8
|
|
|
9
|
+
/** Engine-owned paths that are never part of a fingerprinted or snapshotted workspace. */
|
|
10
|
+
export const FINGERPRINT_EXCLUDED_RELATIVE_PATHS: ReadonlySet<string> = new Set([
|
|
11
|
+
".git",
|
|
12
|
+
".pi/.workflow-runs",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const SOURCE_TREE_MAX_BYTES = 32 << 20;
|
|
16
|
+
const SOURCE_TREE_MAX_FILES = 4096;
|
|
17
|
+
|
|
9
18
|
export type FingerprintCapture =
|
|
10
19
|
| { readonly kind: "verified"; readonly fingerprint: string }
|
|
11
20
|
| { readonly kind: "unverifiable"; readonly reason: string };
|
|
@@ -38,8 +47,6 @@ export interface DeclaredInputFingerprintOptions {
|
|
|
38
47
|
readonly excludedRelativePaths?: ReadonlySet<string>;
|
|
39
48
|
readonly maxBytes: number;
|
|
40
49
|
readonly maxEntries: number;
|
|
41
|
-
/** Record direct directory identity without recursively walking descendants. */
|
|
42
|
-
readonly shallowDirectories?: boolean;
|
|
43
50
|
readonly signal?: AbortSignal;
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -71,9 +78,9 @@ export class BoundedFingerprint {
|
|
|
71
78
|
this.#hash.update("\0");
|
|
72
79
|
}
|
|
73
80
|
|
|
81
|
+
/** File bytes may contain NUL, so they enter the outer hash as a fixed-length digest. */
|
|
74
82
|
async addFileHandle(label: string, handle: FileHandle, signal?: AbortSignal): Promise<void> {
|
|
75
|
-
|
|
76
|
-
this.#hash.update("\0");
|
|
83
|
+
const content = createHash("sha256");
|
|
77
84
|
const stream = handle.createReadStream({ autoClose: false, highWaterMark: 64 << 10, signal });
|
|
78
85
|
try {
|
|
79
86
|
for await (const chunk of stream) {
|
|
@@ -84,13 +91,13 @@ export class BoundedFingerprint {
|
|
|
84
91
|
stream.destroy();
|
|
85
92
|
throw new Error(`content fingerprint exceeded ${this.maxBytes} bytes`);
|
|
86
93
|
}
|
|
87
|
-
|
|
94
|
+
content.update(buffer);
|
|
88
95
|
}
|
|
89
96
|
} catch (error) {
|
|
90
97
|
throwIfAborted(signal);
|
|
91
98
|
throw error;
|
|
92
99
|
}
|
|
93
|
-
this
|
|
100
|
+
this.add(label, content.digest("hex"));
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
digest(): string {
|
|
@@ -178,6 +185,20 @@ export async function captureTreeFingerprint(options: TreeFingerprintOptions): P
|
|
|
178
185
|
}
|
|
179
186
|
}
|
|
180
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Fingerprint a workflow or tool source tree. Workflow provenance captured at
|
|
190
|
+
* load time is revalidated on resume, so both captures must share these bounds.
|
|
191
|
+
*/
|
|
192
|
+
export async function captureSourceTreeFingerprint(root: string, signal?: AbortSignal): Promise<FingerprintCapture> {
|
|
193
|
+
return await captureTreeFingerprint({
|
|
194
|
+
root,
|
|
195
|
+
excludedRelativePaths: FINGERPRINT_EXCLUDED_RELATIVE_PATHS,
|
|
196
|
+
maxBytes: SOURCE_TREE_MAX_BYTES,
|
|
197
|
+
maxFiles: SOURCE_TREE_MAX_FILES,
|
|
198
|
+
signal,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
181
202
|
/** Fingerprint only explicitly declared files/directories under one shared bound. */
|
|
182
203
|
export async function captureDeclaredInputFingerprint(
|
|
183
204
|
options: DeclaredInputFingerprintOptions,
|
|
@@ -227,11 +248,6 @@ export async function captureDeclaredInputFingerprint(
|
|
|
227
248
|
}
|
|
228
249
|
if (!info.isDirectory()) throw new Error(`declared input contains an unsupported entry: ${relativePath}`);
|
|
229
250
|
|
|
230
|
-
if (options.shallowDirectories === true) {
|
|
231
|
-
recordEntry(relativePath, "directory", info.mode);
|
|
232
|
-
return true;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
251
|
const entries = await readdir(path, { withFileTypes: true });
|
|
236
252
|
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
237
253
|
let hasObservableChild = false;
|
|
@@ -341,6 +357,11 @@ export function isPathWithin(root: string, path: string): boolean {
|
|
|
341
357
|
return pathFromRoot === "" || (!isAbsolute(pathFromRoot) && pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep}`));
|
|
342
358
|
}
|
|
343
359
|
|
|
360
|
+
export function portableRelativePath(root: string, path: string): string {
|
|
361
|
+
const value = relative(root, path).split(sep).join("/");
|
|
362
|
+
return value.length === 0 ? "." : value;
|
|
363
|
+
}
|
|
364
|
+
|
|
344
365
|
function isExcludedRelativePath(path: string, excluded: ReadonlySet<string> | undefined): boolean {
|
|
345
366
|
if (!excluded || excluded.size === 0) return false;
|
|
346
367
|
const normalized = normalizeRelativePath(path);
|
|
@@ -361,8 +382,7 @@ function declaredInputPath(root: string, base: string, input: string): string {
|
|
|
361
382
|
const path = resolve(base, input);
|
|
362
383
|
if (!isPathWithin(root, path)) throw new Error(`declared input escapes the repository root: ${input}`);
|
|
363
384
|
if (!isPathWithin(base, path)) throw new Error(`declared input escapes the workflow cwd: ${input}`);
|
|
364
|
-
|
|
365
|
-
return relativePath.length === 0 ? "." : relativePath;
|
|
385
|
+
return portableRelativePath(root, path);
|
|
366
386
|
}
|
|
367
387
|
|
|
368
388
|
function rejectExcludedDeclaredInput(path: string, excluded: ReadonlySet<string> | undefined): void {
|
|
@@ -370,7 +390,7 @@ function rejectExcludedDeclaredInput(path: string, excluded: ReadonlySet<string>
|
|
|
370
390
|
throw new Error(`declared input enters excluded path: ${path}`);
|
|
371
391
|
}
|
|
372
392
|
|
|
373
|
-
function isExcludedDeclaredInput(path: string, excluded: ReadonlySet<string> | undefined): boolean {
|
|
393
|
+
export function isExcludedDeclaredInput(path: string, excluded: ReadonlySet<string> | undefined): boolean {
|
|
374
394
|
if (!excluded) return false;
|
|
375
395
|
const normalized = normalizeRelativePath(path);
|
|
376
396
|
for (const candidate of excluded) {
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import type { Static, TSchema } from "typebox";
|
|
2
|
+
import type { ClassifierAnswer, ClassifierContext } from "@earendil-works/pi-ai";
|
|
2
3
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
3
4
|
import type { WorkflowBudget } from "./budget.ts";
|
|
4
5
|
import type { Pipeline, WorkflowParallel } from "./concurrency.ts";
|
|
5
6
|
import type { PerfSink, PerfSnapshot } from "./perf.ts";
|
|
7
|
+
|
|
8
|
+
/** The host-facing tool this extension registers; subagents also load extensions, so it is withheld from them. */
|
|
9
|
+
export const WORKFLOW_TOOL_NAME = "workflow";
|
|
6
10
|
import type { WorkflowLaneItemStatus, WorkflowProgressSnapshot } from "./progress-types.ts";
|
|
7
11
|
import type { WorkflowUsageSnapshot } from "./usage.ts";
|
|
8
12
|
import type { WorktreeBaseline } from "./worktree.ts";
|
|
9
13
|
import type { WorkflowModelProfileName } from "./model-profiles.ts";
|
|
14
|
+
import type { HostToolBridge } from "./host-tools.ts";
|
|
15
|
+
import type { WorkflowRunState } from "./workflow-run-record.ts";
|
|
10
16
|
|
|
11
17
|
export type { WorkflowLaneItemStatus } from "./progress-types.ts";
|
|
12
18
|
|
|
@@ -75,6 +81,8 @@ export interface WorkflowRunOptions {
|
|
|
75
81
|
signal?: AbortSignal;
|
|
76
82
|
/** Internal recorder override for command/tool invocation timing. */
|
|
77
83
|
perfRecorder?: PerfSink;
|
|
84
|
+
/** Internal bridge to the host session's MCP tools, available while a `workflow` tool call runs. */
|
|
85
|
+
hostTools?: HostToolBridge;
|
|
78
86
|
/** Resolve a sub-workflow reference to a module, enabling `api.workflow()`. When omitted, `api.workflow()` throws. */
|
|
79
87
|
resolveWorkflow?: (ref: WorkflowRef) => Promise<LoadedWorkflow>;
|
|
80
88
|
/** Called with the final performance snapshot when perf is enabled. */
|
|
@@ -91,6 +99,8 @@ export interface WorkflowRunOptions {
|
|
|
91
99
|
|
|
92
100
|
export interface WorkflowProgressSource {
|
|
93
101
|
snapshot(): WorkflowProgressSnapshot;
|
|
102
|
+
/** Durable lifecycle state of the run, so a finished snapshot can tell completed from failed. */
|
|
103
|
+
readonly state?: () => WorkflowRunState;
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
/** Options for a single `agent()` call. */
|
|
@@ -179,6 +189,19 @@ export interface AgentOptions<S extends TSchema = TSchema> {
|
|
|
179
189
|
schema?: S;
|
|
180
190
|
}
|
|
181
191
|
|
|
192
|
+
/** Options for `api.classify()`. */
|
|
193
|
+
export interface ClassifyOptions {
|
|
194
|
+
/**
|
|
195
|
+
* Classifier model as "provider/id", or refs in preference order of which the first with working
|
|
196
|
+
* credentials is used. Default: the first classifier model with working credentials.
|
|
197
|
+
*/
|
|
198
|
+
model?: string | readonly string[];
|
|
199
|
+
/** Label for usage reporting. Default: `classify:<provider>/<id>`. */
|
|
200
|
+
label?: string;
|
|
201
|
+
/** Phase to attribute usage to. Default: the current phase. */
|
|
202
|
+
phase?: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
182
205
|
/**
|
|
183
206
|
* The primitives injected into every workflow run. A workflow is any module that
|
|
184
207
|
* exports `meta` plus a default `async (api: WorkflowApi) => result`.
|
|
@@ -195,6 +218,13 @@ export interface WorkflowApi {
|
|
|
195
218
|
agent<S extends TSchema>(prompt: string, opts: AgentOptions<S> & { schema: S }): Promise<Static<S>>;
|
|
196
219
|
/** Run a subagent and return its final assistant text. */
|
|
197
220
|
agent(prompt: string, opts?: AgentOptions): Promise<string>;
|
|
221
|
+
/**
|
|
222
|
+
* Answer typed `choice`/`score`/`bool` questions about JSON `state` with a classifier model, without
|
|
223
|
+
* starting a chat session: a cheap gate for triage, deduplication, or routing between stages. Answers
|
|
224
|
+
* are keyed by question id. Rejects with a recoverable error when no classifier is configured or the
|
|
225
|
+
* call fails; results are not journaled, so a resumed run classifies again.
|
|
226
|
+
*/
|
|
227
|
+
classify(context: ClassifierContext, opts?: ClassifyOptions): Promise<Record<string, ClassifierAnswer>>;
|
|
198
228
|
/**
|
|
199
229
|
* Run another registered workflow inline as a sub-step and return its result. The child shares
|
|
200
230
|
* this run's concurrency cap, abort signal, and perf sink. Nests one level only: calling
|