@pixelbyte-software/pixcode 1.42.0 → 1.42.2
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/dist/assets/{index-CkOamyD3.js → index-CMeiCqQf.js} +182 -182
- package/dist/index.html +1 -1
- package/dist-server/server/modules/orchestration/workflows/context-packet.js +89 -0
- package/dist-server/server/modules/orchestration/workflows/context-packet.js.map +1 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-fallback-policy.js +114 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-fallback-policy.js.map +1 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-replay.js +177 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-replay.js.map +1 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +58 -7
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow-trace.js +95 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-trace.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +88 -0
- package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/smoke/context-packet.mjs +43 -0
- package/scripts/smoke/workflow-fallback-replay.mjs +56 -0
- package/server/modules/orchestration/workflows/context-packet.ts +186 -0
- package/server/modules/orchestration/workflows/workflow-fallback-policy.ts +161 -0
- package/server/modules/orchestration/workflows/workflow-replay.ts +254 -0
- package/server/modules/orchestration/workflows/workflow-runner.ts +119 -6
- package/server/modules/orchestration/workflows/workflow-trace.ts +98 -0
- package/server/modules/orchestration/workflows/workflow.routes.ts +107 -0
- package/server/modules/orchestration/workflows/workflow.types.ts +7 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Workflow,
|
|
3
|
+
WorkflowNode,
|
|
4
|
+
WorkflowNodeRun,
|
|
5
|
+
WorkflowRun,
|
|
6
|
+
} from '@/modules/orchestration/workflows/workflow.types.js';
|
|
7
|
+
import { redactTraceText } from '@/modules/orchestration/workflows/workflow-trace.js';
|
|
8
|
+
|
|
9
|
+
export const PIXCODE_REPLAY_PROTOCOL = 'pixcode.workflow-replay.v1';
|
|
10
|
+
|
|
11
|
+
export type WorkflowReplayScope = 'run' | 'node';
|
|
12
|
+
export type WorkflowReplaySafetyKind = 'file-write' | 'shell' | 'network';
|
|
13
|
+
|
|
14
|
+
export interface WorkflowReplayOperation {
|
|
15
|
+
kind: WorkflowReplaySafetyKind;
|
|
16
|
+
nodeId?: string;
|
|
17
|
+
summary: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface WorkflowReplayPlan {
|
|
21
|
+
protocol: typeof PIXCODE_REPLAY_PROTOCOL;
|
|
22
|
+
sourceRunId: string;
|
|
23
|
+
sourceWorkflowId: string;
|
|
24
|
+
scope: WorkflowReplayScope;
|
|
25
|
+
fromNodeId?: string;
|
|
26
|
+
selectedNodeIds: string[];
|
|
27
|
+
requiresApproval: boolean;
|
|
28
|
+
approvalReasons: string[];
|
|
29
|
+
destructiveOperations: WorkflowReplayOperation[];
|
|
30
|
+
limitations: string[];
|
|
31
|
+
input: string;
|
|
32
|
+
workflow: Workflow;
|
|
33
|
+
metadata: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function safeNodeId(value: string): string {
|
|
37
|
+
return value.replace(/[^a-zA-Z0-9_]+/g, '_').slice(0, 48) || 'node';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
41
|
+
return value && typeof value === 'object' ? value as Record<string, unknown> : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function visibleNodes(run: WorkflowRun): WorkflowNodeRun[] {
|
|
45
|
+
return run.nodeRuns.filter((node) => !node.internal);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function defaultReplayNode(run: WorkflowRun): WorkflowNodeRun | undefined {
|
|
49
|
+
return visibleNodes(run).find((node) => node.status === 'failed')
|
|
50
|
+
?? [...visibleNodes(run)].reverse().find((node) => node.status !== 'skipped')
|
|
51
|
+
?? visibleNodes(run)[0];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function selectReplayNodes(run: WorkflowRun, scope: WorkflowReplayScope, fromNodeId?: string): WorkflowNodeRun[] {
|
|
55
|
+
const nodes = visibleNodes(run);
|
|
56
|
+
if (scope === 'run') return nodes;
|
|
57
|
+
|
|
58
|
+
const requested = fromNodeId
|
|
59
|
+
? nodes.find((node) => node.nodeId === fromNodeId)
|
|
60
|
+
: defaultReplayNode(run);
|
|
61
|
+
return requested ? [requested] : [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function compact(value: string | undefined, run: WorkflowRun, maxLength = 1_200): string | undefined {
|
|
65
|
+
return redactTraceText(value, run, maxLength);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function nodeTraceSummary(run: WorkflowRun, node: WorkflowNodeRun): string {
|
|
69
|
+
const artifactTypes = (node.artifacts ?? []).map((artifact) => artifact.type).filter(Boolean);
|
|
70
|
+
return [
|
|
71
|
+
`Step: ${node.agentLabel || node.nodeId}`,
|
|
72
|
+
`Node id: ${node.nodeId}`,
|
|
73
|
+
`Status: ${node.status}`,
|
|
74
|
+
node.stage ? `Stage: ${node.stage}` : undefined,
|
|
75
|
+
node.adapterId ? `Adapter: ${node.adapterId}` : undefined,
|
|
76
|
+
node.model ? `Model: ${node.model}` : undefined,
|
|
77
|
+
node.error ? `Error: ${compact(node.error, run, 800)}` : undefined,
|
|
78
|
+
artifactTypes.length > 0 ? `Artifacts: ${artifactTypes.join(', ')}` : undefined,
|
|
79
|
+
node.outputText ? `Output excerpt:\n${compact(node.outputText, run)}` : undefined,
|
|
80
|
+
].filter(Boolean).join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function replayTraceSummary(run: WorkflowRun, nodes: WorkflowNodeRun[]): string {
|
|
84
|
+
return nodes.map((node) => nodeTraceSummary(run, node)).join('\n\n---\n\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function pushOperation(
|
|
88
|
+
operations: WorkflowReplayOperation[],
|
|
89
|
+
kind: WorkflowReplaySafetyKind,
|
|
90
|
+
nodeId: string | undefined,
|
|
91
|
+
summary: string,
|
|
92
|
+
): void {
|
|
93
|
+
if (operations.some((operation) =>
|
|
94
|
+
operation.kind === kind && operation.nodeId === nodeId && operation.summary === summary,
|
|
95
|
+
)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
operations.push({ kind, nodeId, summary });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function detectReplayOperations(run: WorkflowRun, nodes: WorkflowNodeRun[]): WorkflowReplayOperation[] {
|
|
102
|
+
const operations: WorkflowReplayOperation[] = [];
|
|
103
|
+
|
|
104
|
+
for (const node of nodes) {
|
|
105
|
+
for (const artifact of node.artifacts ?? []) {
|
|
106
|
+
if (artifact.type === 'file-diff') {
|
|
107
|
+
pushOperation(operations, 'file-write', node.nodeId, 'Prior step produced a file diff artifact.');
|
|
108
|
+
}
|
|
109
|
+
if (artifact.type === 'command-output') {
|
|
110
|
+
pushOperation(operations, 'shell', node.nodeId, 'Prior step produced command output.');
|
|
111
|
+
}
|
|
112
|
+
const text = [artifact.text, artifact.data ? JSON.stringify(artifact.data) : undefined]
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.join('\n')
|
|
115
|
+
.toLocaleLowerCase('en');
|
|
116
|
+
if (/https?:\/\/|curl |wget |gh |npm publish|npm install|git push|ssh /u.test(text)) {
|
|
117
|
+
pushOperation(operations, 'network', node.nodeId, 'Prior artifact references a network-capable operation.');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const text = [node.outputText, node.error, node.promptPreview].filter(Boolean).join('\n').toLocaleLowerCase('en');
|
|
122
|
+
if (/apply_patch|write file|file write|modified files|changed files/u.test(text)) {
|
|
123
|
+
pushOperation(operations, 'file-write', node.nodeId, 'Prior step text references file-write activity.');
|
|
124
|
+
}
|
|
125
|
+
if (/shell|command|terminal|npm run|node |python |php |go test|cargo |make |exit code/u.test(text)) {
|
|
126
|
+
pushOperation(operations, 'shell', node.nodeId, 'Prior step text references shell execution.');
|
|
127
|
+
}
|
|
128
|
+
if (/https?:\/\/|curl |wget |gh |npm publish|npm install|git push|ssh |network/u.test(text)) {
|
|
129
|
+
pushOperation(operations, 'network', node.nodeId, 'Prior step text references a network-capable operation.');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return operations;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function replayNodeFromRunNode(
|
|
137
|
+
node: WorkflowNodeRun,
|
|
138
|
+
index: number,
|
|
139
|
+
previousReplayNodeId: string | undefined,
|
|
140
|
+
traceSummary: string,
|
|
141
|
+
limitations: string[],
|
|
142
|
+
requiresApproval: boolean,
|
|
143
|
+
): WorkflowNode {
|
|
144
|
+
const replayNodeId = `replay_${index + 1}_${safeNodeId(node.nodeId)}`;
|
|
145
|
+
return {
|
|
146
|
+
id: replayNodeId,
|
|
147
|
+
adapterId: node.adapterId || 'claude-code',
|
|
148
|
+
agentInstanceId: node.agentInstanceId,
|
|
149
|
+
agentLabel: node.agentLabel ? `${node.agentLabel} Replay` : 'Replay agent',
|
|
150
|
+
assignment: node.assignment ? `Replay: ${node.assignment}` : `Replay source node ${node.nodeId}`,
|
|
151
|
+
stage: node.stage ? `replay_${node.stage}` : 'replay',
|
|
152
|
+
model: node.model,
|
|
153
|
+
permissionMode: node.permissionMode === 'bypassPermissions' ? 'default' : node.permissionMode,
|
|
154
|
+
timeoutMs: node.timeoutMs,
|
|
155
|
+
inputs: previousReplayNodeId ? [previousReplayNodeId] : [],
|
|
156
|
+
output: 'both',
|
|
157
|
+
onFail: 'abort',
|
|
158
|
+
prompt: [
|
|
159
|
+
'This is a Pixcode workflow replay run.',
|
|
160
|
+
`Replay protocol: ${PIXCODE_REPLAY_PROTOCOL}`,
|
|
161
|
+
`Source node: ${node.nodeId}`,
|
|
162
|
+
requiresApproval
|
|
163
|
+
? 'Replay safety review found prior shell, network, or file-write activity. Do not repeat any such action unless the current CLI permission flow asks for and receives user approval.'
|
|
164
|
+
: 'Replay safety review did not find prior shell, network, or file-write artifacts, but still avoid destructive actions unless they are required and approved.',
|
|
165
|
+
'Use the trace summary to continue from the failure or inspect the run. Do not expose secrets, local-only paths, raw tool protocol, or irrelevant logs.',
|
|
166
|
+
`Known limitations:\n- ${limitations.join('\n- ')}`,
|
|
167
|
+
`Trace summary:\n${traceSummary}`,
|
|
168
|
+
`Original step prompt:\n${node.promptPreview || '(No source prompt was stored.)'}`,
|
|
169
|
+
].join('\n\n'),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function buildWorkflowReplayPlan(
|
|
174
|
+
run: WorkflowRun,
|
|
175
|
+
options: {
|
|
176
|
+
scope?: WorkflowReplayScope;
|
|
177
|
+
fromNodeId?: string;
|
|
178
|
+
} = {},
|
|
179
|
+
): WorkflowReplayPlan {
|
|
180
|
+
const scope = options.scope ?? 'node';
|
|
181
|
+
const nodes = selectReplayNodes(run, scope, options.fromNodeId);
|
|
182
|
+
if (nodes.length === 0) {
|
|
183
|
+
throw new Error('No replayable workflow steps were found.');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const limitations = [
|
|
187
|
+
'Replay uses stored run traces, prompt previews, messages, and artifacts; it cannot reproduce hidden provider state.',
|
|
188
|
+
'Replay reconstructs selected steps as a new workflow run instead of mutating the source run.',
|
|
189
|
+
'Shell, network, and file-write actions stay under the current CLI permission flow and require explicit replay approval when detected.',
|
|
190
|
+
];
|
|
191
|
+
const destructiveOperations = detectReplayOperations(run, nodes);
|
|
192
|
+
const requiresApproval = destructiveOperations.length > 0;
|
|
193
|
+
const traceSummary = replayTraceSummary(run, nodes);
|
|
194
|
+
const replayNodes = nodes.reduce<WorkflowNode[]>((accumulator, node, index) => {
|
|
195
|
+
const previousReplayNodeId = accumulator[accumulator.length - 1]?.id;
|
|
196
|
+
accumulator.push(replayNodeFromRunNode(
|
|
197
|
+
node,
|
|
198
|
+
index,
|
|
199
|
+
scope === 'run' ? previousReplayNodeId : undefined,
|
|
200
|
+
traceSummary,
|
|
201
|
+
limitations,
|
|
202
|
+
requiresApproval,
|
|
203
|
+
));
|
|
204
|
+
return accumulator;
|
|
205
|
+
}, []);
|
|
206
|
+
const settings = readRecord(run.metadata?.settings) ?? {};
|
|
207
|
+
const replayMetadata = {
|
|
208
|
+
protocol: PIXCODE_REPLAY_PROTOCOL,
|
|
209
|
+
sourceRunId: run.id,
|
|
210
|
+
sourceWorkflowId: run.workflowId,
|
|
211
|
+
scope,
|
|
212
|
+
fromNodeId: options.fromNodeId,
|
|
213
|
+
selectedNodeIds: nodes.map((node) => node.nodeId),
|
|
214
|
+
requiresApproval,
|
|
215
|
+
destructiveOperations,
|
|
216
|
+
limitations,
|
|
217
|
+
createdAt: Date.now(),
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
protocol: PIXCODE_REPLAY_PROTOCOL,
|
|
222
|
+
sourceRunId: run.id,
|
|
223
|
+
sourceWorkflowId: run.workflowId,
|
|
224
|
+
scope,
|
|
225
|
+
fromNodeId: options.fromNodeId,
|
|
226
|
+
selectedNodeIds: nodes.map((node) => node.nodeId),
|
|
227
|
+
requiresApproval,
|
|
228
|
+
approvalReasons: destructiveOperations.map((operation) =>
|
|
229
|
+
`${operation.kind}${operation.nodeId ? ` in ${operation.nodeId}` : ''}: ${operation.summary}`,
|
|
230
|
+
),
|
|
231
|
+
destructiveOperations,
|
|
232
|
+
limitations,
|
|
233
|
+
input: [
|
|
234
|
+
`Replay ${scope === 'run' ? 'full workflow run' : 'workflow step'} from source run ${run.id}.`,
|
|
235
|
+
run.input ? `Original request:\n${compact(run.input, run, 2_000)}` : undefined,
|
|
236
|
+
].filter(Boolean).join('\n\n'),
|
|
237
|
+
workflow: {
|
|
238
|
+
id: `${run.workflowId}_replay`,
|
|
239
|
+
name: `Replay ${run.workflowId}`,
|
|
240
|
+
description: 'Replay generated from stored Pixcode workflow trace data.',
|
|
241
|
+
trigger: 'manual',
|
|
242
|
+
nodes: replayNodes,
|
|
243
|
+
},
|
|
244
|
+
metadata: {
|
|
245
|
+
...run.metadata,
|
|
246
|
+
workflowName: `Replay: ${String(run.metadata?.workflowName ?? run.workflowId)}`,
|
|
247
|
+
replay: replayMetadata,
|
|
248
|
+
settings: {
|
|
249
|
+
...settings,
|
|
250
|
+
replayMode: true,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -12,6 +12,15 @@ import {
|
|
|
12
12
|
handoffArtifactToWorkflowArtifact,
|
|
13
13
|
parseHandoffArtifact,
|
|
14
14
|
} from '@/modules/orchestration/workflows/handoff-artifact.js';
|
|
15
|
+
import {
|
|
16
|
+
buildWorkflowContextPacket,
|
|
17
|
+
formatContextPacketForPrompt,
|
|
18
|
+
} from '@/modules/orchestration/workflows/context-packet.js';
|
|
19
|
+
import {
|
|
20
|
+
type WorkflowFallbackTrigger,
|
|
21
|
+
classifyWorkflowFailure,
|
|
22
|
+
resolveWorkflowFallbackDecision,
|
|
23
|
+
} from '@/modules/orchestration/workflows/workflow-fallback-policy.js';
|
|
15
24
|
import {
|
|
16
25
|
type ResolvedWorkspaceTarget,
|
|
17
26
|
resolveWorkflowWorkspace,
|
|
@@ -1119,6 +1128,8 @@ function nodeRunFromNode(node: WorkflowNode): WorkflowNodeRun {
|
|
|
1119
1128
|
timeoutMs: node.timeoutMs,
|
|
1120
1129
|
stage: node.stage,
|
|
1121
1130
|
internal: node.internal,
|
|
1131
|
+
fallbackTrigger: node.fallbackTrigger,
|
|
1132
|
+
fallbackSourceNodeId: node.fallbackSourceNodeId,
|
|
1122
1133
|
status: 'queued',
|
|
1123
1134
|
};
|
|
1124
1135
|
}
|
|
@@ -1272,7 +1283,12 @@ class WorkflowRunner {
|
|
|
1272
1283
|
return readAgentAssignments(run.metadata).find((agent) => agent.instanceId === fallbackAgentInstanceId);
|
|
1273
1284
|
}
|
|
1274
1285
|
|
|
1275
|
-
private createFallbackNode(
|
|
1286
|
+
private createFallbackNode(
|
|
1287
|
+
node: WorkflowNode,
|
|
1288
|
+
fallbackAgent: AgentAssignment,
|
|
1289
|
+
reason: string,
|
|
1290
|
+
fallbackTrigger: WorkflowFallbackTrigger,
|
|
1291
|
+
): WorkflowNode {
|
|
1276
1292
|
const fallbackSuffix = safeNodeId(fallbackAgent.instanceId, 'fallback');
|
|
1277
1293
|
return {
|
|
1278
1294
|
...node,
|
|
@@ -1285,9 +1301,12 @@ class WorkflowRunner {
|
|
|
1285
1301
|
model: fallbackAgent.model,
|
|
1286
1302
|
permissionMode: fallbackAgent.permissionMode,
|
|
1287
1303
|
toolsSettings: fallbackAgent.toolsSettings,
|
|
1304
|
+
fallbackTrigger,
|
|
1305
|
+
fallbackSourceNodeId: node.id,
|
|
1288
1306
|
prompt: [
|
|
1289
1307
|
'The previous CLI agent failed on this orchestration step.',
|
|
1290
1308
|
`Failed step: ${node.agentLabel || node.id}`,
|
|
1309
|
+
`Fallback trigger: ${fallbackTrigger}`,
|
|
1291
1310
|
`Failure: ${reason}`,
|
|
1292
1311
|
'Take over the same assignment as the backup CLI. Use the original goal and upstream context.',
|
|
1293
1312
|
'Do not repeat unrelated work; complete the failed step and report what you did.',
|
|
@@ -1297,6 +1316,32 @@ class WorkflowRunner {
|
|
|
1297
1316
|
};
|
|
1298
1317
|
}
|
|
1299
1318
|
|
|
1319
|
+
private recordFallbackSkipped(
|
|
1320
|
+
run: WorkflowRun,
|
|
1321
|
+
node: WorkflowNode,
|
|
1322
|
+
reason: string,
|
|
1323
|
+
fallbackTrigger: WorkflowFallbackTrigger,
|
|
1324
|
+
skippedReason: string,
|
|
1325
|
+
): void {
|
|
1326
|
+
const fallbackSkippedEvents = Array.isArray(run.metadata?.fallbackSkippedEvents)
|
|
1327
|
+
? run.metadata.fallbackSkippedEvents
|
|
1328
|
+
: [];
|
|
1329
|
+
run.metadata = {
|
|
1330
|
+
...run.metadata,
|
|
1331
|
+
fallbackSkippedEvents: [
|
|
1332
|
+
...fallbackSkippedEvents,
|
|
1333
|
+
{
|
|
1334
|
+
nodeId: node.id,
|
|
1335
|
+
trigger: fallbackTrigger,
|
|
1336
|
+
reason,
|
|
1337
|
+
skippedReason,
|
|
1338
|
+
createdAt: Date.now(),
|
|
1339
|
+
},
|
|
1340
|
+
],
|
|
1341
|
+
};
|
|
1342
|
+
workflowStore.setRun(run);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1300
1345
|
private async runFallbackAfterFailure(
|
|
1301
1346
|
node: WorkflowNode,
|
|
1302
1347
|
workflow: Workflow,
|
|
@@ -1305,9 +1350,29 @@ class WorkflowRunner {
|
|
|
1305
1350
|
started: Set<string>,
|
|
1306
1351
|
completed: Set<string>,
|
|
1307
1352
|
reason: string,
|
|
1353
|
+
trigger?: WorkflowFallbackTrigger,
|
|
1308
1354
|
): Promise<boolean> {
|
|
1355
|
+
const fallbackTrigger = classifyWorkflowFailure(reason, trigger);
|
|
1309
1356
|
const fallbackAgent = this.fallbackAgentFor(run, node);
|
|
1310
1357
|
if (!fallbackAgent) {
|
|
1358
|
+
this.recordFallbackSkipped(run, node, reason, fallbackTrigger, 'No fallback agent is configured for this run.');
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
const decision = resolveWorkflowFallbackDecision({
|
|
1362
|
+
run,
|
|
1363
|
+
node,
|
|
1364
|
+
reason,
|
|
1365
|
+
trigger: fallbackTrigger,
|
|
1366
|
+
fallbackAgentInstanceId: fallbackAgent.instanceId,
|
|
1367
|
+
});
|
|
1368
|
+
if (!decision.shouldFallback) {
|
|
1369
|
+
this.recordFallbackSkipped(
|
|
1370
|
+
run,
|
|
1371
|
+
node,
|
|
1372
|
+
reason,
|
|
1373
|
+
decision.trigger,
|
|
1374
|
+
decision.skippedReason ?? 'Fallback policy skipped this failure.',
|
|
1375
|
+
);
|
|
1311
1376
|
return false;
|
|
1312
1377
|
}
|
|
1313
1378
|
if (workflow.nodes.length + 1 > 64) {
|
|
@@ -1319,7 +1384,7 @@ class WorkflowRunner {
|
|
|
1319
1384
|
return false;
|
|
1320
1385
|
}
|
|
1321
1386
|
|
|
1322
|
-
let fallbackNode = this.createFallbackNode(node, fallbackAgent, reason);
|
|
1387
|
+
let fallbackNode = this.createFallbackNode(node, fallbackAgent, reason, decision.trigger);
|
|
1323
1388
|
let collision = 1;
|
|
1324
1389
|
while (workflow.nodes.some((candidate) => candidate.id === fallbackNode.id)) {
|
|
1325
1390
|
collision += 1;
|
|
@@ -1353,6 +1418,8 @@ class WorkflowRunner {
|
|
|
1353
1418
|
nodeId: node.id,
|
|
1354
1419
|
fallbackNodeId: fallbackNode.id,
|
|
1355
1420
|
fallbackAgentInstanceId: fallbackAgent.instanceId,
|
|
1421
|
+
trigger: decision.trigger,
|
|
1422
|
+
policy: decision.policy,
|
|
1356
1423
|
reason,
|
|
1357
1424
|
startedAt: Date.now(),
|
|
1358
1425
|
},
|
|
@@ -1561,9 +1628,19 @@ class WorkflowRunner {
|
|
|
1561
1628
|
|
|
1562
1629
|
const inputContext = node.inputs.map((input) => outputs.get(input)).filter(Boolean).join('\n\n');
|
|
1563
1630
|
const workspaceTarget = resolveWorkflowWorkspace(run.metadata);
|
|
1631
|
+
const contextPacket = buildWorkflowContextPacket({
|
|
1632
|
+
run,
|
|
1633
|
+
node,
|
|
1634
|
+
workspaceTarget,
|
|
1635
|
+
inputContext,
|
|
1636
|
+
inputNodeIds: node.inputs,
|
|
1637
|
+
});
|
|
1638
|
+
nodeRun.contextPacket = contextPacket;
|
|
1639
|
+
workflowStore.setRun(run);
|
|
1564
1640
|
const prompt = [
|
|
1565
1641
|
'Original user request (primary task; answer this directly even if the workspace is empty):',
|
|
1566
1642
|
run.input?.trim() || '(No original user request was provided.)',
|
|
1643
|
+
formatContextPacketForPrompt(contextPacket),
|
|
1567
1644
|
inputContext
|
|
1568
1645
|
? `Upstream workflow context from prior agents:\n${inputContext}`
|
|
1569
1646
|
: '',
|
|
@@ -1644,7 +1721,16 @@ class WorkflowRunner {
|
|
|
1644
1721
|
workflowStore.setRun(run);
|
|
1645
1722
|
return;
|
|
1646
1723
|
}
|
|
1647
|
-
if (await this.runFallbackAfterFailure(
|
|
1724
|
+
if (await this.runFallbackAfterFailure(
|
|
1725
|
+
node,
|
|
1726
|
+
workflow,
|
|
1727
|
+
run,
|
|
1728
|
+
outputs,
|
|
1729
|
+
started,
|
|
1730
|
+
completed,
|
|
1731
|
+
nodeRun.error,
|
|
1732
|
+
'provider_failure',
|
|
1733
|
+
)) {
|
|
1648
1734
|
return;
|
|
1649
1735
|
}
|
|
1650
1736
|
if (node.onFail === 'continue') {
|
|
@@ -1696,7 +1782,16 @@ class WorkflowRunner {
|
|
|
1696
1782
|
workflowStore.setRun(run);
|
|
1697
1783
|
return;
|
|
1698
1784
|
}
|
|
1699
|
-
if (await this.runFallbackAfterFailure(
|
|
1785
|
+
if (await this.runFallbackAfterFailure(
|
|
1786
|
+
node,
|
|
1787
|
+
workflow,
|
|
1788
|
+
run,
|
|
1789
|
+
outputs,
|
|
1790
|
+
started,
|
|
1791
|
+
completed,
|
|
1792
|
+
nodeRun.error,
|
|
1793
|
+
'timeout',
|
|
1794
|
+
)) {
|
|
1700
1795
|
return;
|
|
1701
1796
|
}
|
|
1702
1797
|
if (node.onFail === 'continue') {
|
|
@@ -1730,7 +1825,16 @@ class WorkflowRunner {
|
|
|
1730
1825
|
nodeRun.status = 'failed';
|
|
1731
1826
|
nodeRun.error = visibleHandoffError;
|
|
1732
1827
|
workflowStore.setRun(run);
|
|
1733
|
-
if (await this.runFallbackAfterFailure(
|
|
1828
|
+
if (await this.runFallbackAfterFailure(
|
|
1829
|
+
node,
|
|
1830
|
+
workflow,
|
|
1831
|
+
run,
|
|
1832
|
+
outputs,
|
|
1833
|
+
started,
|
|
1834
|
+
completed,
|
|
1835
|
+
visibleHandoffError,
|
|
1836
|
+
'invalid_output',
|
|
1837
|
+
)) {
|
|
1734
1838
|
return;
|
|
1735
1839
|
}
|
|
1736
1840
|
if (node.onFail === 'continue') {
|
|
@@ -1769,7 +1873,16 @@ class WorkflowRunner {
|
|
|
1769
1873
|
workflowStore.setRun(run);
|
|
1770
1874
|
return;
|
|
1771
1875
|
}
|
|
1772
|
-
if (await this.runFallbackAfterFailure(
|
|
1876
|
+
if (await this.runFallbackAfterFailure(
|
|
1877
|
+
node,
|
|
1878
|
+
workflow,
|
|
1879
|
+
run,
|
|
1880
|
+
outputs,
|
|
1881
|
+
started,
|
|
1882
|
+
completed,
|
|
1883
|
+
nodeRun.error,
|
|
1884
|
+
classifyWorkflowFailure(`${nodeRun.error}\n${nodeRun.outputText ?? ''}`),
|
|
1885
|
+
)) {
|
|
1773
1886
|
return;
|
|
1774
1887
|
}
|
|
1775
1888
|
if (node.onFail === 'continue') {
|
|
@@ -22,6 +22,10 @@ function readString(value: unknown): string | undefined {
|
|
|
22
22
|
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
26
|
+
return value && typeof value === 'object' ? value as Record<string, unknown> : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
25
29
|
function redactionValues(run: WorkflowRun): string[] {
|
|
26
30
|
const metadata = run.metadata ?? {};
|
|
27
31
|
const workspaceTarget = metadata.workspaceTarget && typeof metadata.workspaceTarget === 'object'
|
|
@@ -140,6 +144,78 @@ export function buildWorkflowTrace(run: WorkflowRun): WorkflowTraceEvent[] {
|
|
|
140
144
|
},
|
|
141
145
|
});
|
|
142
146
|
|
|
147
|
+
const replay = readRecord(run.metadata?.replay);
|
|
148
|
+
if (replay) {
|
|
149
|
+
pushEvent(events, {
|
|
150
|
+
id: traceId([run.id, 'replay']),
|
|
151
|
+
type: 'run',
|
|
152
|
+
severity: replay.requiresApproval ? 'warning' : 'info',
|
|
153
|
+
status: run.status,
|
|
154
|
+
timestamp: run.startedAt + 0.25,
|
|
155
|
+
actor: 'Pixcode',
|
|
156
|
+
title: 'Workflow replay prepared',
|
|
157
|
+
titleKey: 'workflow.trace.replay',
|
|
158
|
+
summary: redactTraceText([
|
|
159
|
+
`Source run: ${readString(replay.sourceRunId) ?? 'unknown'}`,
|
|
160
|
+
`Scope: ${readString(replay.scope) ?? 'unknown'}`,
|
|
161
|
+
Array.isArray(replay.selectedNodeIds) ? `Selected steps: ${replay.selectedNodeIds.join(', ')}` : undefined,
|
|
162
|
+
replay.requiresApproval ? 'Replay required approval for prior shell, network, or file-write activity.' : undefined,
|
|
163
|
+
].filter(Boolean).join('\n'), run),
|
|
164
|
+
metadata: replay,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const fallbackEvents = Array.isArray(run.metadata?.fallbackEvents)
|
|
169
|
+
? run.metadata.fallbackEvents
|
|
170
|
+
: [];
|
|
171
|
+
fallbackEvents.forEach((event, index) => {
|
|
172
|
+
const record = readRecord(event);
|
|
173
|
+
if (!record) return;
|
|
174
|
+
pushEvent(events, {
|
|
175
|
+
id: traceId([run.id, 'fallback', index]),
|
|
176
|
+
type: 'node',
|
|
177
|
+
severity: 'warning',
|
|
178
|
+
status: 'submitted',
|
|
179
|
+
timestamp: typeof record.startedAt === 'number' ? record.startedAt : run.startedAt + 0.5 + index,
|
|
180
|
+
actor: 'Pixcode',
|
|
181
|
+
nodeId: readString(record.nodeId),
|
|
182
|
+
title: 'Fallback agent started',
|
|
183
|
+
titleKey: 'workflow.trace.fallback',
|
|
184
|
+
summary: redactTraceText([
|
|
185
|
+
`Trigger: ${readString(record.trigger) ?? 'unknown'}`,
|
|
186
|
+
`Source node: ${readString(record.nodeId) ?? 'unknown'}`,
|
|
187
|
+
`Fallback node: ${readString(record.fallbackNodeId) ?? 'unknown'}`,
|
|
188
|
+
readString(record.reason) ? `Reason: ${readString(record.reason)}` : undefined,
|
|
189
|
+
].filter(Boolean).join('\n'), run),
|
|
190
|
+
metadata: record,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const fallbackSkippedEvents = Array.isArray(run.metadata?.fallbackSkippedEvents)
|
|
195
|
+
? run.metadata.fallbackSkippedEvents
|
|
196
|
+
: [];
|
|
197
|
+
fallbackSkippedEvents.forEach((event, index) => {
|
|
198
|
+
const record = readRecord(event);
|
|
199
|
+
if (!record) return;
|
|
200
|
+
pushEvent(events, {
|
|
201
|
+
id: traceId([run.id, 'fallback-skipped', index]),
|
|
202
|
+
type: 'node',
|
|
203
|
+
severity: 'info',
|
|
204
|
+
status: 'skipped',
|
|
205
|
+
timestamp: typeof record.createdAt === 'number' ? record.createdAt : run.startedAt + 0.75 + index,
|
|
206
|
+
actor: 'Pixcode',
|
|
207
|
+
nodeId: readString(record.nodeId),
|
|
208
|
+
title: 'Fallback skipped',
|
|
209
|
+
titleKey: 'workflow.trace.fallback',
|
|
210
|
+
summary: redactTraceText([
|
|
211
|
+
`Trigger: ${readString(record.trigger) ?? 'unknown'}`,
|
|
212
|
+
`Skipped: ${readString(record.skippedReason) ?? 'policy did not allow fallback'}`,
|
|
213
|
+
readString(record.reason) ? `Reason: ${readString(record.reason)}` : undefined,
|
|
214
|
+
].filter(Boolean).join('\n'), run),
|
|
215
|
+
metadata: record,
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
143
219
|
run.nodeRuns.forEach((node, index) => {
|
|
144
220
|
const base = eventBase(node);
|
|
145
221
|
const timestamp = nodeTimestamp(run, node, index);
|
|
@@ -176,6 +252,28 @@ export function buildWorkflowTrace(run: WorkflowRun): WorkflowTraceEvent[] {
|
|
|
176
252
|
});
|
|
177
253
|
}
|
|
178
254
|
|
|
255
|
+
if (node.contextPacket) {
|
|
256
|
+
pushEvent(events, {
|
|
257
|
+
id: traceId([run.id, node.nodeId, 'context-packet']),
|
|
258
|
+
type: 'message',
|
|
259
|
+
severity: node.contextPacket.compaction.wasCompacted ? 'warning' : 'info',
|
|
260
|
+
status: node.status,
|
|
261
|
+
timestamp: timestamp + 1.5,
|
|
262
|
+
...base,
|
|
263
|
+
title: 'Context packet prepared',
|
|
264
|
+
titleKey: 'workflow.trace.contextPacket',
|
|
265
|
+
summary: node.contextPacket.compaction.wasCompacted
|
|
266
|
+
? `Context compacted by ${node.contextPacket.compaction.omittedChars} characters`
|
|
267
|
+
: 'Context packet prepared without compaction',
|
|
268
|
+
metadata: {
|
|
269
|
+
protocol: node.contextPacket.protocol,
|
|
270
|
+
compaction: node.contextPacket.compaction,
|
|
271
|
+
upstreamArtifactCount: node.contextPacket.upstreamArtifacts.length,
|
|
272
|
+
sourceNodeIds: node.contextPacket.upstreamArtifacts.flatMap((artifact) => artifact.sourceNodeIds),
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
179
277
|
if (node.adapterId || node.model) {
|
|
180
278
|
pushEvent(events, {
|
|
181
279
|
id: traceId([run.id, node.nodeId, 'provider']),
|