@exaudeus/workrail 1.14.2 → 1.15.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/dist/application/services/validation-engine.js +3 -3
- package/dist/manifest.json +36 -28
- package/dist/mcp/handler-factory.js +10 -1
- package/dist/mcp/handlers/v2-execution-helpers.js +1 -1
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/tool-descriptions.js +30 -52
- package/dist/mcp/v2/tools.js +7 -1
- package/dist/mcp/v2-response-formatter.d.ts +1 -0
- package/dist/mcp/v2-response-formatter.js +200 -0
- package/dist/v2/durable-core/domain/prompt-renderer.js +55 -6
- package/dist/v2/durable-core/domain/reason-model.js +1 -1
- package/dist/v2/durable-core/schemas/artifacts/loop-control.d.ts +4 -4
- package/dist/v2/durable-core/schemas/artifacts/loop-control.js +5 -3
- package/dist/v2/usecases/console-routes.js +10 -0
- package/dist/v2/usecases/console-service.d.ts +8 -3
- package/dist/v2/usecases/console-service.js +421 -23
- package/dist/v2/usecases/console-types.d.ts +48 -0
- package/package.json +1 -1
- package/spec/workflow.schema.json +7 -3
- package/workflows/coding-task-workflow-agentic.json +5 -4
- package/workflows/design-thinking-workflow-autonomous.agentic.json +4 -4
- package/workflows/design-thinking-workflow.json +12 -12
- package/workflows/mr-review-workflow.agentic.json +2 -2
- package/workflows/mr-review-workflow.json +3 -3
- package/workflows/test-artifact-loop-control.json +4 -3
|
@@ -2,29 +2,39 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ConsoleService = void 0;
|
|
4
4
|
const neverthrow_1 = require("neverthrow");
|
|
5
|
-
const
|
|
5
|
+
const neverthrow_2 = require("neverthrow");
|
|
6
6
|
const session_health_js_1 = require("../projections/session-health.js");
|
|
7
7
|
const run_dag_js_1 = require("../projections/run-dag.js");
|
|
8
8
|
const run_status_signals_js_1 = require("../projections/run-status-signals.js");
|
|
9
9
|
const gaps_js_1 = require("../projections/gaps.js");
|
|
10
10
|
const node_outputs_js_1 = require("../projections/node-outputs.js");
|
|
11
|
+
const advance_outcomes_js_1 = require("../projections/advance-outcomes.js");
|
|
12
|
+
const artifacts_js_1 = require("../projections/artifacts.js");
|
|
13
|
+
const run_context_js_1 = require("../projections/run-context.js");
|
|
11
14
|
const constants_js_1 = require("../durable-core/constants.js");
|
|
12
15
|
const index_js_1 = require("../durable-core/ids/index.js");
|
|
13
|
-
const
|
|
16
|
+
const MAX_SESSIONS_TO_LOAD = 500;
|
|
14
17
|
class ConsoleService {
|
|
15
18
|
constructor(ports) {
|
|
16
19
|
this.ports = ports;
|
|
17
20
|
}
|
|
18
21
|
getSessionList() {
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
dataDir: this.ports.dataDir,
|
|
22
|
-
})
|
|
22
|
+
return this.ports.directoryListing
|
|
23
|
+
.readdirWithMtime(this.ports.dataDir.sessionsDir())
|
|
23
24
|
.mapErr((fsErr) => ({
|
|
24
25
|
code: 'ENUMERATION_FAILED',
|
|
25
26
|
message: `Failed to enumerate sessions: ${fsErr.message}`,
|
|
26
27
|
}))
|
|
27
|
-
.andThen((
|
|
28
|
+
.andThen((entries) => {
|
|
29
|
+
const SESSION_DIR_PATTERN = /^sess_[a-zA-Z0-9_]+$/;
|
|
30
|
+
const validEntries = entries
|
|
31
|
+
.filter((e) => SESSION_DIR_PATTERN.test(e.name))
|
|
32
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs || a.name.localeCompare(b.name))
|
|
33
|
+
.slice(0, MAX_SESSIONS_TO_LOAD);
|
|
34
|
+
const mtimeBySessionId = new Map(validEntries.map((e) => [e.name, e.mtimeMs]));
|
|
35
|
+
const sessionIds = validEntries.map((e) => (0, index_js_1.asSessionId)(e.name));
|
|
36
|
+
return this.collectSessionSummaries(sessionIds, mtimeBySessionId);
|
|
37
|
+
});
|
|
28
38
|
}
|
|
29
39
|
getSessionDetail(sessionIdStr) {
|
|
30
40
|
const sessionId = (0, index_js_1.asSessionId)(sessionIdStr);
|
|
@@ -34,20 +44,281 @@ class ConsoleService {
|
|
|
34
44
|
code: 'SESSION_LOAD_FAILED',
|
|
35
45
|
message: `Failed to load session ${sessionIdStr}: ${storeErr.message}`,
|
|
36
46
|
}))
|
|
37
|
-
.
|
|
47
|
+
.andThen((truth) => {
|
|
48
|
+
const dagRes = (0, run_dag_js_1.projectRunDagV2)(truth.events);
|
|
49
|
+
if (dagRes.isErr()) {
|
|
50
|
+
return resolveRunCompletion(truth.events, this.ports.snapshotStore)
|
|
51
|
+
.map((completionMap) => projectSessionDetail(sessionId, truth, completionMap, {}, {}));
|
|
52
|
+
}
|
|
53
|
+
const dag = dagRes.value;
|
|
54
|
+
return neverthrow_1.ResultAsync.combine([
|
|
55
|
+
resolveRunCompletion(truth.events, this.ports.snapshotStore),
|
|
56
|
+
resolveStepLabels(dag, this.ports.snapshotStore, this.ports.pinnedWorkflowStore),
|
|
57
|
+
resolveWorkflowNames(dag, this.ports.pinnedWorkflowStore),
|
|
58
|
+
]).map(([completionMap, stepLabels, workflowNames]) => projectSessionDetail(sessionId, truth, completionMap, stepLabels, workflowNames));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
getNodeDetail(sessionIdStr, nodeId) {
|
|
62
|
+
const sessionId = (0, index_js_1.asSessionId)(sessionIdStr);
|
|
63
|
+
return this.ports.sessionStore
|
|
64
|
+
.load(sessionId)
|
|
65
|
+
.mapErr((storeErr) => ({
|
|
66
|
+
code: 'SESSION_LOAD_FAILED',
|
|
67
|
+
message: `Failed to load session ${sessionIdStr}: ${storeErr.message}`,
|
|
68
|
+
}))
|
|
69
|
+
.andThen((truth) => {
|
|
70
|
+
const dagRes = (0, run_dag_js_1.projectRunDagV2)(truth.events);
|
|
71
|
+
if (dagRes.isErr()) {
|
|
72
|
+
return (0, neverthrow_2.errAsync)({
|
|
73
|
+
code: 'NODE_NOT_FOUND',
|
|
74
|
+
message: `Node ${nodeId} not found in session ${sessionIdStr}`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return resolveStepLabels(dagRes.value, this.ports.snapshotStore, this.ports.pinnedWorkflowStore)
|
|
78
|
+
.map((stepLabels) => {
|
|
79
|
+
const result = projectNodeDetail(truth.events, nodeId, stepLabels);
|
|
80
|
+
return result;
|
|
81
|
+
})
|
|
82
|
+
.andThen((result) => {
|
|
83
|
+
if (!result) {
|
|
84
|
+
return (0, neverthrow_2.errAsync)({
|
|
85
|
+
code: 'NODE_NOT_FOUND',
|
|
86
|
+
message: `Node ${nodeId} not found in session ${sessionIdStr}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return (0, neverthrow_2.okAsync)(result);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
38
92
|
}
|
|
39
|
-
collectSessionSummaries(sessionIds) {
|
|
40
|
-
|
|
93
|
+
collectSessionSummaries(sessionIds, mtimeBySessionId) {
|
|
94
|
+
const tasks = sessionIds.map((id) => this.loadSessionSummary(id, mtimeBySessionId.get(id) ?? 0));
|
|
95
|
+
return neverthrow_1.ResultAsync.combine(tasks).map((results) => {
|
|
96
|
+
const sessions = results.filter((s) => s !== null);
|
|
97
|
+
return { sessions, totalCount: sessions.length };
|
|
98
|
+
});
|
|
41
99
|
}
|
|
42
|
-
loadSessionSummary(sessionId) {
|
|
100
|
+
loadSessionSummary(sessionId, lastModifiedMs) {
|
|
43
101
|
return this.ports.sessionStore
|
|
44
102
|
.load(sessionId)
|
|
45
|
-
.
|
|
46
|
-
|
|
103
|
+
.andThen((truth) => {
|
|
104
|
+
const dagRes = (0, run_dag_js_1.projectRunDagV2)(truth.events);
|
|
105
|
+
const workflowNamesRA = dagRes.isOk()
|
|
106
|
+
? resolveWorkflowNames(dagRes.value, this.ports.pinnedWorkflowStore)
|
|
107
|
+
: (0, neverthrow_2.okAsync)({});
|
|
108
|
+
return neverthrow_1.ResultAsync.combine([
|
|
109
|
+
resolveRunCompletion(truth.events, this.ports.snapshotStore),
|
|
110
|
+
workflowNamesRA,
|
|
111
|
+
]).map(([completionMap, workflowNames]) => projectSessionSummary(sessionId, truth, completionMap, workflowNames, lastModifiedMs));
|
|
112
|
+
})
|
|
113
|
+
.orElse(() => (0, neverthrow_2.okAsync)(null));
|
|
47
114
|
}
|
|
48
115
|
}
|
|
49
116
|
exports.ConsoleService = ConsoleService;
|
|
50
|
-
function
|
|
117
|
+
function resolveRunCompletion(events, snapshotStore) {
|
|
118
|
+
const dagRes = (0, run_dag_js_1.projectRunDagV2)(events);
|
|
119
|
+
if (dagRes.isErr())
|
|
120
|
+
return (0, neverthrow_2.okAsync)({});
|
|
121
|
+
return resolveRunCompletionFromDag(dagRes.value, snapshotStore);
|
|
122
|
+
}
|
|
123
|
+
function resolveRunCompletionFromDag(dag, snapshotStore) {
|
|
124
|
+
const tipRefs = collectPreferredTipSnapshotRefs(dag);
|
|
125
|
+
if (tipRefs.length === 0)
|
|
126
|
+
return (0, neverthrow_2.okAsync)({});
|
|
127
|
+
const tasks = tipRefs.map(({ runId, snapshotRef }) => snapshotStore
|
|
128
|
+
.getExecutionSnapshotV1(snapshotRef)
|
|
129
|
+
.map((snapshot) => [
|
|
130
|
+
runId,
|
|
131
|
+
snapshot?.enginePayload.engineState.kind === 'complete',
|
|
132
|
+
])
|
|
133
|
+
.orElse(() => (0, neverthrow_2.okAsync)([runId, false])));
|
|
134
|
+
return neverthrow_1.ResultAsync.combine(tasks).map((entries) => Object.fromEntries(entries));
|
|
135
|
+
}
|
|
136
|
+
function collectPreferredTipSnapshotRefs(dag) {
|
|
137
|
+
const refs = [];
|
|
138
|
+
for (const run of Object.values(dag.runsById)) {
|
|
139
|
+
if (!run.preferredTipNodeId)
|
|
140
|
+
continue;
|
|
141
|
+
const tip = run.nodesById[run.preferredTipNodeId];
|
|
142
|
+
if (tip)
|
|
143
|
+
refs.push({ runId: run.runId, snapshotRef: (0, index_js_1.asSnapshotRef)((0, index_js_1.asSha256Digest)(tip.snapshotRef)) });
|
|
144
|
+
}
|
|
145
|
+
return refs;
|
|
146
|
+
}
|
|
147
|
+
function resolveStepLabels(dag, snapshotStore, pinnedWorkflowStore) {
|
|
148
|
+
const allNodes = [];
|
|
149
|
+
for (const run of Object.values(dag.runsById)) {
|
|
150
|
+
allNodes.push(...Object.values(run.nodesById));
|
|
151
|
+
}
|
|
152
|
+
if (allNodes.length === 0)
|
|
153
|
+
return (0, neverthrow_2.okAsync)({});
|
|
154
|
+
const uniqueSnapshotRefs = [...new Set(allNodes.map((n) => n.snapshotRef))];
|
|
155
|
+
const snapshotTasks = uniqueSnapshotRefs.map((ref) => snapshotStore
|
|
156
|
+
.getExecutionSnapshotV1((0, index_js_1.asSnapshotRef)((0, index_js_1.asSha256Digest)(ref)))
|
|
157
|
+
.map((snap) => [ref, snap])
|
|
158
|
+
.orElse(() => (0, neverthrow_2.okAsync)([ref, null])));
|
|
159
|
+
return neverthrow_1.ResultAsync.combine(snapshotTasks).andThen((snapshotEntries) => {
|
|
160
|
+
const snapshotByRef = new Map(snapshotEntries);
|
|
161
|
+
const uniqueHashes = [...new Set(allNodes.map((n) => n.workflowHash))];
|
|
162
|
+
const workflowTasks = uniqueHashes.map((hash) => pinnedWorkflowStore
|
|
163
|
+
.get((0, index_js_1.asWorkflowHash)((0, index_js_1.asSha256Digest)(hash)))
|
|
164
|
+
.map((compiled) => [
|
|
165
|
+
hash,
|
|
166
|
+
compiled ? extractStepTitlesFromCompiled(compiled) : new Map(),
|
|
167
|
+
])
|
|
168
|
+
.orElse(() => (0, neverthrow_2.okAsync)([hash, new Map()])));
|
|
169
|
+
return neverthrow_1.ResultAsync.combine(workflowTasks).map((workflowEntries) => {
|
|
170
|
+
const titlesByHash = new Map(workflowEntries);
|
|
171
|
+
const labels = {};
|
|
172
|
+
for (const node of allNodes) {
|
|
173
|
+
const snap = snapshotByRef.get(node.snapshotRef);
|
|
174
|
+
const stepId = snap ? extractPendingStepId(snap) : null;
|
|
175
|
+
if (!stepId)
|
|
176
|
+
continue;
|
|
177
|
+
const titles = titlesByHash.get(node.workflowHash);
|
|
178
|
+
const title = titles?.get(stepId);
|
|
179
|
+
labels[node.nodeId] = title ?? stepId;
|
|
180
|
+
}
|
|
181
|
+
return labels;
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function resolveWorkflowNames(dag, pinnedWorkflowStore) {
|
|
186
|
+
const hashSet = new Set();
|
|
187
|
+
for (const run of Object.values(dag.runsById)) {
|
|
188
|
+
if (run.workflow.kind === 'with_workflow') {
|
|
189
|
+
hashSet.add(run.workflow.workflowHash);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (hashSet.size === 0)
|
|
193
|
+
return (0, neverthrow_2.okAsync)({});
|
|
194
|
+
const tasks = [...hashSet].map((hash) => pinnedWorkflowStore
|
|
195
|
+
.get((0, index_js_1.asWorkflowHash)((0, index_js_1.asSha256Digest)(hash)))
|
|
196
|
+
.map((compiled) => [hash, compiled?.name ?? null])
|
|
197
|
+
.orElse(() => (0, neverthrow_2.okAsync)([hash, null])));
|
|
198
|
+
return neverthrow_1.ResultAsync.combine(tasks).map((entries) => {
|
|
199
|
+
const names = {};
|
|
200
|
+
for (const [hash, name] of entries) {
|
|
201
|
+
if (name)
|
|
202
|
+
names[hash] = name;
|
|
203
|
+
}
|
|
204
|
+
return names;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function extractPendingStepId(snapshot) {
|
|
208
|
+
const state = snapshot.enginePayload.engineState;
|
|
209
|
+
if ((state.kind === 'running' || state.kind === 'blocked') && state.pending.kind === 'some') {
|
|
210
|
+
return String(state.pending.step.stepId);
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
function extractStepTitlesFromCompiled(compiled) {
|
|
215
|
+
const titles = new Map();
|
|
216
|
+
if (compiled.sourceKind === 'v1_preview') {
|
|
217
|
+
titles.set(compiled.preview.stepId, compiled.preview.title);
|
|
218
|
+
return titles;
|
|
219
|
+
}
|
|
220
|
+
const def = compiled.definition;
|
|
221
|
+
const steps = Array.isArray(def?.['steps']) ? def['steps'] : [];
|
|
222
|
+
for (const step of steps) {
|
|
223
|
+
if (typeof step['id'] === 'string' && typeof step['title'] === 'string') {
|
|
224
|
+
titles.set(step['id'], step['title']);
|
|
225
|
+
}
|
|
226
|
+
if (step['type'] === 'loop' && Array.isArray(step['body'])) {
|
|
227
|
+
for (const bodyStep of step['body']) {
|
|
228
|
+
if (typeof bodyStep['id'] === 'string' && typeof bodyStep['title'] === 'string') {
|
|
229
|
+
titles.set(bodyStep['id'], bodyStep['title']);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return titles;
|
|
235
|
+
}
|
|
236
|
+
const TITLE_CONTEXT_KEYS = ['goal', 'taskDescription', 'mrTitle', 'prTitle', 'ticketTitle', 'problem'];
|
|
237
|
+
function deriveSessionTitle(events) {
|
|
238
|
+
const contextRes = (0, run_context_js_1.projectRunContextV2)(events);
|
|
239
|
+
if (contextRes.isOk()) {
|
|
240
|
+
for (const runCtx of Object.values(contextRes.value.byRunId)) {
|
|
241
|
+
for (const key of TITLE_CONTEXT_KEYS) {
|
|
242
|
+
const val = runCtx.context[key];
|
|
243
|
+
if (typeof val === 'string' && val.trim().length > 0) {
|
|
244
|
+
return truncateTitle(val.trim());
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const title = extractTitleFromFirstRecap(events);
|
|
250
|
+
if (title)
|
|
251
|
+
return title;
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
function extractTitleFromFirstRecap(events) {
|
|
255
|
+
const outputsRes = (0, node_outputs_js_1.projectNodeOutputsV2)(events);
|
|
256
|
+
if (outputsRes.isErr())
|
|
257
|
+
return null;
|
|
258
|
+
let rootNodeId = null;
|
|
259
|
+
let minIndex = Infinity;
|
|
260
|
+
for (const e of events) {
|
|
261
|
+
if (e.kind === constants_js_1.EVENT_KIND.NODE_CREATED && e.eventIndex < minIndex) {
|
|
262
|
+
minIndex = e.eventIndex;
|
|
263
|
+
rootNodeId = e.scope.nodeId;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (!rootNodeId)
|
|
267
|
+
return null;
|
|
268
|
+
const nodeOutputs = outputsRes.value.nodesById[rootNodeId];
|
|
269
|
+
if (!nodeOutputs)
|
|
270
|
+
return null;
|
|
271
|
+
const recaps = nodeOutputs.currentByChannel[constants_js_1.OUTPUT_CHANNEL.RECAP];
|
|
272
|
+
const first = recaps?.[0];
|
|
273
|
+
if (!first || first.payload.payloadKind !== constants_js_1.PAYLOAD_KIND.NOTES)
|
|
274
|
+
return null;
|
|
275
|
+
return extractDescriptiveText(first.payload.notesMarkdown);
|
|
276
|
+
}
|
|
277
|
+
function extractDescriptiveText(markdown) {
|
|
278
|
+
const lines = markdown.split('\n').map((l) => l.trim()).filter((l) => l.length > 0);
|
|
279
|
+
for (const line of lines) {
|
|
280
|
+
if (/^#{1,3}\s/.test(line))
|
|
281
|
+
continue;
|
|
282
|
+
if (/^[-_*]{3,}$/.test(line))
|
|
283
|
+
continue;
|
|
284
|
+
if (line.startsWith('|'))
|
|
285
|
+
continue;
|
|
286
|
+
const boldLabel = line.match(/^\*{2}[^*]+\*{2}:?\s*(.*)/);
|
|
287
|
+
if (boldLabel) {
|
|
288
|
+
const value = boldLabel[1]?.trim();
|
|
289
|
+
if (value && value.length > 10)
|
|
290
|
+
return truncateTitle(value);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const listBoldLabel = line.match(/^-\s+\*{2}[^*]+\*{2}:?\s*(.*)/);
|
|
294
|
+
if (listBoldLabel) {
|
|
295
|
+
const value = listBoldLabel[1]?.trim();
|
|
296
|
+
if (value && value.length > 10)
|
|
297
|
+
return truncateTitle(value);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (line.length > 10) {
|
|
301
|
+
return truncateTitle(line);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
function extractGitBranch(events) {
|
|
307
|
+
for (const e of events) {
|
|
308
|
+
if (e.kind !== constants_js_1.EVENT_KIND.OBSERVATION_RECORDED)
|
|
309
|
+
continue;
|
|
310
|
+
if (e.data.key === 'git_branch') {
|
|
311
|
+
return e.data.value.value;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
function truncateTitle(text, maxLen = 120) {
|
|
317
|
+
if (text.length <= maxLen)
|
|
318
|
+
return text;
|
|
319
|
+
return text.slice(0, maxLen - 1) + '…';
|
|
320
|
+
}
|
|
321
|
+
function projectSessionSummary(sessionId, truth, completionByRunId, workflowNames, lastModifiedMs) {
|
|
51
322
|
const { events } = truth;
|
|
52
323
|
const health = (0, session_health_js_1.projectSessionHealthV2)(truth);
|
|
53
324
|
if (health.isErr())
|
|
@@ -59,12 +330,16 @@ function projectSessionSummary(sessionId, truth) {
|
|
|
59
330
|
const dag = dagRes.value;
|
|
60
331
|
const statusRes = (0, run_status_signals_js_1.projectRunStatusSignalsV2)(events);
|
|
61
332
|
const gapsRes = (0, gaps_js_1.projectGapsV2)(events);
|
|
333
|
+
const sessionTitle = deriveSessionTitle(events);
|
|
334
|
+
const gitBranch = extractGitBranch(events);
|
|
62
335
|
const runs = Object.values(dag.runsById);
|
|
63
336
|
const run = runs[0];
|
|
64
337
|
if (!run) {
|
|
65
338
|
return {
|
|
66
339
|
sessionId,
|
|
340
|
+
sessionTitle,
|
|
67
341
|
workflowId: null,
|
|
342
|
+
workflowName: null,
|
|
68
343
|
workflowHash: null,
|
|
69
344
|
runId: null,
|
|
70
345
|
status: 'in_progress',
|
|
@@ -74,13 +349,16 @@ function projectSessionSummary(sessionId, truth) {
|
|
|
74
349
|
tipCount: 0,
|
|
75
350
|
hasUnresolvedGaps: false,
|
|
76
351
|
recapSnippet: null,
|
|
352
|
+
gitBranch,
|
|
353
|
+
lastModifiedMs,
|
|
77
354
|
};
|
|
78
355
|
}
|
|
79
356
|
const workflow = run.workflow;
|
|
80
357
|
const workflowId = workflow.kind === 'with_workflow' ? workflow.workflowId : null;
|
|
81
358
|
const workflowHash = workflow.kind === 'with_workflow' ? workflow.workflowHash : null;
|
|
359
|
+
const workflowName = workflowHash ? (workflowNames[workflowHash] ?? null) : null;
|
|
82
360
|
const statusSignals = statusRes.isOk() ? statusRes.value.byRunId[run.runId] : undefined;
|
|
83
|
-
const status = deriveRunStatus(statusSignals?.isBlocked ?? false, statusSignals?.hasUnresolvedCriticalGaps ?? false);
|
|
361
|
+
const status = deriveRunStatus(statusSignals?.isBlocked ?? false, statusSignals?.hasUnresolvedCriticalGaps ?? false, completionByRunId[run.runId] ?? false);
|
|
84
362
|
const hasUnresolvedGaps = gapsRes.isOk()
|
|
85
363
|
? Object.keys(gapsRes.value.unresolvedCriticalByRunId).length > 0
|
|
86
364
|
: false;
|
|
@@ -98,7 +376,9 @@ function projectSessionSummary(sessionId, truth) {
|
|
|
98
376
|
}
|
|
99
377
|
return {
|
|
100
378
|
sessionId,
|
|
379
|
+
sessionTitle,
|
|
101
380
|
workflowId,
|
|
381
|
+
workflowName,
|
|
102
382
|
workflowHash,
|
|
103
383
|
runId: run.runId,
|
|
104
384
|
status,
|
|
@@ -108,21 +388,24 @@ function projectSessionSummary(sessionId, truth) {
|
|
|
108
388
|
tipCount: run.tipNodeIds.length,
|
|
109
389
|
hasUnresolvedGaps,
|
|
110
390
|
recapSnippet,
|
|
391
|
+
gitBranch,
|
|
392
|
+
lastModifiedMs,
|
|
111
393
|
};
|
|
112
394
|
}
|
|
113
|
-
function projectSessionDetail(sessionId, truth) {
|
|
395
|
+
function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, workflowNames) {
|
|
114
396
|
const { events } = truth;
|
|
115
397
|
const health = (0, session_health_js_1.projectSessionHealthV2)(truth);
|
|
116
398
|
const sessionHealth = health.isOk() && health.value.kind === 'healthy' ? 'healthy' : 'corrupt';
|
|
399
|
+
const sessionTitle = deriveSessionTitle(events);
|
|
117
400
|
const dagRes = (0, run_dag_js_1.projectRunDagV2)(events);
|
|
118
401
|
if (dagRes.isErr()) {
|
|
119
|
-
return { sessionId, health: sessionHealth, runs: [] };
|
|
402
|
+
return { sessionId, sessionTitle, health: sessionHealth, runs: [] };
|
|
120
403
|
}
|
|
121
404
|
const statusRes = (0, run_status_signals_js_1.projectRunStatusSignalsV2)(events);
|
|
122
405
|
const gapsRes = (0, gaps_js_1.projectGapsV2)(events);
|
|
123
406
|
const runs = Object.values(dagRes.value.runsById).map((run) => {
|
|
124
407
|
const statusSignals = statusRes.isOk() ? statusRes.value.byRunId[run.runId] : undefined;
|
|
125
|
-
const status = deriveRunStatus(statusSignals?.isBlocked ?? false, statusSignals?.hasUnresolvedCriticalGaps ?? false);
|
|
408
|
+
const status = deriveRunStatus(statusSignals?.isBlocked ?? false, statusSignals?.hasUnresolvedCriticalGaps ?? false, completionByRunId[run.runId] ?? false);
|
|
126
409
|
const tipSet = new Set(run.tipNodeIds);
|
|
127
410
|
const nodes = Object.values(run.nodesById).map((node) => ({
|
|
128
411
|
nodeId: node.nodeId,
|
|
@@ -131,6 +414,7 @@ function projectSessionDetail(sessionId, truth) {
|
|
|
131
414
|
createdAtEventIndex: node.createdAtEventIndex,
|
|
132
415
|
isPreferredTip: node.nodeId === run.preferredTipNodeId,
|
|
133
416
|
isTip: tipSet.has(node.nodeId),
|
|
417
|
+
stepLabel: stepLabels[node.nodeId] ?? null,
|
|
134
418
|
}));
|
|
135
419
|
const edges = run.edges.map((edge) => ({
|
|
136
420
|
edgeKind: edge.edgeKind,
|
|
@@ -139,10 +423,12 @@ function projectSessionDetail(sessionId, truth) {
|
|
|
139
423
|
createdAtEventIndex: edge.createdAtEventIndex,
|
|
140
424
|
}));
|
|
141
425
|
const workflow = run.workflow;
|
|
426
|
+
const wfHash = workflow.kind === 'with_workflow' ? workflow.workflowHash : null;
|
|
142
427
|
return {
|
|
143
428
|
runId: run.runId,
|
|
144
429
|
workflowId: workflow.kind === 'with_workflow' ? workflow.workflowId : null,
|
|
145
|
-
|
|
430
|
+
workflowName: wfHash ? (workflowNames[wfHash] ?? null) : null,
|
|
431
|
+
workflowHash: wfHash,
|
|
146
432
|
preferredTipNodeId: run.preferredTipNodeId,
|
|
147
433
|
nodes,
|
|
148
434
|
edges,
|
|
@@ -153,12 +439,124 @@ function projectSessionDetail(sessionId, truth) {
|
|
|
153
439
|
: false,
|
|
154
440
|
};
|
|
155
441
|
});
|
|
156
|
-
return { sessionId, health: sessionHealth, runs };
|
|
442
|
+
return { sessionId, sessionTitle, health: sessionHealth, runs };
|
|
157
443
|
}
|
|
158
|
-
function deriveRunStatus(isBlocked, hasUnresolvedCriticalGaps) {
|
|
444
|
+
function deriveRunStatus(isBlocked, hasUnresolvedCriticalGaps, isComplete) {
|
|
159
445
|
if (isBlocked)
|
|
160
446
|
return 'blocked';
|
|
161
|
-
if (
|
|
162
|
-
return 'complete_with_gaps';
|
|
447
|
+
if (isComplete)
|
|
448
|
+
return hasUnresolvedCriticalGaps ? 'complete_with_gaps' : 'complete';
|
|
163
449
|
return 'in_progress';
|
|
164
450
|
}
|
|
451
|
+
function projectNodeDetail(events, nodeId, stepLabels) {
|
|
452
|
+
const dagRes = (0, run_dag_js_1.projectRunDagV2)(events);
|
|
453
|
+
if (dagRes.isErr())
|
|
454
|
+
return null;
|
|
455
|
+
const { node, run } = findNodeInDag(dagRes.value, nodeId) ?? {};
|
|
456
|
+
if (!node || !run)
|
|
457
|
+
return null;
|
|
458
|
+
const tipSet = new Set(run.tipNodeIds);
|
|
459
|
+
const recapMarkdown = extractRecapMarkdown(events, nodeId);
|
|
460
|
+
const artifacts = extractArtifacts(events, nodeId);
|
|
461
|
+
const advanceOutcome = extractAdvanceOutcome(events, nodeId);
|
|
462
|
+
const validations = extractValidations(events, nodeId);
|
|
463
|
+
const gaps = extractGaps(events, nodeId);
|
|
464
|
+
return {
|
|
465
|
+
nodeId: node.nodeId,
|
|
466
|
+
nodeKind: node.nodeKind,
|
|
467
|
+
parentNodeId: node.parentNodeId,
|
|
468
|
+
createdAtEventIndex: node.createdAtEventIndex,
|
|
469
|
+
isPreferredTip: node.nodeId === run.preferredTipNodeId,
|
|
470
|
+
isTip: tipSet.has(node.nodeId),
|
|
471
|
+
stepLabel: stepLabels[node.nodeId] ?? null,
|
|
472
|
+
recapMarkdown,
|
|
473
|
+
artifacts,
|
|
474
|
+
advanceOutcome,
|
|
475
|
+
validations,
|
|
476
|
+
gaps,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function findNodeInDag(dag, nodeId) {
|
|
480
|
+
for (const run of Object.values(dag.runsById)) {
|
|
481
|
+
const node = run.nodesById[nodeId];
|
|
482
|
+
if (node)
|
|
483
|
+
return { node, run };
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
function extractRecapMarkdown(events, nodeId) {
|
|
488
|
+
const outputsRes = (0, node_outputs_js_1.projectNodeOutputsV2)(events);
|
|
489
|
+
if (outputsRes.isErr())
|
|
490
|
+
return null;
|
|
491
|
+
const nodeOutputs = outputsRes.value.nodesById[nodeId];
|
|
492
|
+
if (!nodeOutputs)
|
|
493
|
+
return null;
|
|
494
|
+
const recaps = nodeOutputs.currentByChannel[constants_js_1.OUTPUT_CHANNEL.RECAP];
|
|
495
|
+
const latest = recaps?.at(-1);
|
|
496
|
+
if (latest && latest.payload.payloadKind === constants_js_1.PAYLOAD_KIND.NOTES) {
|
|
497
|
+
return latest.payload.notesMarkdown;
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
function extractArtifacts(events, nodeId) {
|
|
502
|
+
const artifactsRes = (0, artifacts_js_1.projectArtifactsV2)(events);
|
|
503
|
+
if (artifactsRes.isErr())
|
|
504
|
+
return [];
|
|
505
|
+
const nodeArtifacts = artifactsRes.value.byNodeId[nodeId];
|
|
506
|
+
if (!nodeArtifacts)
|
|
507
|
+
return [];
|
|
508
|
+
return nodeArtifacts.artifacts.map((a) => ({
|
|
509
|
+
sha256: a.sha256,
|
|
510
|
+
contentType: a.contentType,
|
|
511
|
+
byteLength: a.byteLength,
|
|
512
|
+
content: a.content,
|
|
513
|
+
}));
|
|
514
|
+
}
|
|
515
|
+
function extractAdvanceOutcome(events, nodeId) {
|
|
516
|
+
const outcomesRes = (0, advance_outcomes_js_1.projectAdvanceOutcomesV2)(events);
|
|
517
|
+
if (outcomesRes.isErr())
|
|
518
|
+
return null;
|
|
519
|
+
const outcome = outcomesRes.value.byNodeId[nodeId];
|
|
520
|
+
if (!outcome)
|
|
521
|
+
return null;
|
|
522
|
+
return {
|
|
523
|
+
attemptId: outcome.latestAttemptId,
|
|
524
|
+
kind: outcome.outcome.kind,
|
|
525
|
+
recordedAtEventIndex: outcome.recordedAtEventIndex,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
function extractValidations(events, nodeId) {
|
|
529
|
+
const results = [];
|
|
530
|
+
for (const e of events) {
|
|
531
|
+
if (e.kind !== constants_js_1.EVENT_KIND.VALIDATION_PERFORMED)
|
|
532
|
+
continue;
|
|
533
|
+
if (e.scope.nodeId !== nodeId)
|
|
534
|
+
continue;
|
|
535
|
+
results.push({
|
|
536
|
+
validationId: e.data.validationId,
|
|
537
|
+
attemptId: e.data.attemptId,
|
|
538
|
+
contractRef: e.data.contractRef,
|
|
539
|
+
outcome: (e.data.result.valid ? 'pass' : 'fail'),
|
|
540
|
+
issues: [...e.data.result.issues],
|
|
541
|
+
suggestions: [...e.data.result.suggestions],
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
return results;
|
|
545
|
+
}
|
|
546
|
+
function extractGaps(events, nodeId) {
|
|
547
|
+
const gapsRes = (0, gaps_js_1.projectGapsV2)(events);
|
|
548
|
+
if (gapsRes.isErr())
|
|
549
|
+
return [];
|
|
550
|
+
const gaps = [];
|
|
551
|
+
for (const gap of Object.values(gapsRes.value.byGapId)) {
|
|
552
|
+
if (gap.nodeId !== nodeId)
|
|
553
|
+
continue;
|
|
554
|
+
gaps.push({
|
|
555
|
+
gapId: gap.gapId,
|
|
556
|
+
severity: gap.severity,
|
|
557
|
+
summary: gap.summary,
|
|
558
|
+
isResolved: gapsRes.value.resolvedGapIds.has(gap.gapId),
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
return gaps;
|
|
562
|
+
}
|
|
@@ -2,7 +2,9 @@ export type ConsoleRunStatus = 'in_progress' | 'complete' | 'complete_with_gaps'
|
|
|
2
2
|
export type ConsoleSessionHealth = 'healthy' | 'corrupt';
|
|
3
3
|
export interface ConsoleSessionSummary {
|
|
4
4
|
readonly sessionId: string;
|
|
5
|
+
readonly sessionTitle: string | null;
|
|
5
6
|
readonly workflowId: string | null;
|
|
7
|
+
readonly workflowName: string | null;
|
|
6
8
|
readonly workflowHash: string | null;
|
|
7
9
|
readonly runId: string | null;
|
|
8
10
|
readonly status: ConsoleRunStatus;
|
|
@@ -12,6 +14,8 @@ export interface ConsoleSessionSummary {
|
|
|
12
14
|
readonly tipCount: number;
|
|
13
15
|
readonly hasUnresolvedGaps: boolean;
|
|
14
16
|
readonly recapSnippet: string | null;
|
|
17
|
+
readonly gitBranch: string | null;
|
|
18
|
+
readonly lastModifiedMs: number;
|
|
15
19
|
}
|
|
16
20
|
export interface ConsoleSessionListResponse {
|
|
17
21
|
readonly sessions: readonly ConsoleSessionSummary[];
|
|
@@ -24,6 +28,7 @@ export interface ConsoleDagNode {
|
|
|
24
28
|
readonly createdAtEventIndex: number;
|
|
25
29
|
readonly isPreferredTip: boolean;
|
|
26
30
|
readonly isTip: boolean;
|
|
31
|
+
readonly stepLabel: string | null;
|
|
27
32
|
}
|
|
28
33
|
export interface ConsoleDagEdge {
|
|
29
34
|
readonly edgeKind: 'acked_step' | 'checkpoint';
|
|
@@ -34,6 +39,7 @@ export interface ConsoleDagEdge {
|
|
|
34
39
|
export interface ConsoleDagRun {
|
|
35
40
|
readonly runId: string;
|
|
36
41
|
readonly workflowId: string | null;
|
|
42
|
+
readonly workflowName: string | null;
|
|
37
43
|
readonly workflowHash: string | null;
|
|
38
44
|
readonly preferredTipNodeId: string | null;
|
|
39
45
|
readonly nodes: readonly ConsoleDagNode[];
|
|
@@ -44,6 +50,48 @@ export interface ConsoleDagRun {
|
|
|
44
50
|
}
|
|
45
51
|
export interface ConsoleSessionDetail {
|
|
46
52
|
readonly sessionId: string;
|
|
53
|
+
readonly sessionTitle: string | null;
|
|
47
54
|
readonly health: ConsoleSessionHealth;
|
|
48
55
|
readonly runs: readonly ConsoleDagRun[];
|
|
49
56
|
}
|
|
57
|
+
export type ConsoleValidationOutcome = 'pass' | 'fail';
|
|
58
|
+
export interface ConsoleValidationResult {
|
|
59
|
+
readonly validationId: string;
|
|
60
|
+
readonly attemptId: string;
|
|
61
|
+
readonly contractRef: string;
|
|
62
|
+
readonly outcome: ConsoleValidationOutcome;
|
|
63
|
+
readonly issues: readonly string[];
|
|
64
|
+
readonly suggestions: readonly string[];
|
|
65
|
+
}
|
|
66
|
+
export type ConsoleAdvanceOutcomeKind = 'advanced' | 'blocked';
|
|
67
|
+
export interface ConsoleAdvanceOutcome {
|
|
68
|
+
readonly attemptId: string;
|
|
69
|
+
readonly kind: ConsoleAdvanceOutcomeKind;
|
|
70
|
+
readonly recordedAtEventIndex: number;
|
|
71
|
+
}
|
|
72
|
+
export interface ConsoleNodeGap {
|
|
73
|
+
readonly gapId: string;
|
|
74
|
+
readonly severity: 'critical' | 'non_critical';
|
|
75
|
+
readonly summary: string;
|
|
76
|
+
readonly isResolved: boolean;
|
|
77
|
+
}
|
|
78
|
+
export interface ConsoleArtifact {
|
|
79
|
+
readonly sha256: string;
|
|
80
|
+
readonly contentType: string;
|
|
81
|
+
readonly byteLength: number;
|
|
82
|
+
readonly content: unknown;
|
|
83
|
+
}
|
|
84
|
+
export interface ConsoleNodeDetail {
|
|
85
|
+
readonly nodeId: string;
|
|
86
|
+
readonly nodeKind: 'step' | 'checkpoint' | 'blocked_attempt';
|
|
87
|
+
readonly parentNodeId: string | null;
|
|
88
|
+
readonly createdAtEventIndex: number;
|
|
89
|
+
readonly isPreferredTip: boolean;
|
|
90
|
+
readonly isTip: boolean;
|
|
91
|
+
readonly stepLabel: string | null;
|
|
92
|
+
readonly recapMarkdown: string | null;
|
|
93
|
+
readonly artifacts: readonly ConsoleArtifact[];
|
|
94
|
+
readonly advanceOutcome: ConsoleAdvanceOutcome | null;
|
|
95
|
+
readonly validations: readonly ConsoleValidationResult[];
|
|
96
|
+
readonly gaps: readonly ConsoleNodeGap[];
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -332,7 +332,7 @@
|
|
|
332
332
|
"contractRef": { "type": "string", "minLength": 1 },
|
|
333
333
|
"loopId": { "type": "string", "minLength": 1 }
|
|
334
334
|
},
|
|
335
|
-
"required": ["kind", "contractRef"
|
|
335
|
+
"required": ["kind", "contractRef"],
|
|
336
336
|
"additionalProperties": false
|
|
337
337
|
},
|
|
338
338
|
{
|
|
@@ -351,10 +351,14 @@
|
|
|
351
351
|
"allOf": [
|
|
352
352
|
{
|
|
353
353
|
"if": {
|
|
354
|
-
"properties": { "type": { "enum": ["while", "until"] } }
|
|
354
|
+
"properties": { "type": { "enum": ["while", "until"] } },
|
|
355
|
+
"required": ["type"]
|
|
355
356
|
},
|
|
356
357
|
"then": {
|
|
357
|
-
"
|
|
358
|
+
"anyOf": [
|
|
359
|
+
{ "required": ["condition"] },
|
|
360
|
+
{ "required": ["conditionSource"] }
|
|
361
|
+
]
|
|
358
362
|
}
|
|
359
363
|
},
|
|
360
364
|
{
|