@wichayutdew/pi-workflows 1.0.1 → 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 -223
- package/agents/step.md +4 -11
- package/dist/index.js +2554 -976
- package/examples/mr-comments.workflow.yaml +16 -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 -44
- package/src/command-names.ts +1 -0
- package/src/commands.ts +33 -4
- package/src/config/ceiling.ts +6 -12
- package/src/config/load-workflows.ts +12 -1
- package/src/config/step-digests.ts +19 -0
- package/src/config/types.ts +17 -22
- package/src/config/validation/permissions.ts +5 -66
- package/src/config/validation/prompt.ts +1 -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 +3 -0
- package/src/engine/gate-transitions.ts +58 -9
- 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 +235 -0
- package/src/engine/run-workflow-validation.ts +191 -0
- package/src/engine/state-types.ts +108 -5
- 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 -84
- 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 +3 -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
|
@@ -1,13 +1,105 @@
|
|
|
1
1
|
export const RUN_STATE_VERSION = 1 as const;
|
|
2
|
+
export const MAX_RESUME_INPUT_CHARS = 16_000;
|
|
3
|
+
export const MAX_STEP_TRACE_TASK_CHARS = 64_000;
|
|
4
|
+
export const MAX_STEP_TRACE_ATTEMPTS = 16;
|
|
5
|
+
export const MAX_STEP_TRACE_SUMMARY_CHARS = 8_000;
|
|
6
|
+
export const MAX_STEP_TRACE_ARTIFACT_CHARS = 16_000;
|
|
7
|
+
export const MAX_STEP_TRACE_LOG_EVENTS = 400;
|
|
8
|
+
export const MAX_STEP_TRACE_LOG_EVENT_CHARS = 2_000;
|
|
9
|
+
export const MAX_STEP_TRACE_LOG_CHARS = 120_000;
|
|
10
|
+
export const MAX_WORKFLOW_TRACE_CHARS = 2_000_000;
|
|
2
11
|
|
|
3
12
|
export type WorkflowRunStatus =
|
|
4
13
|
'running' | 'paused' | 'awaiting-gate' | 'completed' | 'aborted';
|
|
5
14
|
|
|
15
|
+
export type GateApproval = {
|
|
16
|
+
/** Stable identity of the gate request that produced this approval. */
|
|
17
|
+
readonly requestId: string;
|
|
18
|
+
readonly artifact: string;
|
|
19
|
+
readonly feedback: string;
|
|
20
|
+
/** Prompt-excluded step structure approved by this gate request. */
|
|
21
|
+
readonly stepStructuralDigest: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type StepAttemptResult = {
|
|
25
|
+
readonly outcome: string;
|
|
26
|
+
readonly summary: string;
|
|
27
|
+
readonly summaryTruncated?: true | undefined;
|
|
28
|
+
readonly artifact?: string | undefined;
|
|
29
|
+
readonly artifactTruncated?: true | undefined;
|
|
30
|
+
readonly workspaceCwd?: string | undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type StepGateDecision = {
|
|
34
|
+
readonly provider: 'prompt' | 'plannotator';
|
|
35
|
+
readonly requestId: string;
|
|
36
|
+
readonly approved: boolean;
|
|
37
|
+
readonly feedback: string;
|
|
38
|
+
readonly feedbackTruncated?: true | undefined;
|
|
39
|
+
readonly resolvedAt: number;
|
|
40
|
+
readonly reviewId?: string | undefined;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type SubagentTranscriptReference = {
|
|
44
|
+
/** Trusted parent-derived directory containing this child run. */
|
|
45
|
+
readonly trustedRoot: string;
|
|
46
|
+
/** Exact child session returned by the delegation protocol. */
|
|
47
|
+
readonly sessionFile: string;
|
|
48
|
+
/** Pi-subagents run identity used to confine the session path. */
|
|
49
|
+
readonly runId: string;
|
|
50
|
+
readonly childIndex: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type StepExecutionAttempt =
|
|
54
|
+
| {
|
|
55
|
+
readonly kind: 'main';
|
|
56
|
+
readonly requestId: string;
|
|
57
|
+
/** One-based attempt position within this step visit. */
|
|
58
|
+
readonly ordinal?: number | undefined;
|
|
59
|
+
/** Exact workflow task sent to the main agent. */
|
|
60
|
+
readonly task: string;
|
|
61
|
+
readonly taskTruncated?: true | undefined;
|
|
62
|
+
readonly omittedTaskChars?: number | undefined;
|
|
63
|
+
/** Redacted assistant/tool events retained from the start of this attempt. */
|
|
64
|
+
readonly log?: ReadonlyArray<string> | undefined;
|
|
65
|
+
/** Whether later events were omitted from the bounded prefix. */
|
|
66
|
+
readonly logTruncated?: true | undefined;
|
|
67
|
+
readonly omittedLogEvents?: number | undefined;
|
|
68
|
+
readonly startedAt: number;
|
|
69
|
+
readonly result?: StepAttemptResult | undefined;
|
|
70
|
+
readonly gateDecision?: StepGateDecision | undefined;
|
|
71
|
+
}
|
|
72
|
+
| {
|
|
73
|
+
readonly kind: 'subagent';
|
|
74
|
+
readonly requestId: string;
|
|
75
|
+
/** One-based attempt position within this step visit. */
|
|
76
|
+
readonly ordinal?: number | undefined;
|
|
77
|
+
readonly agent: string;
|
|
78
|
+
/** Exact task body sent after the private child-policy envelope. */
|
|
79
|
+
readonly task: string;
|
|
80
|
+
readonly taskTruncated?: true | undefined;
|
|
81
|
+
readonly omittedTaskChars?: number | undefined;
|
|
82
|
+
readonly startedAt: number;
|
|
83
|
+
readonly transcript?: SubagentTranscriptReference | undefined;
|
|
84
|
+
readonly result?: StepAttemptResult | undefined;
|
|
85
|
+
readonly gateDecision?: StepGateDecision | undefined;
|
|
86
|
+
};
|
|
87
|
+
|
|
6
88
|
export type StepHistoryEntry = {
|
|
7
89
|
readonly stepId: string;
|
|
8
90
|
readonly stepDigest: string;
|
|
9
91
|
readonly outcome: string;
|
|
10
92
|
readonly summary: string;
|
|
93
|
+
/** Canonical execution directory selected by this completed step. */
|
|
94
|
+
readonly workspaceCwd?: string | undefined;
|
|
95
|
+
/** Artifact projection paired exactly with `approval` on approved gates. */
|
|
96
|
+
readonly artifact?: string | undefined;
|
|
97
|
+
/** Complete approval provenance for a newly completed gate step. */
|
|
98
|
+
readonly approval?: GateApproval | undefined;
|
|
99
|
+
/** Durable execution evidence for every attempt of this completed step. */
|
|
100
|
+
readonly attempts?: ReadonlyArray<StepExecutionAttempt> | undefined;
|
|
101
|
+
/** Older attempts compacted to keep the checkpoint bounded. */
|
|
102
|
+
readonly omittedAttempts?: number | undefined;
|
|
11
103
|
readonly completedAt: number;
|
|
12
104
|
};
|
|
13
105
|
|
|
@@ -22,8 +114,8 @@ export type PendingGate = {
|
|
|
22
114
|
readonly requestId: string;
|
|
23
115
|
readonly stepId: string;
|
|
24
116
|
readonly artifact: string;
|
|
25
|
-
/**
|
|
26
|
-
summary?: string | undefined;
|
|
117
|
+
/** Compact step handoff kept separate from the opaque review artifact. */
|
|
118
|
+
readonly summary?: string | undefined;
|
|
27
119
|
readonly submittedOutcome: string;
|
|
28
120
|
readonly requestedAt: number;
|
|
29
121
|
readonly reviewId?: string | undefined;
|
|
@@ -42,15 +134,24 @@ export type WorkflowRun = {
|
|
|
42
134
|
readonly baselineTools: ReadonlyArray<string>;
|
|
43
135
|
readonly visits: Readonly<Record<string, number>>;
|
|
44
136
|
readonly history: ReadonlyArray<StepHistoryEntry>;
|
|
137
|
+
/** Attempts for the current, not-yet-completed step. */
|
|
138
|
+
readonly currentStepAttempts?:
|
|
139
|
+
ReadonlyArray<StepExecutionAttempt> | undefined;
|
|
140
|
+
/** Older current-step attempts compacted from the checkpoint. */
|
|
141
|
+
readonly currentStepOmittedAttempts?: number | undefined;
|
|
45
142
|
readonly startedAt: number;
|
|
46
143
|
readonly updatedAt: number;
|
|
47
144
|
/**
|
|
48
|
-
* Most recent human-approved gate artifact.
|
|
49
|
-
*
|
|
145
|
+
* Most recent human-approved gate artifact. The workflow prompt decides how
|
|
146
|
+
* to interpret this opaque value.
|
|
50
147
|
*/
|
|
51
148
|
readonly reviewedArtifact?: string | undefined;
|
|
52
|
-
/** Final feedback paired with the
|
|
149
|
+
/** Final feedback paired with the approved artifact. */
|
|
53
150
|
readonly reviewedFeedback?: string | undefined;
|
|
151
|
+
/** Immutable working directory captured when the workflow started. */
|
|
152
|
+
readonly startCwd?: string | undefined;
|
|
153
|
+
/** Current canonical execution directory for delegated workflow steps. */
|
|
154
|
+
readonly cwd?: string | undefined;
|
|
54
155
|
/**
|
|
55
156
|
* Input inherited from the previous completed step. Unlike `lastSummary`,
|
|
56
157
|
* this survives a paused attempt of the current step.
|
|
@@ -58,6 +159,8 @@ export type WorkflowRun = {
|
|
|
58
159
|
readonly stepHandoff?: string | undefined;
|
|
59
160
|
readonly lastSummary: string;
|
|
60
161
|
readonly gateFeedback: string;
|
|
162
|
+
/** User-authored guidance supplied for the current resume attempt. */
|
|
163
|
+
readonly resumeInput?: string | undefined;
|
|
61
164
|
readonly pauseReason?: string | undefined;
|
|
62
165
|
readonly pausedFrom?: 'running' | 'awaiting-gate' | undefined;
|
|
63
166
|
/** Current step when execution failed and was paused for a resumable retry. */
|
package/src/engine/state.ts
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
export { createRun } from './create-run.ts';
|
|
2
2
|
export { isWorkflowRun } from './run-validation.ts';
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
MAX_RESUME_INPUT_CHARS,
|
|
5
|
+
MAX_STEP_TRACE_ATTEMPTS,
|
|
6
|
+
MAX_STEP_TRACE_LOG_CHARS,
|
|
7
|
+
MAX_STEP_TRACE_LOG_EVENT_CHARS,
|
|
8
|
+
MAX_STEP_TRACE_LOG_EVENTS,
|
|
9
|
+
MAX_STEP_TRACE_TASK_CHARS,
|
|
10
|
+
MAX_WORKFLOW_TRACE_CHARS,
|
|
11
|
+
RUN_STATE_VERSION,
|
|
12
|
+
} from './state-types.ts';
|
|
4
13
|
export type {
|
|
14
|
+
GateApproval,
|
|
5
15
|
GateResolution,
|
|
6
16
|
PendingGate,
|
|
17
|
+
StepAttemptResult,
|
|
18
|
+
StepExecutionAttempt,
|
|
19
|
+
StepGateDecision,
|
|
7
20
|
StepHistoryEntry,
|
|
21
|
+
SubagentTranscriptReference,
|
|
8
22
|
WorkflowRun,
|
|
9
23
|
WorkflowRunStatus,
|
|
10
24
|
} from './state-types.ts';
|
|
@@ -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
|
+
}
|