@jameslovespancakes/pi-plus 1.0.11 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -20
- package/config/skills/workflow-code-review-actions/SKILL.md +44 -0
- package/package.json +6 -3
- package/src/core/accounts/oauth-pool.ts +102 -0
- package/src/core/accounts/registry.ts +8 -25
- package/src/core/accounts/routing.ts +122 -0
- package/src/core/anthropic/client-identity.ts +9 -64
- package/src/core/anthropic/quota.ts +8 -53
- package/src/core/anthropic/routing.ts +40 -115
- package/src/core/anthropic/store.ts +7 -9
- package/src/core/catalog/quality.ts +33 -15
- package/src/core/codex/quota.ts +8 -2
- package/src/core/codex/store.ts +12 -9
- package/src/core/config.ts +4 -17
- package/src/core/store.ts +13 -12
- package/src/domains/models/provider-picker.ts +1 -1
- package/src/domains/setup/index.ts +23 -23
- package/src/domains/subscriptions/accounts-picker.ts +18 -37
- package/src/domains/subscriptions/accounts.ts +23 -29
- package/src/domains/subscriptions/footer.ts +48 -29
- package/src/domains/subscriptions/index.ts +11 -27
- package/src/domains/subscriptions/provider.ts +38 -103
- package/src/domains/subscriptions/providers/anthropic.ts +5 -17
- package/src/domains/subscriptions/providers/codex.ts +171 -4
- package/src/domains/subscriptions/providers/hosted.ts +18 -0
- package/src/domains/subscriptions/providers/oauth-pool.ts +273 -0
- package/src/domains/subscriptions/routing.ts +19 -11
- package/src/domains/workflows/LICENSE.md +21 -0
- package/src/domains/workflows/index.ts +836 -0
- package/src/domains/workflows/runtime/advisory-challenge.ts +75 -0
- package/src/domains/workflows/runtime/advisory-evidence.ts +90 -0
- package/src/domains/workflows/runtime/advisory-schema.ts +83 -0
- package/src/domains/workflows/runtime/agent-attempt.ts +147 -0
- package/src/domains/workflows/runtime/agent-limits.ts +43 -0
- package/src/domains/workflows/runtime/agent-replay.ts +399 -0
- package/src/domains/workflows/runtime/agent-retry.ts +116 -0
- package/src/domains/workflows/runtime/agent-runner-types.ts +96 -0
- package/src/domains/workflows/runtime/agent-runner.ts +203 -0
- package/src/domains/workflows/runtime/agent-session-identity.ts +256 -0
- package/src/domains/workflows/runtime/agent-session-providers.ts +50 -0
- package/src/domains/workflows/runtime/agent-session.ts +382 -0
- package/src/domains/workflows/runtime/agent-skills.ts +270 -0
- package/src/domains/workflows/runtime/agent-workspace.ts +103 -0
- package/src/domains/workflows/runtime/background-workflow-tool.ts +75 -0
- package/src/domains/workflows/runtime/background-workflows.ts +492 -0
- package/src/domains/workflows/runtime/budget.ts +53 -0
- package/src/domains/workflows/runtime/cancellation.ts +87 -0
- package/src/domains/workflows/runtime/command-completions.ts +36 -0
- package/src/domains/workflows/runtime/concurrency.ts +403 -0
- package/src/domains/workflows/runtime/debug.ts +3 -0
- package/src/domains/workflows/runtime/diff-capture.ts +81 -0
- package/src/domains/workflows/runtime/discovery.ts +137 -0
- package/src/domains/workflows/runtime/dynamax-shortcuts.ts +122 -0
- package/src/domains/workflows/runtime/dynamax.ts +330 -0
- package/src/domains/workflows/runtime/engine.ts +543 -0
- package/src/domains/workflows/runtime/filesystem-error.ts +4 -0
- package/src/domains/workflows/runtime/finalizers.ts +66 -0
- package/src/domains/workflows/runtime/identity-canonicalization.ts +138 -0
- package/src/domains/workflows/runtime/identity-fingerprint.ts +15 -0
- package/src/domains/workflows/runtime/inline-workflow.ts +403 -0
- package/src/domains/workflows/runtime/journal.ts +313 -0
- package/src/domains/workflows/runtime/model-profiles.ts +310 -0
- package/src/domains/workflows/runtime/options.ts +157 -0
- package/src/domains/workflows/runtime/perf.ts +146 -0
- package/src/domains/workflows/runtime/pi-compat.ts +32 -0
- package/src/domains/workflows/runtime/process-runner.ts +260 -0
- package/src/domains/workflows/runtime/progress-types.ts +59 -0
- package/src/domains/workflows/runtime/progress.ts +400 -0
- package/src/domains/workflows/runtime/provider-usage-limit.ts +191 -0
- package/src/domains/workflows/runtime/replay-path-identity.ts +48 -0
- package/src/domains/workflows/runtime/research-contract.ts +89 -0
- package/src/domains/workflows/runtime/research-evidence.ts +289 -0
- package/src/domains/workflows/runtime/resume-context.ts +751 -0
- package/src/domains/workflows/runtime/review/code-review-orchestration.ts +24 -0
- package/src/domains/workflows/runtime/review/github-pr-comments.ts +249 -0
- package/src/domains/workflows/runtime/review/patch-validation.ts +120 -0
- package/src/domains/workflows/runtime/review/review-actions.ts +180 -0
- package/src/domains/workflows/runtime/review/review-budget.ts +45 -0
- package/src/domains/workflows/runtime/review/review-fix-workflow.ts +153 -0
- package/src/domains/workflows/runtime/review/review-format.ts +133 -0
- package/src/domains/workflows/runtime/review/review-handoff.ts +38 -0
- package/src/domains/workflows/runtime/review/review-issues.ts +87 -0
- package/src/domains/workflows/runtime/review/review-report.ts +42 -0
- package/src/domains/workflows/runtime/review/review-results-flow.ts +52 -0
- package/src/domains/workflows/runtime/review/review-results-viewer.ts +312 -0
- package/src/domains/workflows/runtime/review/review-session-coordinator.ts +153 -0
- package/src/domains/workflows/runtime/review/review-snapshot.ts +294 -0
- package/src/domains/workflows/runtime/review-diff-target.ts +130 -0
- package/src/domains/workflows/runtime/session-identity.ts +6 -0
- package/src/domains/workflows/runtime/structured-output.ts +32 -0
- package/src/domains/workflows/runtime/tool-capabilities.ts +44 -0
- package/src/domains/workflows/runtime/tool-source-identity.ts +150 -0
- package/src/domains/workflows/runtime/tree-fingerprint.ts +404 -0
- package/src/domains/workflows/runtime/types.ts +255 -0
- package/src/domains/workflows/runtime/ui/display-text.ts +13 -0
- package/src/domains/workflows/runtime/ui/dynamax-editor-decoration.ts +220 -0
- package/src/domains/workflows/runtime/ui/workflow-format.ts +165 -0
- package/src/domains/workflows/runtime/ui/workflow-inspector.ts +398 -0
- package/src/domains/workflows/runtime/ui/workflow-result-renderer.ts +171 -0
- package/src/domains/workflows/runtime/ui/workflow-viewer-layout.ts +51 -0
- package/src/domains/workflows/runtime/ui/workflow-widget.ts +78 -0
- package/src/domains/workflows/runtime/unknown-error.ts +8 -0
- package/src/domains/workflows/runtime/usage.ts +341 -0
- package/src/domains/workflows/runtime/workflow-advisory-utils.ts +245 -0
- package/src/domains/workflows/runtime/workflow-execution.ts +121 -0
- package/src/domains/workflows/runtime/workflow-module.ts +75 -0
- package/src/domains/workflows/runtime/workflow-run-background.ts +72 -0
- package/src/domains/workflows/runtime/workflow-run-controller.ts +342 -0
- package/src/domains/workflows/runtime/workflow-run-history.ts +162 -0
- package/src/domains/workflows/runtime/workflow-run-record.ts +635 -0
- package/src/domains/workflows/runtime/workflow-run-store.ts +176 -0
- package/src/domains/workflows/runtime/workflow-usage-limit-scheduler.ts +80 -0
- package/src/domains/workflows/runtime/workflows.ts +40 -0
- package/src/domains/workflows/runtime/worktree.ts +581 -0
- package/src/domains/workflows/workflows/code-review.ts +232 -0
- package/src/domains/workflows/workflows/diagnose.ts +154 -0
- package/src/domains/workflows/workflows/perf-review.ts +149 -0
- package/src/domains/workflows/workflows/refactor-scout.ts +143 -0
- package/src/domains/workflows/workflows/research.ts +169 -0
- package/src/services/usage-service.ts +6 -30
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { WorkflowRunOptions } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export const WORKFLOW_BUDGET_MIN = 1;
|
|
4
|
+
export const WORKFLOW_BUDGET_MAX = 1_000_000_000;
|
|
5
|
+
export const WORKFLOW_MAX_AGENTS_MIN = 1;
|
|
6
|
+
export const WORKFLOW_MAX_AGENTS_MAX = 10_000;
|
|
7
|
+
export const WORKFLOW_AGENT_TIMEOUT_MIN_MS = 1_000;
|
|
8
|
+
export const WORKFLOW_AGENT_TIMEOUT_MAX_MS = 86_400_000;
|
|
9
|
+
export const WORKFLOW_AGENT_RETRIES_MIN = 0;
|
|
10
|
+
export const WORKFLOW_AGENT_RETRIES_MAX = 10;
|
|
11
|
+
export const DEFAULT_WORKFLOW_AGENT_RETRIES = 0;
|
|
12
|
+
export const WORKFLOW_USAGE_LIMIT_ATTEMPTS_MIN = 1;
|
|
13
|
+
export const WORKFLOW_USAGE_LIMIT_ATTEMPTS_MAX = 10;
|
|
14
|
+
export const DEFAULT_WORKFLOW_USAGE_LIMIT_MAX_ATTEMPTS = 3;
|
|
15
|
+
export const WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS = 5_000;
|
|
16
|
+
export const WORKFLOW_USAGE_LIMIT_DELAY_MAX_MS = 86_400_000;
|
|
17
|
+
export const DEFAULT_WORKFLOW_USAGE_LIMIT_MAX_DELAY_MS = 21_600_000;
|
|
18
|
+
|
|
19
|
+
export type ResolvedWorkflowRunOptions = Omit<
|
|
20
|
+
WorkflowRunOptions,
|
|
21
|
+
| "perf"
|
|
22
|
+
| "concurrency"
|
|
23
|
+
| "parallelSubmissionLimit"
|
|
24
|
+
| "maxAgents"
|
|
25
|
+
| "agentTimeoutMs"
|
|
26
|
+
| "agentRetries"
|
|
27
|
+
| "autoResumeOnUsageLimit"
|
|
28
|
+
| "usageLimitMaxAttempts"
|
|
29
|
+
| "usageLimitMaxDelayMs"
|
|
30
|
+
| "usageLimitAttempt"
|
|
31
|
+
| "resumeEditedWorkflow"
|
|
32
|
+
| "budget"
|
|
33
|
+
> & {
|
|
34
|
+
readonly perf: boolean;
|
|
35
|
+
readonly concurrency: number | null;
|
|
36
|
+
readonly parallelSubmissionLimit: number | null;
|
|
37
|
+
readonly maxAgents: number | null;
|
|
38
|
+
readonly agentTimeoutMs: number | null;
|
|
39
|
+
readonly agentRetries: number;
|
|
40
|
+
readonly autoResumeOnUsageLimit: boolean;
|
|
41
|
+
readonly usageLimitMaxAttempts: number;
|
|
42
|
+
readonly usageLimitMaxDelayMs: number;
|
|
43
|
+
readonly usageLimitAttempt: number;
|
|
44
|
+
readonly resumeEditedWorkflow: boolean;
|
|
45
|
+
readonly budget: number | null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function resolveWorkflowRunOptions(
|
|
49
|
+
input: WorkflowRunOptions = {},
|
|
50
|
+
env: Record<string, string | undefined> = process.env,
|
|
51
|
+
): ResolvedWorkflowRunOptions {
|
|
52
|
+
const concurrency = optionalClampedInteger(
|
|
53
|
+
input.concurrency ?? parseInteger(env.PI_WORKFLOW_CONCURRENCY),
|
|
54
|
+
1,
|
|
55
|
+
64,
|
|
56
|
+
);
|
|
57
|
+
const parallelSubmissionLimit = optionalClampedInteger(
|
|
58
|
+
input.parallelSubmissionLimit ?? parseInteger(env.PI_WORKFLOW_PARALLEL_SUBMISSION_LIMIT),
|
|
59
|
+
1,
|
|
60
|
+
10_000,
|
|
61
|
+
);
|
|
62
|
+
const maxAgents = optionalClampedInteger(
|
|
63
|
+
input.maxAgents ?? parseWorkflowIntegerString(env.PI_WORKFLOW_MAX_AGENTS),
|
|
64
|
+
WORKFLOW_MAX_AGENTS_MIN,
|
|
65
|
+
WORKFLOW_MAX_AGENTS_MAX,
|
|
66
|
+
);
|
|
67
|
+
const agentTimeoutMs = optionalClampedInteger(
|
|
68
|
+
input.agentTimeoutMs ?? parseWorkflowIntegerString(env.PI_WORKFLOW_AGENT_TIMEOUT_MS),
|
|
69
|
+
WORKFLOW_AGENT_TIMEOUT_MIN_MS,
|
|
70
|
+
WORKFLOW_AGENT_TIMEOUT_MAX_MS,
|
|
71
|
+
);
|
|
72
|
+
const agentRetries = clampInteger(
|
|
73
|
+
input.agentRetries ?? parseWorkflowIntegerString(env.PI_WORKFLOW_AGENT_RETRIES),
|
|
74
|
+
WORKFLOW_AGENT_RETRIES_MIN,
|
|
75
|
+
WORKFLOW_AGENT_RETRIES_MAX,
|
|
76
|
+
DEFAULT_WORKFLOW_AGENT_RETRIES,
|
|
77
|
+
);
|
|
78
|
+
const budget = resolveBudget(input.budget, env.PI_WORKFLOW_BUDGET);
|
|
79
|
+
const usageLimitMaxAttempts = clampInteger(
|
|
80
|
+
input.usageLimitMaxAttempts ?? parseWorkflowIntegerString(env.PI_WORKFLOW_USAGE_LIMIT_MAX_ATTEMPTS),
|
|
81
|
+
WORKFLOW_USAGE_LIMIT_ATTEMPTS_MIN,
|
|
82
|
+
WORKFLOW_USAGE_LIMIT_ATTEMPTS_MAX,
|
|
83
|
+
DEFAULT_WORKFLOW_USAGE_LIMIT_MAX_ATTEMPTS,
|
|
84
|
+
);
|
|
85
|
+
const usageLimitMaxDelayMs = clampInteger(
|
|
86
|
+
input.usageLimitMaxDelayMs ?? parseWorkflowIntegerString(env.PI_WORKFLOW_USAGE_LIMIT_MAX_DELAY_MS),
|
|
87
|
+
WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS,
|
|
88
|
+
WORKFLOW_USAGE_LIMIT_DELAY_MAX_MS,
|
|
89
|
+
DEFAULT_WORKFLOW_USAGE_LIMIT_MAX_DELAY_MS,
|
|
90
|
+
);
|
|
91
|
+
return {
|
|
92
|
+
...input,
|
|
93
|
+
perf: input.perf ?? env.PI_WORKFLOW_PERF === "1",
|
|
94
|
+
concurrency: concurrency ?? null,
|
|
95
|
+
parallelSubmissionLimit: parallelSubmissionLimit ?? null,
|
|
96
|
+
maxAgents: maxAgents ?? null,
|
|
97
|
+
agentTimeoutMs: agentTimeoutMs ?? null,
|
|
98
|
+
agentRetries,
|
|
99
|
+
autoResumeOnUsageLimit: input.autoResumeOnUsageLimit ?? env.PI_WORKFLOW_USAGE_LIMIT_AUTO_RESUME === "1",
|
|
100
|
+
usageLimitMaxAttempts,
|
|
101
|
+
usageLimitMaxDelayMs,
|
|
102
|
+
usageLimitAttempt: clampInteger(
|
|
103
|
+
input.usageLimitAttempt,
|
|
104
|
+
0,
|
|
105
|
+
WORKFLOW_USAGE_LIMIT_ATTEMPTS_MAX,
|
|
106
|
+
0,
|
|
107
|
+
),
|
|
108
|
+
resumeEditedWorkflow: input.resumeEditedWorkflow === true && hasResumeRunId(input.resumeFromRunId),
|
|
109
|
+
budget: budget ?? null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function hasResumeRunId(value: string | undefined): boolean {
|
|
114
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function optionalClampedInteger(value: number | undefined, min: number, max: number): number | undefined {
|
|
118
|
+
if (value === undefined || !Number.isFinite(value)) return undefined;
|
|
119
|
+
return clampInteger(value, min, max, value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function clampInteger(value: number | undefined, min: number, max: number, fallback: number): number {
|
|
123
|
+
if (value === undefined || !Number.isFinite(value)) return fallback;
|
|
124
|
+
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseInteger(value: string | undefined): number | undefined {
|
|
128
|
+
if (value === undefined || value.trim() === "") return undefined;
|
|
129
|
+
const parsed = Number(value);
|
|
130
|
+
return Number.isFinite(parsed) ? Math.trunc(parsed) : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function parseWorkflowIntegerString(value: string | undefined): number | undefined {
|
|
134
|
+
if (value === undefined || value.trim() === "") return undefined;
|
|
135
|
+
const parsed = Number(value);
|
|
136
|
+
return Number.isSafeInteger(parsed) ? parsed : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveBudget(inputBudget: number | undefined, envBudget: string | undefined): number | undefined {
|
|
140
|
+
if (inputBudget !== undefined) return normalizeExplicitBudget(inputBudget);
|
|
141
|
+
return envBudget === undefined ? undefined : parseWorkflowBudgetString(envBudget);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizeExplicitBudget(value: number): number {
|
|
145
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < WORKFLOW_BUDGET_MIN || value > WORKFLOW_BUDGET_MAX) {
|
|
146
|
+
throw new RangeError(`Workflow budget must be an integer between ${WORKFLOW_BUDGET_MIN} and ${WORKFLOW_BUDGET_MAX}.`);
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function parseWorkflowBudgetString(value: string): number | undefined {
|
|
152
|
+
const trimmed = value.trim();
|
|
153
|
+
if (!/^\d+$/.test(trimmed)) return undefined;
|
|
154
|
+
const parsed = Number(trimmed);
|
|
155
|
+
if (!Number.isSafeInteger(parsed) || parsed < WORKFLOW_BUDGET_MIN || parsed > WORKFLOW_BUDGET_MAX) return undefined;
|
|
156
|
+
return parsed;
|
|
157
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
|
|
3
|
+
export type PerfTags = Record<string, string | number>;
|
|
4
|
+
|
|
5
|
+
export interface PerfSample {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly durationMs?: number;
|
|
8
|
+
readonly value?: number;
|
|
9
|
+
readonly tags?: PerfTags;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PerfAggregate {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly count: number;
|
|
15
|
+
readonly total: number;
|
|
16
|
+
readonly min: number;
|
|
17
|
+
readonly max: number;
|
|
18
|
+
readonly mean: number;
|
|
19
|
+
readonly p50: number;
|
|
20
|
+
readonly p95: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PerfSnapshot {
|
|
24
|
+
readonly enabled: boolean;
|
|
25
|
+
readonly startedAt: number;
|
|
26
|
+
readonly samples: readonly PerfSample[];
|
|
27
|
+
readonly aggregates: readonly PerfAggregate[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PerfSink {
|
|
31
|
+
time<T>(name: string, fn: () => Promise<T>, tags?: PerfTags): Promise<T>;
|
|
32
|
+
timeSync<T>(name: string, fn: () => T, tags?: PerfTags): T;
|
|
33
|
+
observe(name: string, value: number, tags?: PerfTags): void;
|
|
34
|
+
counter(name: string, delta?: number, tags?: PerfTags): void;
|
|
35
|
+
snapshot(): PerfSnapshot;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class PerfRecorder implements PerfSink {
|
|
39
|
+
private readonly samples: PerfSample[] = [];
|
|
40
|
+
private readonly startedAt: number;
|
|
41
|
+
|
|
42
|
+
constructor(startedAt = Date.now()) {
|
|
43
|
+
this.startedAt = startedAt;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async time<T>(name: string, fn: () => Promise<T>, tags?: PerfTags): Promise<T> {
|
|
47
|
+
const start = performance.now();
|
|
48
|
+
try {
|
|
49
|
+
return await fn();
|
|
50
|
+
} finally {
|
|
51
|
+
this.observeDuration(name, performance.now() - start, tags);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
timeSync<T>(name: string, fn: () => T, tags?: PerfTags): T {
|
|
56
|
+
const start = performance.now();
|
|
57
|
+
try {
|
|
58
|
+
return fn();
|
|
59
|
+
} finally {
|
|
60
|
+
this.observeDuration(name, performance.now() - start, tags);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
observe(name: string, value: number, tags?: PerfTags): void {
|
|
65
|
+
if (!Number.isFinite(value)) return;
|
|
66
|
+
this.samples.push({ name, value, tags });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
counter(name: string, delta = 1, tags?: PerfTags): void {
|
|
70
|
+
this.observe(name, delta, tags);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
snapshot(): PerfSnapshot {
|
|
74
|
+
return {
|
|
75
|
+
enabled: true,
|
|
76
|
+
startedAt: this.startedAt,
|
|
77
|
+
samples: this.samples.map((sample) => ({ ...sample, tags: sample.tags ? { ...sample.tags } : undefined })),
|
|
78
|
+
aggregates: aggregateSamples(this.samples),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private observeDuration(name: string, durationMs: number, tags?: PerfTags): void {
|
|
83
|
+
if (!Number.isFinite(durationMs)) return;
|
|
84
|
+
this.samples.push({ name, durationMs, tags });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class NoopPerfRecorder implements PerfSink {
|
|
89
|
+
private readonly startedAt: number;
|
|
90
|
+
|
|
91
|
+
constructor(startedAt = Date.now()) {
|
|
92
|
+
this.startedAt = startedAt;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async time<T>(_name: string, fn: () => Promise<T>, _tags?: PerfTags): Promise<T> {
|
|
96
|
+
return await fn();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
timeSync<T>(_name: string, fn: () => T, _tags?: PerfTags): T {
|
|
100
|
+
return fn();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
observe(_name: string, _value: number, _tags?: PerfTags): void {}
|
|
104
|
+
|
|
105
|
+
counter(_name: string, _delta?: number, _tags?: PerfTags): void {}
|
|
106
|
+
|
|
107
|
+
snapshot(): PerfSnapshot {
|
|
108
|
+
return { enabled: false, startedAt: this.startedAt, samples: [], aggregates: [] };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function createPerfRecorder(enabled: boolean, startedAt?: number): PerfSink {
|
|
113
|
+
return enabled ? new PerfRecorder(startedAt) : new NoopPerfRecorder(startedAt);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function aggregateSamples(samples: readonly PerfSample[]): PerfAggregate[] {
|
|
117
|
+
const byName = new Map<string, number[]>();
|
|
118
|
+
for (const sample of samples) {
|
|
119
|
+
const value = sample.durationMs ?? sample.value;
|
|
120
|
+
if (value === undefined || !Number.isFinite(value)) continue;
|
|
121
|
+
const values = byName.get(sample.name) ?? [];
|
|
122
|
+
values.push(value);
|
|
123
|
+
byName.set(sample.name, values);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return [...byName.entries()].map(([name, rawValues]) => {
|
|
127
|
+
const values = [...rawValues].sort((a, b) => a - b);
|
|
128
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
129
|
+
return {
|
|
130
|
+
name,
|
|
131
|
+
count: values.length,
|
|
132
|
+
total,
|
|
133
|
+
min: values[0] ?? 0,
|
|
134
|
+
max: values[values.length - 1] ?? 0,
|
|
135
|
+
mean: values.length === 0 ? 0 : total / values.length,
|
|
136
|
+
p50: percentile(values, 0.5),
|
|
137
|
+
p95: percentile(values, 0.95),
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function percentile(sortedValues: readonly number[], p: number): number {
|
|
143
|
+
if (sortedValues.length === 0) return 0;
|
|
144
|
+
const index = Math.min(sortedValues.length - 1, Math.max(0, Math.ceil(sortedValues.length * p) - 1));
|
|
145
|
+
return sortedValues[index] ?? 0;
|
|
146
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const MINIMUM_PI_VERSION = "0.80.10";
|
|
2
|
+
|
|
3
|
+
const MINIMUM_PI_CORE = [0, 80, 10] as const;
|
|
4
|
+
|
|
5
|
+
/** Fail during extension registration instead of failing later in a workflow. */
|
|
6
|
+
export function assertSupportedPiVersion(version: string): void {
|
|
7
|
+
const core = parseVersionCore(version);
|
|
8
|
+
if (!core || compareVersionCore(core, MINIMUM_PI_CORE) < 0) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`The workflow engine requires pi ${MINIMUM_PI_VERSION} or newer; detected ${JSON.stringify(version)}. Update pi before loading this extension.`,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseVersionCore(version: string): readonly [number, number, number] | undefined {
|
|
16
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version.trim());
|
|
17
|
+
if (!match) return undefined;
|
|
18
|
+
const major = Number(match[1]);
|
|
19
|
+
const minor = Number(match[2]);
|
|
20
|
+
const patch = Number(match[3]);
|
|
21
|
+
if (![major, minor, patch].every(Number.isSafeInteger)) return undefined;
|
|
22
|
+
return [major, minor, patch];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function compareVersionCore(
|
|
26
|
+
left: readonly [number, number, number],
|
|
27
|
+
right: readonly [number, number, number],
|
|
28
|
+
): number {
|
|
29
|
+
const [leftMajor, leftMinor, leftPatch] = left;
|
|
30
|
+
const [rightMajor, rightMinor, rightPatch] = right;
|
|
31
|
+
return leftMajor - rightMajor || leftMinor - rightMinor || leftPatch - rightPatch;
|
|
32
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
4
|
+
|
|
5
|
+
export interface BoundedProcessOptions {
|
|
6
|
+
readonly file: string;
|
|
7
|
+
readonly args: readonly string[];
|
|
8
|
+
readonly cwd: string;
|
|
9
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
10
|
+
readonly stdin?: string | Uint8Array;
|
|
11
|
+
readonly signal?: AbortSignal;
|
|
12
|
+
readonly timeoutMs: number;
|
|
13
|
+
readonly maxBufferBytes?: number;
|
|
14
|
+
readonly killGraceMs?: number;
|
|
15
|
+
readonly abortError: string;
|
|
16
|
+
readonly timeoutError: string;
|
|
17
|
+
readonly maxBufferError?: string;
|
|
18
|
+
readonly exitError: (stderr: string, code: number | null, signal: NodeJS.Signals | null) => string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface BoundedProcessResultBase {
|
|
22
|
+
readonly stdout: string;
|
|
23
|
+
readonly stderr: string;
|
|
24
|
+
readonly durationMs: number;
|
|
25
|
+
readonly bytes: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UnconfirmedProcessTermination {
|
|
29
|
+
readonly status: "unconfirmed";
|
|
30
|
+
readonly reason: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type BoundedProcessFailure =
|
|
34
|
+
| { readonly kind: "spawn" | "abort" | "timeout" | "max-buffer"; readonly message: string }
|
|
35
|
+
| {
|
|
36
|
+
readonly kind: "exit";
|
|
37
|
+
readonly message: string;
|
|
38
|
+
readonly code: number | null;
|
|
39
|
+
readonly signal: NodeJS.Signals | null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type BoundedProcessResult =
|
|
43
|
+
| (BoundedProcessResultBase & { readonly ok: true; readonly error?: undefined; readonly failure?: undefined })
|
|
44
|
+
| (BoundedProcessResultBase & {
|
|
45
|
+
readonly ok: false;
|
|
46
|
+
/** Backward-compatible display message. Branch on `failure.kind` for control flow. */
|
|
47
|
+
readonly error: string;
|
|
48
|
+
readonly failure: BoundedProcessFailure;
|
|
49
|
+
/** Present only when the runner's final bound elapsed without observing child closure. */
|
|
50
|
+
readonly termination?: UnconfirmedProcessTermination;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export type WindowsTaskkillResult =
|
|
54
|
+
| { readonly ok: true }
|
|
55
|
+
| { readonly ok: false; readonly reason: string };
|
|
56
|
+
|
|
57
|
+
export interface ProcessRunnerDependencies {
|
|
58
|
+
readonly platform: NodeJS.Platform;
|
|
59
|
+
readonly runWindowsTaskkill: (pid: number, complete: (result: WindowsTaskkillResult) => void) => void;
|
|
60
|
+
readonly killChild: (child: ChildProcess, signal: NodeJS.Signals) => boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const NODE_PROCESS_RUNNER_DEPENDENCIES: ProcessRunnerDependencies = {
|
|
64
|
+
platform: process.platform,
|
|
65
|
+
runWindowsTaskkill(pid, complete) {
|
|
66
|
+
let completed = false;
|
|
67
|
+
const finish = (result: WindowsTaskkillResult) => {
|
|
68
|
+
if (completed) return;
|
|
69
|
+
completed = true;
|
|
70
|
+
complete(result);
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
const killer = spawn("taskkill.exe", ["/pid", String(pid), "/t", "/f"], {
|
|
74
|
+
detached: false,
|
|
75
|
+
stdio: "ignore",
|
|
76
|
+
windowsHide: true,
|
|
77
|
+
});
|
|
78
|
+
killer.once("error", (error) => finish({ ok: false, reason: `taskkill failed to start: ${unknownErrorMessage(error)}` }));
|
|
79
|
+
killer.once("close", (code, signal) => {
|
|
80
|
+
if (code === 0) {
|
|
81
|
+
finish({ ok: true });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
finish({
|
|
85
|
+
ok: false,
|
|
86
|
+
reason: `taskkill exited with ${code === null ? `signal ${signal ?? "unknown"}` : `code ${code}`}`,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
killer.unref();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
finish({ ok: false, reason: `taskkill could not be started: ${unknownErrorMessage(error)}` });
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
killChild: (child, signal) => child.kill(signal),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export async function runBoundedProcess(
|
|
98
|
+
options: BoundedProcessOptions,
|
|
99
|
+
dependencies: ProcessRunnerDependencies = NODE_PROCESS_RUNNER_DEPENDENCIES,
|
|
100
|
+
): Promise<BoundedProcessResult> {
|
|
101
|
+
const start = performance.now();
|
|
102
|
+
let stdout = "";
|
|
103
|
+
let stderr = "";
|
|
104
|
+
let bytes = 0;
|
|
105
|
+
let pendingFailure: BoundedProcessFailure | undefined;
|
|
106
|
+
const child = spawn(options.file, [...options.args], {
|
|
107
|
+
cwd: options.cwd,
|
|
108
|
+
env: options.env ?? process.env,
|
|
109
|
+
detached: dependencies.platform !== "win32",
|
|
110
|
+
stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return await new Promise<BoundedProcessResult>((resolve) => {
|
|
114
|
+
let settled = false;
|
|
115
|
+
let forceKillTimer: ReturnType<typeof setTimeout> | undefined;
|
|
116
|
+
let hardSettleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
117
|
+
let directChildKillAttempted = false;
|
|
118
|
+
let windowsTerminationReason = "process close was not observed after taskkill and direct-child fallback";
|
|
119
|
+
const killGraceMs = Math.max(1, options.killGraceMs ?? 100);
|
|
120
|
+
const cleanup = () => {
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
123
|
+
if (hardSettleTimer) clearTimeout(hardSettleTimer);
|
|
124
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
125
|
+
};
|
|
126
|
+
const finish = (failure?: BoundedProcessFailure, termination?: UnconfirmedProcessTermination) => {
|
|
127
|
+
if (settled) return;
|
|
128
|
+
settled = true;
|
|
129
|
+
cleanup();
|
|
130
|
+
const base = { stdout, stderr, durationMs: performance.now() - start, bytes };
|
|
131
|
+
if (!failure) {
|
|
132
|
+
resolve({ ...base, ok: true });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
resolve({ ...base, ok: false, error: failure.message, failure, ...(termination ? { termination } : {}) });
|
|
136
|
+
};
|
|
137
|
+
const killDirectChild = (signal: NodeJS.Signals, reason: string) => {
|
|
138
|
+
if (directChildKillAttempted) return;
|
|
139
|
+
directChildKillAttempted = true;
|
|
140
|
+
windowsTerminationReason = reason;
|
|
141
|
+
try {
|
|
142
|
+
if (!dependencies.killChild(child, signal)) {
|
|
143
|
+
windowsTerminationReason = `${reason}; direct-child termination was not accepted`;
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
windowsTerminationReason = `${reason}; direct-child termination failed: ${unknownErrorMessage(error)}`;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const signalPosixProcess = (signal: NodeJS.Signals) => {
|
|
150
|
+
let groupSignalled = false;
|
|
151
|
+
if (child.pid !== undefined) {
|
|
152
|
+
try {
|
|
153
|
+
process.kill(-child.pid, signal);
|
|
154
|
+
groupSignalled = true;
|
|
155
|
+
} catch {
|
|
156
|
+
// The process may have exited before its descendants released stdio.
|
|
157
|
+
// Fall back to the direct child below.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!groupSignalled) {
|
|
161
|
+
try {
|
|
162
|
+
dependencies.killChild(child, signal);
|
|
163
|
+
} catch {
|
|
164
|
+
// The hard-settlement timer remains authoritative when signalling fails.
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const destroyStdio = () => {
|
|
169
|
+
child.stdin?.destroy();
|
|
170
|
+
child.stdout?.destroy();
|
|
171
|
+
child.stderr?.destroy();
|
|
172
|
+
};
|
|
173
|
+
const terminate = (failure: BoundedProcessFailure) => {
|
|
174
|
+
pendingFailure ??= failure;
|
|
175
|
+
if (forceKillTimer) return;
|
|
176
|
+
if (dependencies.platform === "win32") {
|
|
177
|
+
if (child.pid === undefined) {
|
|
178
|
+
killDirectChild("SIGKILL", "taskkill was unavailable because the child process had no pid");
|
|
179
|
+
} else {
|
|
180
|
+
try {
|
|
181
|
+
dependencies.runWindowsTaskkill(child.pid, (result) => {
|
|
182
|
+
if (settled || result.ok) return;
|
|
183
|
+
killDirectChild("SIGKILL", result.reason);
|
|
184
|
+
});
|
|
185
|
+
} catch (error) {
|
|
186
|
+
killDirectChild("SIGKILL", `taskkill invocation failed: ${unknownErrorMessage(error)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
signalPosixProcess("SIGTERM");
|
|
191
|
+
}
|
|
192
|
+
forceKillTimer = setTimeout(() => {
|
|
193
|
+
if (dependencies.platform === "win32") {
|
|
194
|
+
if (directChildKillAttempted) {
|
|
195
|
+
windowsTerminationReason = `${windowsTerminationReason}; child did not close within the termination grace period`;
|
|
196
|
+
} else {
|
|
197
|
+
killDirectChild("SIGKILL", "taskkill did not close the child within the termination grace period");
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
signalPosixProcess("SIGKILL");
|
|
201
|
+
}
|
|
202
|
+
destroyStdio();
|
|
203
|
+
if (!settled) {
|
|
204
|
+
hardSettleTimer = setTimeout(() => {
|
|
205
|
+
if (!pendingFailure) return;
|
|
206
|
+
finish(
|
|
207
|
+
pendingFailure,
|
|
208
|
+
{ status: "unconfirmed", reason: dependencies.platform === "win32"
|
|
209
|
+
? windowsTerminationReason
|
|
210
|
+
: "process close was not observed after forced process-group termination" },
|
|
211
|
+
);
|
|
212
|
+
}, killGraceMs);
|
|
213
|
+
}
|
|
214
|
+
}, killGraceMs);
|
|
215
|
+
};
|
|
216
|
+
const onAbort = () => terminate({ kind: "abort", message: options.abortError });
|
|
217
|
+
const timeout = setTimeout(() => terminate({ kind: "timeout", message: options.timeoutError }), options.timeoutMs);
|
|
218
|
+
|
|
219
|
+
const captureChunk = (chunk: Buffer, append: (text: string) => void) => {
|
|
220
|
+
if (pendingFailure) return;
|
|
221
|
+
bytes += chunk.length;
|
|
222
|
+
if (options.maxBufferBytes !== undefined && bytes > options.maxBufferBytes) {
|
|
223
|
+
terminate({
|
|
224
|
+
kind: "max-buffer",
|
|
225
|
+
message: options.maxBufferError ?? `process output exceeded ${options.maxBufferBytes} bytes`,
|
|
226
|
+
});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
append(chunk.toString("utf8"));
|
|
230
|
+
};
|
|
231
|
+
child.stdout?.on("data", (chunk: Buffer) => captureChunk(chunk, (text) => (stdout += text)));
|
|
232
|
+
child.stderr?.on("data", (chunk: Buffer) => captureChunk(chunk, (text) => (stderr += text)));
|
|
233
|
+
child.on("error", (spawnError) => {
|
|
234
|
+
if (!pendingFailure) finish({ kind: "spawn", message: spawnError.message });
|
|
235
|
+
});
|
|
236
|
+
child.on("close", (code, signal) => {
|
|
237
|
+
if (pendingFailure) {
|
|
238
|
+
finish(pendingFailure);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (code === 0) {
|
|
242
|
+
finish();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const message = options.exitError(stderr, code, signal);
|
|
246
|
+
finish({ kind: "exit", message, code, signal });
|
|
247
|
+
});
|
|
248
|
+
if (options.signal?.aborted) {
|
|
249
|
+
onAbort();
|
|
250
|
+
} else {
|
|
251
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
252
|
+
}
|
|
253
|
+
if (options.stdin !== undefined) {
|
|
254
|
+
// The child may reject input and exit before consuming it. Its process exit
|
|
255
|
+
// remains the authoritative result; avoid surfacing a secondary EPIPE.
|
|
256
|
+
child.stdin?.on("error", () => undefined);
|
|
257
|
+
child.stdin?.end(options.stdin);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { WorkflowUsageSnapshot } from "./usage.ts";
|
|
2
|
+
|
|
3
|
+
export type AgentRowStatus = "queued" | "running" | "done" | "failed";
|
|
4
|
+
export type AgentChatRole = "task" | "user" | "assistant" | "tool" | "status";
|
|
5
|
+
export type WorkflowLaneItemStatus = "pending" | "running" | "success" | "warning" | "error";
|
|
6
|
+
|
|
7
|
+
export interface AgentChatMessage {
|
|
8
|
+
readonly role: AgentChatRole;
|
|
9
|
+
readonly text: string;
|
|
10
|
+
readonly createdAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AgentRowSnapshot {
|
|
14
|
+
readonly id: number;
|
|
15
|
+
readonly label: string;
|
|
16
|
+
/** provider/id of the model actually routed to this agent. */
|
|
17
|
+
readonly model?: string;
|
|
18
|
+
readonly status: AgentRowStatus;
|
|
19
|
+
readonly startedAt?: number;
|
|
20
|
+
readonly doneAt?: number;
|
|
21
|
+
readonly toolUses: number;
|
|
22
|
+
readonly lastTool?: string;
|
|
23
|
+
readonly error?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PhaseSnapshot {
|
|
27
|
+
readonly title: string;
|
|
28
|
+
readonly agents: readonly AgentRowSnapshot[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface WorkflowCounterSnapshot {
|
|
32
|
+
readonly key: string;
|
|
33
|
+
readonly label: string;
|
|
34
|
+
readonly value: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WorkflowLaneItemSnapshot {
|
|
38
|
+
readonly lane: string;
|
|
39
|
+
readonly title: string;
|
|
40
|
+
readonly subtitle?: string;
|
|
41
|
+
readonly status: WorkflowLaneItemStatus;
|
|
42
|
+
readonly details?: string;
|
|
43
|
+
readonly createdAt: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface WorkflowProgressSnapshot {
|
|
47
|
+
readonly runId: string;
|
|
48
|
+
readonly title: string;
|
|
49
|
+
readonly startedAt: number;
|
|
50
|
+
readonly doneAt?: number;
|
|
51
|
+
readonly currentPhase: string;
|
|
52
|
+
readonly phases: readonly PhaseSnapshot[];
|
|
53
|
+
readonly counters: readonly WorkflowCounterSnapshot[];
|
|
54
|
+
readonly summary: readonly [string, string | number][];
|
|
55
|
+
readonly lanes: readonly [string, readonly WorkflowLaneItemSnapshot[]][];
|
|
56
|
+
readonly laneOverflow: readonly [string, number][];
|
|
57
|
+
readonly logs: readonly string[];
|
|
58
|
+
readonly usage?: WorkflowUsageSnapshot;
|
|
59
|
+
}
|