@wichayutdew/pi-workflows 1.0.0 → 2.0.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/README.md +312 -221
- package/agents/step.md +4 -11
- package/dist/index.js +2556 -961
- package/examples/mr-comments.workflow.yaml +18 -8
- package/examples/prompts/mr-comments/implement.md +13 -9
- package/examples/prompts/mr-comments/plan.md +44 -15
- package/examples/prompts/mr-comments/verify.md +8 -7
- package/examples/settings.yaml +7 -1
- package/examples/starter-kit/mr-comment.workflow.yaml +149 -0
- package/examples/starter-kit/mr-review.workflow.yaml +130 -0
- package/examples/starter-kit/steps/mr-comment/fetch.md +33 -0
- package/examples/starter-kit/steps/mr-comment/implement.md +38 -0
- package/examples/starter-kit/steps/mr-comment/plan.md +62 -0
- package/examples/starter-kit/steps/mr-comment/publish.md +36 -0
- package/examples/starter-kit/steps/mr-comment/verify.md +35 -0
- package/examples/starter-kit/steps/mr-review/fetch.md +25 -0
- package/examples/starter-kit/steps/mr-review/publish.md +30 -0
- package/examples/starter-kit/steps/mr-review/review.md +69 -0
- package/examples/starter-kit/steps/mr-review/verify.md +24 -0
- package/examples/starter-kit/steps/shared/prepare-workspace.md +57 -0
- package/examples/starter-kit/steps/ticket/implement.md +35 -0
- package/examples/starter-kit/steps/ticket/plan.md +40 -0
- package/examples/starter-kit/steps/ticket/verify.md +29 -0
- package/examples/starter-kit/steps/work/implement.md +34 -0
- package/examples/starter-kit/steps/work/plan.md +38 -0
- package/examples/starter-kit/steps/work/verify.md +30 -0
- package/examples/starter-kit/ticket.workflow.yaml +100 -0
- package/examples/starter-kit/work.workflow.yaml +97 -0
- package/package.json +3 -3
- package/schemas/workflow.schema.json +78 -35
- package/src/command-names.ts +1 -0
- package/src/commands.ts +33 -4
- package/src/config/ceiling.ts +6 -8
- package/src/config/load-workflows.ts +12 -1
- package/src/config/step-digests.ts +19 -0
- package/src/config/types.ts +17 -11
- package/src/config/validation/permissions.ts +5 -45
- package/src/config/validation/prompt.ts +3 -0
- package/src/config/validation/shared.ts +0 -2
- package/src/config/validation/step.ts +106 -2
- package/src/config/validation/workflow.ts +76 -0
- package/src/engine/create-run.ts +4 -0
- package/src/engine/gate-transitions.ts +58 -6
- package/src/engine/reconciliation-history.ts +61 -24
- package/src/engine/run-advance.ts +37 -4
- package/src/engine/run-lifecycle.ts +11 -0
- package/src/engine/run-reconciliation.ts +51 -21
- package/src/engine/run-validation.ts +236 -0
- package/src/engine/run-workflow-validation.ts +191 -0
- package/src/engine/state-types.ts +109 -4
- package/src/engine/state.ts +15 -1
- package/src/engine/step-trace.ts +495 -0
- package/src/engine/transitions.ts +1 -0
- package/src/harness/action-context.ts +31 -6
- package/src/harness/core-actions.ts +30 -1
- package/src/harness/delegation-control-actions.ts +27 -2
- package/src/harness/delegation-failure.ts +42 -9
- package/src/harness/delegation-plan.ts +96 -40
- package/src/harness/delegation-recovery-validation.ts +3 -1
- package/src/harness/delegation-response-actions.ts +73 -3
- package/src/harness/delegation-retry-policy.ts +5 -3
- package/src/harness/dependencies.ts +11 -2
- package/src/harness/gate-submission-action.ts +5 -31
- package/src/harness/lifecycle-actions.ts +4 -5
- package/src/harness/pause-actions.ts +10 -1
- package/src/harness/plannotator-result-actions.ts +20 -11
- package/src/harness/prompt-gate-actions.ts +40 -2
- package/src/harness/resume-action.ts +75 -1
- package/src/harness/start-actions.ts +77 -3
- package/src/harness/status-actions.ts +7 -17
- package/src/harness/step-effects.ts +61 -0
- package/src/harness/step-execution-actions.ts +181 -25
- package/src/harness/step-reporting.ts +168 -0
- package/src/harness/types.ts +1 -0
- package/src/harness/workspace-directory.ts +67 -0
- package/src/harness.ts +37 -9
- package/src/integrations/plannotator-requests.ts +40 -5
- package/src/integrations/subagents/child-policy-sections.ts +71 -72
- package/src/integrations/subagents/child-policy-types.ts +7 -7
- package/src/integrations/subagents/child-policy-validation.ts +10 -4
- package/src/integrations/subagents/child-runtime-completion.ts +1 -0
- package/src/integrations/subagents/child-runtime-files.ts +29 -86
- package/src/integrations/subagents/child-runtime-policy.ts +8 -33
- package/src/integrations/subagents/child-runtime.ts +3 -15
- package/src/integrations/subagents/delegated-result.ts +1 -0
- package/src/integrations/subagents/diagnostic-format.ts +5 -0
- package/src/integrations/subagents/diagnostic-types.ts +7 -1
- package/src/integrations/subagents/diagnostics.ts +2 -0
- package/src/integrations/subagents/failure-correlation.ts +52 -2
- package/src/integrations/subagents/failure-transcript.ts +60 -1
- package/src/integrations/subagents/replay-audit.ts +0 -1
- package/src/integrations/subagents/replay-safety.ts +12 -27
- package/src/integrations/subagents/session-diagnostics.ts +102 -3
- package/src/policy/bash-authorization.ts +2 -21
- package/src/policy/bash-types.ts +0 -5
- package/src/policy/bash.ts +1 -2
- package/src/policy/tool-call-authorization.ts +1 -7
- package/src/preflight.ts +0 -5
- package/src/prompt/retry-task.ts +11 -3
- package/src/prompt/step-contract.ts +10 -56
- package/src/prompt/step-sections.ts +8 -10
- package/src/prompt/step-task.ts +41 -10
- package/src/prompt/template.ts +5 -2
- package/src/runtime/completion-tool.ts +14 -0
- package/src/runtime/main-step-completion.ts +3 -0
- package/src/runtime/main-step-policy.ts +2 -1
- package/src/runtime/main-step-runtime-types.ts +9 -1
- package/src/runtime/main-step-runtime.ts +2 -0
- package/src/runtime/main-step-state.ts +6 -0
- package/src/runtime/main-step-trace.ts +45 -0
- package/src/runtime/step-result.ts +57 -0
- package/src/step-log.ts +178 -0
- package/src/workflow-doctor.ts +254 -0
- package/src/workflow-status/format-status.ts +32 -3
- package/src/workflow-status/render-board.ts +7 -4
- package/src/workflow-status/render-path.ts +51 -24
- package/src/workflow-status/render-step-detail.ts +379 -0
- package/src/workflow-status/render-summary.ts +22 -0
- package/src/workflow-status/transcript-reader.ts +231 -0
- package/src/workflow-status/types.ts +8 -0
- package/src/workflow-status/view.ts +161 -14
- package/src/workflow-status.ts +6 -0
- package/src/policy/approved-command-extraction.ts +0 -71
- package/src/policy/approved-commands.ts +0 -15
- package/src/policy/bash-read-only.ts +0 -161
- package/src/policy/restricted-git.ts +0 -30
- package/src/policy/reviewed-artifact.ts +0 -97
- package/src/policy/reviewed-command-safety.ts +0 -150
- package/src/policy/reviewed-command-shape.ts +0 -49
- package/src/policy/reviewed-repository-contract.ts +0 -138
- package/src/policy/reviewed-repository-cwd.ts +0 -102
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
2
|
+
import type { WorkflowStepResult } from '../runtime/step-result.ts';
|
|
3
|
+
import { redactStepLogText } from '../step-log.ts';
|
|
4
|
+
import {
|
|
5
|
+
MAX_STEP_TRACE_ARTIFACT_CHARS,
|
|
6
|
+
MAX_STEP_TRACE_ATTEMPTS,
|
|
7
|
+
MAX_STEP_TRACE_LOG_CHARS,
|
|
8
|
+
MAX_STEP_TRACE_LOG_EVENTS,
|
|
9
|
+
MAX_STEP_TRACE_SUMMARY_CHARS,
|
|
10
|
+
MAX_STEP_TRACE_TASK_CHARS,
|
|
11
|
+
MAX_WORKFLOW_TRACE_CHARS,
|
|
12
|
+
type StepAttemptResult,
|
|
13
|
+
type StepExecutionAttempt,
|
|
14
|
+
type StepGateDecision,
|
|
15
|
+
type SubagentTranscriptReference,
|
|
16
|
+
type WorkflowRun,
|
|
17
|
+
} from './state-types.ts';
|
|
18
|
+
|
|
19
|
+
type CompactedTask = {
|
|
20
|
+
readonly task: string;
|
|
21
|
+
readonly taskTruncated?: true;
|
|
22
|
+
readonly omittedTaskChars?: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const COMPACTED_FIELD_CHARS = 512;
|
|
26
|
+
const COMPACTED_LOG_CHARS = 4_096;
|
|
27
|
+
|
|
28
|
+
type CompactedLog = {
|
|
29
|
+
readonly log?: ReadonlyArray<string>;
|
|
30
|
+
readonly logTruncated?: true;
|
|
31
|
+
readonly omittedLogEvents?: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function logChars(lines: ReadonlyArray<string> | undefined): number {
|
|
35
|
+
return lines?.reduce((total, line) => total + line.length, 0) ?? 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function attemptSize(attempt: StepExecutionAttempt): number {
|
|
39
|
+
return (
|
|
40
|
+
attempt.requestId.length +
|
|
41
|
+
attempt.task.length +
|
|
42
|
+
(attempt.kind === 'subagent' ? attempt.agent.length : 0) +
|
|
43
|
+
(attempt.kind === 'main' ? logChars(attempt.log) : 0) +
|
|
44
|
+
(attempt.result?.outcome.length ?? 0) +
|
|
45
|
+
(attempt.result?.summary.length ?? 0) +
|
|
46
|
+
(attempt.result?.artifact?.length ?? 0) +
|
|
47
|
+
(attempt.result?.workspaceCwd?.length ?? 0) +
|
|
48
|
+
(attempt.kind === 'subagent'
|
|
49
|
+
? (attempt.transcript?.trustedRoot.length ?? 0) +
|
|
50
|
+
(attempt.transcript?.sessionFile.length ?? 0) +
|
|
51
|
+
(attempt.transcript?.runId.length ?? 0)
|
|
52
|
+
: 0) +
|
|
53
|
+
(attempt.gateDecision?.requestId.length ?? 0) +
|
|
54
|
+
(attempt.gateDecision?.feedback.length ?? 0) +
|
|
55
|
+
(attempt.gateDecision?.reviewId?.length ?? 0)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function compactMainLog(attempt: StepExecutionAttempt): CompactedLog {
|
|
60
|
+
if (attempt.kind !== 'main' || !attempt.log) return {};
|
|
61
|
+
const retained: Array<string> = [];
|
|
62
|
+
let chars = 0;
|
|
63
|
+
for (const line of attempt.log) {
|
|
64
|
+
if (chars + line.length > COMPACTED_LOG_CHARS) break;
|
|
65
|
+
retained.push(line);
|
|
66
|
+
chars += line.length;
|
|
67
|
+
}
|
|
68
|
+
const newlyOmitted = attempt.log.length - retained.length;
|
|
69
|
+
const omittedLogEvents = (attempt.omittedLogEvents ?? 0) + newlyOmitted;
|
|
70
|
+
return {
|
|
71
|
+
log: retained,
|
|
72
|
+
...(attempt.logTruncated || omittedLogEvents > 0
|
|
73
|
+
? { logTruncated: true, omittedLogEvents }
|
|
74
|
+
: {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function compactAttempt(attempt: StepExecutionAttempt): StepExecutionAttempt {
|
|
79
|
+
const task = attempt.task.slice(0, COMPACTED_FIELD_CHARS);
|
|
80
|
+
const removedTaskChars = attempt.task.length - task.length;
|
|
81
|
+
const result = attempt.result
|
|
82
|
+
? {
|
|
83
|
+
...attempt.result,
|
|
84
|
+
summary: attempt.result.summary.slice(0, COMPACTED_FIELD_CHARS),
|
|
85
|
+
...(attempt.result.summary.length > COMPACTED_FIELD_CHARS
|
|
86
|
+
? { summaryTruncated: true as const }
|
|
87
|
+
: {}),
|
|
88
|
+
...(attempt.result.artifact !== undefined
|
|
89
|
+
? {
|
|
90
|
+
artifact: attempt.result.artifact.slice(0, COMPACTED_FIELD_CHARS),
|
|
91
|
+
...(attempt.result.artifact.length > COMPACTED_FIELD_CHARS
|
|
92
|
+
? { artifactTruncated: true as const }
|
|
93
|
+
: {}),
|
|
94
|
+
}
|
|
95
|
+
: {}),
|
|
96
|
+
}
|
|
97
|
+
: undefined;
|
|
98
|
+
const gateDecision = attempt.gateDecision
|
|
99
|
+
? {
|
|
100
|
+
...attempt.gateDecision,
|
|
101
|
+
feedback: attempt.gateDecision.feedback.slice(0, COMPACTED_FIELD_CHARS),
|
|
102
|
+
...(attempt.gateDecision.feedback.length > COMPACTED_FIELD_CHARS
|
|
103
|
+
? { feedbackTruncated: true as const }
|
|
104
|
+
: {}),
|
|
105
|
+
}
|
|
106
|
+
: undefined;
|
|
107
|
+
const compactedLog = compactMainLog(attempt);
|
|
108
|
+
return {
|
|
109
|
+
...attempt,
|
|
110
|
+
task,
|
|
111
|
+
...(attempt.taskTruncated || removedTaskChars > 0
|
|
112
|
+
? {
|
|
113
|
+
taskTruncated: true as const,
|
|
114
|
+
omittedTaskChars: (attempt.omittedTaskChars ?? 0) + removedTaskChars,
|
|
115
|
+
}
|
|
116
|
+
: {}),
|
|
117
|
+
...(result ? { result } : {}),
|
|
118
|
+
...(gateDecision ? { gateDecision } : {}),
|
|
119
|
+
...(attempt.kind === 'main' && attempt.log ? compactedLog : {}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Returns the bounded checkpoint payload attributable to step traces. */
|
|
124
|
+
export function workflowTraceChars(run: WorkflowRun): number {
|
|
125
|
+
return [
|
|
126
|
+
...run.history.flatMap((entry) => entry.attempts ?? []),
|
|
127
|
+
...(run.currentStepAttempts ?? []),
|
|
128
|
+
].reduce((total, attempt) => total + attemptSize(attempt), 0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function compactRunTraceBudget(run: WorkflowRun): WorkflowRun {
|
|
132
|
+
let remaining = workflowTraceChars(run);
|
|
133
|
+
if (remaining <= MAX_WORKFLOW_TRACE_CHARS) return run;
|
|
134
|
+
|
|
135
|
+
const compactAttempts = (
|
|
136
|
+
values: ReadonlyArray<StepExecutionAttempt>,
|
|
137
|
+
): Array<StepExecutionAttempt> => {
|
|
138
|
+
const attempts = [...values];
|
|
139
|
+
for (let index = 0; index < attempts.length; index += 1) {
|
|
140
|
+
if (remaining <= MAX_WORKFLOW_TRACE_CHARS) break;
|
|
141
|
+
const attempt = attempts[index];
|
|
142
|
+
if (!attempt) continue;
|
|
143
|
+
const compacted = compactAttempt(attempt);
|
|
144
|
+
remaining -= attemptSize(attempt) - attemptSize(compacted);
|
|
145
|
+
attempts[index] = compacted;
|
|
146
|
+
}
|
|
147
|
+
return attempts;
|
|
148
|
+
};
|
|
149
|
+
const history = run.history.map((entry) =>
|
|
150
|
+
entry.attempts
|
|
151
|
+
? { ...entry, attempts: compactAttempts(entry.attempts) }
|
|
152
|
+
: entry,
|
|
153
|
+
);
|
|
154
|
+
const currentStepAttempts = [...(run.currentStepAttempts ?? [])];
|
|
155
|
+
const compactedCurrent = compactAttempts(currentStepAttempts);
|
|
156
|
+
for (let index = 0; index < history.length; index += 1) {
|
|
157
|
+
if (remaining <= MAX_WORKFLOW_TRACE_CHARS) break;
|
|
158
|
+
const entry = history[index];
|
|
159
|
+
if (!entry?.attempts || entry.attempts.length <= 1) continue;
|
|
160
|
+
const attempts = [...entry.attempts];
|
|
161
|
+
let omittedAttempts = entry.omittedAttempts ?? 0;
|
|
162
|
+
while (remaining > MAX_WORKFLOW_TRACE_CHARS && attempts.length > 2) {
|
|
163
|
+
const removed = attempts.splice(1, 1)[0];
|
|
164
|
+
if (removed) remaining -= attemptSize(removed);
|
|
165
|
+
omittedAttempts += 1;
|
|
166
|
+
}
|
|
167
|
+
history[index] = { ...entry, attempts, omittedAttempts };
|
|
168
|
+
}
|
|
169
|
+
let currentOmittedAttempts = run.currentStepOmittedAttempts ?? 0;
|
|
170
|
+
while (remaining > MAX_WORKFLOW_TRACE_CHARS && compactedCurrent.length > 2) {
|
|
171
|
+
const removed = compactedCurrent.splice(1, 1)[0];
|
|
172
|
+
if (removed) remaining -= attemptSize(removed);
|
|
173
|
+
currentOmittedAttempts += 1;
|
|
174
|
+
}
|
|
175
|
+
for (let index = 0; index < history.length; index += 1) {
|
|
176
|
+
if (remaining <= MAX_WORKFLOW_TRACE_CHARS) break;
|
|
177
|
+
const entry = history[index];
|
|
178
|
+
if (!entry?.attempts || entry.attempts.length === 0) continue;
|
|
179
|
+
const attempts = [...entry.attempts];
|
|
180
|
+
let omittedAttempts = entry.omittedAttempts ?? 0;
|
|
181
|
+
while (remaining > MAX_WORKFLOW_TRACE_CHARS && attempts.length > 0) {
|
|
182
|
+
const removed = attempts.shift();
|
|
183
|
+
if (removed) remaining -= attemptSize(removed);
|
|
184
|
+
omittedAttempts += 1;
|
|
185
|
+
}
|
|
186
|
+
history[index] = { ...entry, attempts, omittedAttempts };
|
|
187
|
+
}
|
|
188
|
+
while (remaining > MAX_WORKFLOW_TRACE_CHARS && compactedCurrent.length > 0) {
|
|
189
|
+
const removed = compactedCurrent.shift();
|
|
190
|
+
if (removed) remaining -= attemptSize(removed);
|
|
191
|
+
currentOmittedAttempts += 1;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
...run,
|
|
196
|
+
history,
|
|
197
|
+
...(run.currentStepAttempts
|
|
198
|
+
? { currentStepAttempts: compactedCurrent }
|
|
199
|
+
: {}),
|
|
200
|
+
...(currentOmittedAttempts > 0
|
|
201
|
+
? { currentStepOmittedAttempts: currentOmittedAttempts }
|
|
202
|
+
: {}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function compactTask(task: string): CompactedTask {
|
|
207
|
+
if (task.length <= MAX_STEP_TRACE_TASK_CHARS) return { task };
|
|
208
|
+
return {
|
|
209
|
+
task: task.slice(0, MAX_STEP_TRACE_TASK_CHARS),
|
|
210
|
+
taskTruncated: true,
|
|
211
|
+
omittedTaskChars: task.length - MAX_STEP_TRACE_TASK_CHARS,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function appendAttempt(
|
|
216
|
+
run: WorkflowRun,
|
|
217
|
+
attempt: StepExecutionAttempt,
|
|
218
|
+
now: number,
|
|
219
|
+
): WorkflowRun {
|
|
220
|
+
if (
|
|
221
|
+
run.currentStepAttempts?.some(
|
|
222
|
+
(candidate) => candidate.requestId === attempt.requestId,
|
|
223
|
+
)
|
|
224
|
+
) {
|
|
225
|
+
return run;
|
|
226
|
+
}
|
|
227
|
+
const existing = run.currentStepAttempts ?? [];
|
|
228
|
+
const isFull = existing.length >= MAX_STEP_TRACE_ATTEMPTS;
|
|
229
|
+
const retained = isFull
|
|
230
|
+
? [existing[0], ...existing.slice(-(MAX_STEP_TRACE_ATTEMPTS - 2))].filter(
|
|
231
|
+
(candidate): candidate is StepExecutionAttempt =>
|
|
232
|
+
candidate !== undefined,
|
|
233
|
+
)
|
|
234
|
+
: existing;
|
|
235
|
+
const omittedAttempts =
|
|
236
|
+
(run.currentStepOmittedAttempts ?? 0) + (isFull ? 1 : 0);
|
|
237
|
+
return compactRunTraceBudget({
|
|
238
|
+
...run,
|
|
239
|
+
currentStepAttempts: [...retained, attempt],
|
|
240
|
+
...(omittedAttempts > 0
|
|
241
|
+
? { currentStepOmittedAttempts: omittedAttempts }
|
|
242
|
+
: {}),
|
|
243
|
+
updatedAt: now,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function nextAttemptOrdinal(run: WorkflowRun): number {
|
|
248
|
+
const attempts = run.currentStepAttempts ?? [];
|
|
249
|
+
const largestRecordedOrdinal = attempts.reduce(
|
|
250
|
+
(largest, attempt) => Math.max(largest, attempt.ordinal ?? 0),
|
|
251
|
+
0,
|
|
252
|
+
);
|
|
253
|
+
const attemptedCount =
|
|
254
|
+
attempts.length + (run.currentStepOmittedAttempts ?? 0);
|
|
255
|
+
return Math.max(largestRecordedOrdinal, attemptedCount) + 1;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Records the exact task supplied for one main-agent attempt. */
|
|
259
|
+
export function beginMainStepAttempt(
|
|
260
|
+
run: WorkflowRun,
|
|
261
|
+
requestId: string,
|
|
262
|
+
task: string,
|
|
263
|
+
now: number,
|
|
264
|
+
): WorkflowRun {
|
|
265
|
+
if (!requestId || requestId.includes('\0') || !task.trim()) return run;
|
|
266
|
+
const compacted = compactTask(task);
|
|
267
|
+
return appendAttempt(
|
|
268
|
+
run,
|
|
269
|
+
{
|
|
270
|
+
kind: 'main',
|
|
271
|
+
requestId,
|
|
272
|
+
ordinal: nextAttemptOrdinal(run),
|
|
273
|
+
...compacted,
|
|
274
|
+
startedAt: now,
|
|
275
|
+
},
|
|
276
|
+
now,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Records the exact task body supplied for one child attempt. */
|
|
281
|
+
export function beginSubagentStepAttempt(
|
|
282
|
+
run: WorkflowRun,
|
|
283
|
+
requestId: string,
|
|
284
|
+
agent: string,
|
|
285
|
+
task: string,
|
|
286
|
+
now: number,
|
|
287
|
+
): WorkflowRun {
|
|
288
|
+
if (!requestId || requestId.includes('\0') || !agent || !task.trim()) {
|
|
289
|
+
return run;
|
|
290
|
+
}
|
|
291
|
+
const compacted = compactTask(task);
|
|
292
|
+
return appendAttempt(
|
|
293
|
+
run,
|
|
294
|
+
{
|
|
295
|
+
kind: 'subagent',
|
|
296
|
+
requestId,
|
|
297
|
+
ordinal: nextAttemptOrdinal(run),
|
|
298
|
+
agent,
|
|
299
|
+
...compacted,
|
|
300
|
+
startedAt: now,
|
|
301
|
+
},
|
|
302
|
+
now,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Appends a redacted, bounded prefix of main-agent events to one exact attempt. */
|
|
307
|
+
export function appendMainStepLog(
|
|
308
|
+
run: WorkflowRun,
|
|
309
|
+
requestId: string,
|
|
310
|
+
lines: ReadonlyArray<string>,
|
|
311
|
+
now: number,
|
|
312
|
+
): WorkflowRun {
|
|
313
|
+
if (!requestId || requestId.includes('\0') || lines.length === 0) return run;
|
|
314
|
+
const attempts = run.currentStepAttempts;
|
|
315
|
+
if (!attempts || attempts.length === 0) return run;
|
|
316
|
+
const index = attempts.length - 1;
|
|
317
|
+
const attempt = attempts[index];
|
|
318
|
+
if (!attempt || attempt.kind !== 'main' || attempt.requestId !== requestId) {
|
|
319
|
+
return run;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const safeLines = lines
|
|
323
|
+
.map(redactStepLogText)
|
|
324
|
+
.filter((line) => line.length > 0);
|
|
325
|
+
if (safeLines.length === 0) return run;
|
|
326
|
+
|
|
327
|
+
const existing = [...(attempt.log ?? [])];
|
|
328
|
+
let chars = logChars(existing);
|
|
329
|
+
let accepted = 0;
|
|
330
|
+
if (!attempt.logTruncated) {
|
|
331
|
+
for (const line of safeLines) {
|
|
332
|
+
if (
|
|
333
|
+
existing.length >= MAX_STEP_TRACE_LOG_EVENTS ||
|
|
334
|
+
chars + line.length > MAX_STEP_TRACE_LOG_CHARS
|
|
335
|
+
) {
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
existing.push(line);
|
|
339
|
+
chars += line.length;
|
|
340
|
+
accepted += 1;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const newlyOmitted = safeLines.length - accepted;
|
|
344
|
+
const omittedLogEvents = (attempt.omittedLogEvents ?? 0) + newlyOmitted;
|
|
345
|
+
const currentStepAttempts = [...attempts];
|
|
346
|
+
currentStepAttempts[index] = {
|
|
347
|
+
...attempt,
|
|
348
|
+
log: existing,
|
|
349
|
+
...(attempt.logTruncated || omittedLogEvents > 0
|
|
350
|
+
? { logTruncated: true, omittedLogEvents }
|
|
351
|
+
: {}),
|
|
352
|
+
};
|
|
353
|
+
return compactRunTraceBudget({
|
|
354
|
+
...run,
|
|
355
|
+
currentStepAttempts,
|
|
356
|
+
updatedAt: now,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function isSafeTranscriptReference(
|
|
361
|
+
reference: SubagentTranscriptReference,
|
|
362
|
+
): boolean {
|
|
363
|
+
if (
|
|
364
|
+
!isAbsolute(reference.trustedRoot) ||
|
|
365
|
+
!isAbsolute(reference.sessionFile) ||
|
|
366
|
+
reference.trustedRoot.includes('\0') ||
|
|
367
|
+
reference.sessionFile.includes('\0') ||
|
|
368
|
+
!reference.runId ||
|
|
369
|
+
reference.runId.includes('\0') ||
|
|
370
|
+
reference.runId.includes('/') ||
|
|
371
|
+
reference.runId.includes('\\') ||
|
|
372
|
+
reference.runId === '.' ||
|
|
373
|
+
reference.runId === '..' ||
|
|
374
|
+
!Number.isSafeInteger(reference.childIndex) ||
|
|
375
|
+
reference.childIndex < 0
|
|
376
|
+
) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
const expected = resolve(
|
|
380
|
+
reference.trustedRoot,
|
|
381
|
+
reference.runId,
|
|
382
|
+
`run-${reference.childIndex}`,
|
|
383
|
+
'session.jsonl',
|
|
384
|
+
);
|
|
385
|
+
const relativePath = relative(
|
|
386
|
+
resolve(reference.trustedRoot),
|
|
387
|
+
resolve(reference.sessionFile),
|
|
388
|
+
);
|
|
389
|
+
const isWithinTrustedRoot =
|
|
390
|
+
relativePath !== '' &&
|
|
391
|
+
relativePath !== '..' &&
|
|
392
|
+
!relativePath.startsWith(`..${sep}`) &&
|
|
393
|
+
!isAbsolute(relativePath);
|
|
394
|
+
return isWithinTrustedRoot && resolve(reference.sessionFile) === expected;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Attaches a lexically confined child transcript reference to its attempt.
|
|
399
|
+
*
|
|
400
|
+
* The status reader repeats this check and then verifies real paths, file type,
|
|
401
|
+
* no-follow opening, and a stable bounded read before displaying anything.
|
|
402
|
+
*/
|
|
403
|
+
export function attachSubagentTranscript(
|
|
404
|
+
run: WorkflowRun,
|
|
405
|
+
requestId: string,
|
|
406
|
+
reference: SubagentTranscriptReference,
|
|
407
|
+
now: number,
|
|
408
|
+
): WorkflowRun {
|
|
409
|
+
if (!isSafeTranscriptReference(reference)) return run;
|
|
410
|
+
const attempts = run.currentStepAttempts;
|
|
411
|
+
const index = attempts?.findIndex(
|
|
412
|
+
(attempt) => attempt.requestId === requestId && attempt.kind === 'subagent',
|
|
413
|
+
);
|
|
414
|
+
if (index === undefined || index < 0 || !attempts) return run;
|
|
415
|
+
const currentStepAttempts = [...attempts];
|
|
416
|
+
const attempt = currentStepAttempts[index];
|
|
417
|
+
if (!attempt || attempt.kind !== 'subagent') return run;
|
|
418
|
+
currentStepAttempts[index] = { ...attempt, transcript: reference };
|
|
419
|
+
return compactRunTraceBudget({ ...run, currentStepAttempts, updatedAt: now });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function attemptResult(
|
|
423
|
+
result: WorkflowStepResult,
|
|
424
|
+
workspaceCwd?: string,
|
|
425
|
+
): StepAttemptResult {
|
|
426
|
+
const summary = result.summary.slice(0, MAX_STEP_TRACE_SUMMARY_CHARS);
|
|
427
|
+
const artifact = result.artifact?.slice(0, MAX_STEP_TRACE_ARTIFACT_CHARS);
|
|
428
|
+
return {
|
|
429
|
+
outcome: result.outcome,
|
|
430
|
+
summary,
|
|
431
|
+
...(summary.length < result.summary.length
|
|
432
|
+
? { summaryTruncated: true as const }
|
|
433
|
+
: {}),
|
|
434
|
+
...(artifact !== undefined ? { artifact } : {}),
|
|
435
|
+
...(artifact !== undefined &&
|
|
436
|
+
artifact.length < (result.artifact?.length ?? 0)
|
|
437
|
+
? { artifactTruncated: true as const }
|
|
438
|
+
: {}),
|
|
439
|
+
...(workspaceCwd ? { workspaceCwd } : {}),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Stores the submitted result on the latest attempt before transition. */
|
|
444
|
+
export function recordCurrentStepResult(
|
|
445
|
+
run: WorkflowRun,
|
|
446
|
+
result: WorkflowStepResult,
|
|
447
|
+
now: number,
|
|
448
|
+
workspaceCwd?: string,
|
|
449
|
+
): WorkflowRun {
|
|
450
|
+
const attempts = run.currentStepAttempts;
|
|
451
|
+
if (!attempts || attempts.length === 0) return run;
|
|
452
|
+
const index = attempts.length - 1;
|
|
453
|
+
const latest = attempts[index];
|
|
454
|
+
if (!latest) return run;
|
|
455
|
+
const currentStepAttempts = [...attempts];
|
|
456
|
+
currentStepAttempts[index] = {
|
|
457
|
+
...latest,
|
|
458
|
+
result: attemptResult(result, workspaceCwd),
|
|
459
|
+
};
|
|
460
|
+
return compactRunTraceBudget({
|
|
461
|
+
...run,
|
|
462
|
+
currentStepAttempts,
|
|
463
|
+
updatedAt: now,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Durably pairs a human gate decision with the attempt it reviewed. */
|
|
468
|
+
export function recordCurrentGateDecision(
|
|
469
|
+
run: WorkflowRun,
|
|
470
|
+
decision: StepGateDecision,
|
|
471
|
+
now: number,
|
|
472
|
+
): WorkflowRun {
|
|
473
|
+
const attempts = run.currentStepAttempts;
|
|
474
|
+
if (!attempts || attempts.length === 0) return run;
|
|
475
|
+
const index = attempts.length - 1;
|
|
476
|
+
const latest = attempts[index];
|
|
477
|
+
if (!latest) return run;
|
|
478
|
+
const feedback = decision.feedback.slice(0, MAX_STEP_TRACE_SUMMARY_CHARS);
|
|
479
|
+
const currentStepAttempts = [...attempts];
|
|
480
|
+
currentStepAttempts[index] = {
|
|
481
|
+
...latest,
|
|
482
|
+
gateDecision: {
|
|
483
|
+
...decision,
|
|
484
|
+
feedback,
|
|
485
|
+
...(feedback.length < decision.feedback.length
|
|
486
|
+
? { feedbackTruncated: true }
|
|
487
|
+
: {}),
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
return compactRunTraceBudget({
|
|
491
|
+
...run,
|
|
492
|
+
currentStepAttempts,
|
|
493
|
+
updatedAt: now,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
@@ -22,6 +22,7 @@ import type { WorkflowStepResult } from '../runtime/step-result.ts';
|
|
|
22
22
|
import type { WorkflowStatusSnapshot } from '../workflow-status.ts';
|
|
23
23
|
import type { DelegationFailureActions } from './delegation-failure.ts';
|
|
24
24
|
import type { WorkflowHarnessDependencies } from './dependencies.ts';
|
|
25
|
+
import type { SettledStepReport } from './step-reporting.ts';
|
|
25
26
|
import type {
|
|
26
27
|
ActiveDelegation,
|
|
27
28
|
ActivePromptReview,
|
|
@@ -61,6 +62,10 @@ export type HarnessActionContext = {
|
|
|
61
62
|
legacyProgressWidgetContext: ExtensionContext | undefined;
|
|
62
63
|
workflowIds: () => Array<string>;
|
|
63
64
|
list: (context: ExtensionCommandContext) => Promise<void>;
|
|
65
|
+
doctor: (
|
|
66
|
+
workflowId: string,
|
|
67
|
+
context: ExtensionCommandContext,
|
|
68
|
+
) => Promise<void>;
|
|
64
69
|
start: (
|
|
65
70
|
workflowId: string,
|
|
66
71
|
input: string,
|
|
@@ -74,8 +79,11 @@ export type HarnessActionContext = {
|
|
|
74
79
|
) => Promise<void>;
|
|
75
80
|
pause: (reason: string, context: ExtensionCommandContext) => Promise<void>;
|
|
76
81
|
pauseNow: (reason: string, context: ExtensionCommandContext) => Promise<void>;
|
|
77
|
-
resume: (context: ExtensionCommandContext) => Promise<void>;
|
|
78
|
-
resumeNow: (
|
|
82
|
+
resume: (input: string, context: ExtensionCommandContext) => Promise<void>;
|
|
83
|
+
resumeNow: (
|
|
84
|
+
context: ExtensionCommandContext,
|
|
85
|
+
input?: string,
|
|
86
|
+
) => Promise<void>;
|
|
79
87
|
abort: (reason: string, context: ExtensionCommandContext) => Promise<void>;
|
|
80
88
|
abortNow: (reason: string, context: ExtensionCommandContext) => Promise<void>;
|
|
81
89
|
reload: (context: ExtensionCommandContext) => Promise<void>;
|
|
@@ -93,6 +101,16 @@ export type HarnessActionContext = {
|
|
|
93
101
|
run: WorkflowRun,
|
|
94
102
|
step: WorkflowStep,
|
|
95
103
|
) => void;
|
|
104
|
+
queueMainStepLog: (
|
|
105
|
+
identity: MainStepIdentity,
|
|
106
|
+
lines: ReadonlyArray<string>,
|
|
107
|
+
context: ExtensionContext,
|
|
108
|
+
) => Promise<void>;
|
|
109
|
+
recordMainStepLog: (
|
|
110
|
+
identity: MainStepIdentity,
|
|
111
|
+
lines: ReadonlyArray<string>,
|
|
112
|
+
context: ExtensionContext,
|
|
113
|
+
) => Promise<void>;
|
|
96
114
|
queueMainStepResult: (
|
|
97
115
|
identity: MainStepIdentity,
|
|
98
116
|
result: WorkflowStepResult | undefined,
|
|
@@ -123,8 +141,12 @@ export type HarnessActionContext = {
|
|
|
123
141
|
failure: DelegationFailureDetails | undefined,
|
|
124
142
|
reason: string,
|
|
125
143
|
) => boolean;
|
|
126
|
-
pauseForDelegationFailure: (reason: string) => void;
|
|
127
|
-
pauseForExecutionFailure: (
|
|
144
|
+
pauseForDelegationFailure: (reason: string, failureSummary?: string) => void;
|
|
145
|
+
pauseForExecutionFailure: (
|
|
146
|
+
label: string,
|
|
147
|
+
reason: string,
|
|
148
|
+
failureSummary?: string,
|
|
149
|
+
) => void;
|
|
128
150
|
retainUnconfirmedDelegation: (
|
|
129
151
|
active: ActiveDelegation,
|
|
130
152
|
reason: string,
|
|
@@ -134,6 +156,7 @@ export type HarnessActionContext = {
|
|
|
134
156
|
workflow: LoadedWorkflow,
|
|
135
157
|
originalRun: WorkflowRun,
|
|
136
158
|
outcome: string,
|
|
159
|
+
summary: string,
|
|
137
160
|
artifact: string,
|
|
138
161
|
) => Promise<void>;
|
|
139
162
|
launchPromptReview: (
|
|
@@ -157,7 +180,10 @@ export type HarnessActionContext = {
|
|
|
157
180
|
cancelPromptReview: () => void;
|
|
158
181
|
registerPlannotatorResults: () => void;
|
|
159
182
|
handlePlannotatorResult: (data: unknown) => Promise<void>;
|
|
160
|
-
settleAfterTransition: (
|
|
183
|
+
settleAfterTransition: (
|
|
184
|
+
workflow: LoadedWorkflow,
|
|
185
|
+
report: SettledStepReport,
|
|
186
|
+
) => void;
|
|
161
187
|
preflight: (workflow: LoadedWorkflow, stepId: string) => Array<string>;
|
|
162
188
|
isolateMainSessionTools: () => void;
|
|
163
189
|
restoreBaselineTools: () => void;
|
|
@@ -176,6 +202,5 @@ export type HarnessActionContext = {
|
|
|
176
202
|
refreshStatusWhileRunning: () => void;
|
|
177
203
|
stopStatusRefresh: () => void;
|
|
178
204
|
registerWorkflowStatusShortcut: () => void;
|
|
179
|
-
openWorkflowStatus: (context: ExtensionContext) => void;
|
|
180
205
|
showWorkflowStatus: (context: ExtensionContext) => Promise<void>;
|
|
181
206
|
};
|
|
@@ -7,6 +7,14 @@ import { failRun, pauseRun } from '../engine/transitions.ts';
|
|
|
7
7
|
import { preflightStep } from '../preflight.ts';
|
|
8
8
|
import type { HarnessActionContext as FullHarnessActionContext } from './action-context.ts';
|
|
9
9
|
import { formatCatalogDiagnostics } from './catalog.ts';
|
|
10
|
+
import {
|
|
11
|
+
conciseStepFailureSummary,
|
|
12
|
+
conciseStepPauseSummary,
|
|
13
|
+
reportFailedStep,
|
|
14
|
+
reportPausedStep,
|
|
15
|
+
reportSettledStep,
|
|
16
|
+
type SettledStepReport,
|
|
17
|
+
} from './step-reporting.ts';
|
|
10
18
|
|
|
11
19
|
const STATE_ENTRY_TYPE = 'pi-workflows-state-v1';
|
|
12
20
|
|
|
@@ -39,6 +47,7 @@ export type CoreActions = {
|
|
|
39
47
|
settleAfterTransition: (
|
|
40
48
|
this: HarnessActionContext,
|
|
41
49
|
workflow: LoadedWorkflow,
|
|
50
|
+
report: SettledStepReport,
|
|
42
51
|
) => void;
|
|
43
52
|
preflight: (
|
|
44
53
|
this: HarnessActionContext,
|
|
@@ -71,6 +80,7 @@ export type CoreActions = {
|
|
|
71
80
|
function settleAfterTransition(
|
|
72
81
|
this: HarnessActionContext,
|
|
73
82
|
workflow: LoadedWorkflow,
|
|
83
|
+
report: SettledStepReport,
|
|
74
84
|
): void {
|
|
75
85
|
if (!this.run) return;
|
|
76
86
|
if (this.run.status === 'running') {
|
|
@@ -85,6 +95,14 @@ function settleAfterTransition(
|
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
this.persist();
|
|
98
|
+
reportSettledStep(this.pi, workflow, this.run, report);
|
|
99
|
+
if (
|
|
100
|
+
this.run.status === 'paused' &&
|
|
101
|
+
this.run.failedStepId === this.run.currentStepId &&
|
|
102
|
+
this.run.pauseReason
|
|
103
|
+
) {
|
|
104
|
+
reportFailedStep(this.pi, workflow, this.run, this.run.pauseReason);
|
|
105
|
+
}
|
|
88
106
|
if (this.run.status !== 'running') {
|
|
89
107
|
this.restoreBaselineTools();
|
|
90
108
|
this.updateStatus();
|
|
@@ -94,8 +112,13 @@ function settleAfterTransition(
|
|
|
94
112
|
'info',
|
|
95
113
|
);
|
|
96
114
|
} else if (this.run.status === 'paused') {
|
|
115
|
+
const reason = this.run.pauseReason ?? 'manual action required';
|
|
97
116
|
this.latestContext?.ui.notify(
|
|
98
|
-
`Workflow paused: ${
|
|
117
|
+
`Workflow paused: ${
|
|
118
|
+
this.run.failedStepId
|
|
119
|
+
? conciseStepFailureSummary(reason)
|
|
120
|
+
: conciseStepPauseSummary(reason)
|
|
121
|
+
}`,
|
|
99
122
|
'warning',
|
|
100
123
|
);
|
|
101
124
|
}
|
|
@@ -195,6 +218,12 @@ function restoreFromSession(
|
|
|
195
218
|
this.dependencies.now(),
|
|
196
219
|
);
|
|
197
220
|
this.persist();
|
|
221
|
+
reportPausedStep(
|
|
222
|
+
this.pi,
|
|
223
|
+
this.catalog.workflows.get(this.run.workflowId),
|
|
224
|
+
this.run,
|
|
225
|
+
this.run.pauseReason ?? 'Session was restored',
|
|
226
|
+
);
|
|
198
227
|
}
|
|
199
228
|
if (!this.run && previousBaseline) {
|
|
200
229
|
this.pi.setActiveTools([...previousBaseline]);
|