@mingchuno/agent-workflows 0.1.0 → 0.3.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 +37 -6
- package/dist/src/adapters/agents.js +6 -3
- package/dist/src/adapters/hosting.js +16 -10
- package/dist/src/adapters/sdk-protocol.d.ts +3 -3
- package/dist/src/adapters/sdk-protocol.js +9 -7
- package/dist/src/attribution.d.ts +9 -0
- package/dist/src/attribution.js +57 -0
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +80 -25
- package/dist/src/config.d.ts +24 -30
- package/dist/src/config.js +32 -27
- package/dist/src/defaults.d.ts +2 -0
- package/dist/src/defaults.js +2 -0
- package/dist/src/domain.d.ts +31 -4
- package/dist/src/domain.js +10 -3
- package/dist/src/evidence.d.ts +54 -0
- package/dist/src/evidence.js +214 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/invocation.d.ts +25 -0
- package/dist/src/invocation.js +166 -0
- package/dist/src/operations.d.ts +8 -2
- package/dist/src/operations.js +100 -136
- package/dist/src/prompts.d.ts +28 -0
- package/dist/src/prompts.js +63 -0
- package/dist/src/recovery.d.ts +19 -0
- package/dist/src/recovery.js +99 -0
- package/dist/src/runner.d.ts +7 -0
- package/dist/src/runner.js +170 -18
- package/dist/src/runtime/process.d.ts +2 -0
- package/dist/src/runtime/process.js +41 -12
- package/dist/src/store.d.ts +21 -2
- package/dist/src/store.js +122 -1
- package/dist/src/tui/actions.d.ts +16 -0
- package/dist/src/tui/actions.js +23 -0
- package/dist/src/tui/constants.d.ts +6 -0
- package/dist/src/tui/constants.js +3 -0
- package/dist/src/{tui-data.d.ts → tui/data.d.ts} +10 -7
- package/dist/src/tui/data.js +146 -0
- package/dist/src/tui/dialogs.d.ts +17 -0
- package/dist/src/tui/dialogs.js +149 -0
- package/dist/src/tui/format.d.ts +7 -0
- package/dist/src/tui/format.js +62 -0
- package/dist/src/tui/index.d.ts +3 -0
- package/dist/src/tui/index.js +2 -0
- package/dist/src/tui/layout.d.ts +25 -0
- package/dist/src/tui/layout.js +36 -0
- package/dist/src/tui/log-file.d.ts +26 -0
- package/dist/src/tui/log-file.js +156 -0
- package/dist/src/tui/log.d.ts +11 -0
- package/dist/src/tui/log.js +90 -0
- package/dist/src/tui/monitor.d.ts +10 -0
- package/dist/src/tui/monitor.js +284 -0
- package/dist/src/tui/notifications.d.ts +23 -0
- package/dist/src/tui/notifications.js +104 -0
- package/dist/src/tui/text.d.ts +3 -0
- package/dist/src/tui/text.js +10 -0
- package/dist/src/tui/use-log-controller.d.ts +27 -0
- package/dist/src/tui/use-log-controller.js +192 -0
- package/dist/src/tui/views.d.ts +25 -0
- package/dist/src/tui/views.js +327 -0
- package/dist/src/workspace.js +21 -8
- package/docs/api.md +132 -8
- package/docs/architecture.md +21 -4
- package/docs/configuration.md +181 -8
- package/docs/database.md +7 -0
- package/docs/operations.md +160 -5
- package/docs/providers.md +58 -2
- package/docs/releases.md +34 -79
- package/examples/config.ts +6 -6
- package/examples/run.ts +4 -1
- package/package.json +4 -2
- package/dist/src/tui-data.js +0 -89
- package/dist/src/tui.d.ts +0 -5
- package/dist/src/tui.js +0 -69
- package/docs/acceptance.md +0 -35
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import stringWidth from "string-width";
|
|
4
|
+
import { recoveryUnavailable } from "../recovery.js";
|
|
5
|
+
import { cells, colorFor, duration, elapsedRun, executionDuration, wrapLines, } from "./format.js";
|
|
6
|
+
// The detail panel contributes four columns of border and padding.
|
|
7
|
+
const detailWideBreakpoint = 136;
|
|
8
|
+
const detailLabelWidth = 19;
|
|
9
|
+
const hangingLabelMarker = "\u{e000}";
|
|
10
|
+
const conciseErrorWidth = 120;
|
|
11
|
+
const detailColumnGap = " │ ";
|
|
12
|
+
function localTimestamp(value) {
|
|
13
|
+
if (!value)
|
|
14
|
+
return "not recorded";
|
|
15
|
+
const date = new Date(value);
|
|
16
|
+
if (!Number.isFinite(date.valueOf()))
|
|
17
|
+
return "unavailable";
|
|
18
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
19
|
+
dateStyle: "medium",
|
|
20
|
+
timeStyle: "medium",
|
|
21
|
+
}).format(date);
|
|
22
|
+
}
|
|
23
|
+
function profileValue(value, key) {
|
|
24
|
+
if (!value || typeof value !== "object")
|
|
25
|
+
return undefined;
|
|
26
|
+
const candidate = value[key];
|
|
27
|
+
return typeof candidate === "string" && candidate ? candidate : undefined;
|
|
28
|
+
}
|
|
29
|
+
function readableProfile(value, fallbackProvider) {
|
|
30
|
+
return [
|
|
31
|
+
profileValue(value, "provider") ??
|
|
32
|
+
fallbackProvider ??
|
|
33
|
+
"provider unavailable",
|
|
34
|
+
profileValue(value, "model") ?? "model unavailable",
|
|
35
|
+
profileValue(value, "reasoningEffort") ?? "reasoning unavailable",
|
|
36
|
+
].join(" / ");
|
|
37
|
+
}
|
|
38
|
+
function sameProfile(requested, effective, provider) {
|
|
39
|
+
return (readableProfile(requested, provider) ===
|
|
40
|
+
readableProfile(effective, provider));
|
|
41
|
+
}
|
|
42
|
+
function json(value) {
|
|
43
|
+
return JSON.stringify(value) ?? "unavailable";
|
|
44
|
+
}
|
|
45
|
+
function conciseError(run) {
|
|
46
|
+
const error = run.error ?? run.executions?.at(-1)?.error;
|
|
47
|
+
if (!error)
|
|
48
|
+
return run.outcome === "cancelled"
|
|
49
|
+
? "Run was cancelled."
|
|
50
|
+
: run.outcome === "blocked"
|
|
51
|
+
? "Run is blocked."
|
|
52
|
+
: "Run requires operator attention.";
|
|
53
|
+
const firstLine = error.split("\n", 1)[0].replace(/^Error:\s*/i, "");
|
|
54
|
+
const summary = cells(firstLine, conciseErrorWidth);
|
|
55
|
+
return cells(firstLine, conciseErrorWidth + 1) === summary
|
|
56
|
+
? summary
|
|
57
|
+
: `${cells(firstLine, conciseErrorWidth - 1)}…`;
|
|
58
|
+
}
|
|
59
|
+
function label(label, value) {
|
|
60
|
+
return `${hangingLabelMarker}${label.padEnd(detailLabelWidth)}${value}`;
|
|
61
|
+
}
|
|
62
|
+
export function wrapDetailLines(lines, width) {
|
|
63
|
+
return lines.flatMap((line) => {
|
|
64
|
+
if (!line.startsWith(hangingLabelMarker))
|
|
65
|
+
return wrapLines([line], width);
|
|
66
|
+
const content = line.slice(hangingLabelMarker.length);
|
|
67
|
+
const prefix = content.slice(0, detailLabelWidth);
|
|
68
|
+
const value = content.slice(detailLabelWidth);
|
|
69
|
+
const wrappedValue = wrapLines([value], Math.max(1, width - detailLabelWidth));
|
|
70
|
+
return wrappedValue.map((part, index) => index === 0
|
|
71
|
+
? `${prefix}${part}`
|
|
72
|
+
: `${" ".repeat(detailLabelWidth)}${part}`);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function validationLines(run, now) {
|
|
76
|
+
return [
|
|
77
|
+
"VALIDATION",
|
|
78
|
+
...(run.validation?.length
|
|
79
|
+
? run.validation.flatMap((check) => [
|
|
80
|
+
`${check.command} ${check.args.join(" ")} · exit ${check.exitCode} · ${duration(check.startedAt, check.finishedAt, now)}`,
|
|
81
|
+
])
|
|
82
|
+
: ["not recorded"]),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
function validationSummary(run) {
|
|
86
|
+
const checks = run.validation ?? [];
|
|
87
|
+
if (!checks.length)
|
|
88
|
+
return "not recorded";
|
|
89
|
+
const successful = checks.filter((check) => check.exitCode === 0).length;
|
|
90
|
+
const failed = checks.length - successful;
|
|
91
|
+
return [
|
|
92
|
+
`${checks.length} recorded`,
|
|
93
|
+
successful ? `${successful} exit 0` : "",
|
|
94
|
+
failed ? `${failed} nonzero` : "",
|
|
95
|
+
]
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.join(" · ");
|
|
98
|
+
}
|
|
99
|
+
function sessionLines(sessions, now) {
|
|
100
|
+
return [
|
|
101
|
+
"AGENT SESSIONS",
|
|
102
|
+
...(sessions.length
|
|
103
|
+
? sessions.flatMap((session) => {
|
|
104
|
+
const effective = readableProfile(session.effective, session.provider);
|
|
105
|
+
const profile = sameProfile(session.requested, session.effective, session.provider)
|
|
106
|
+
? `Effective ${effective}`
|
|
107
|
+
: `Requested ${readableProfile(session.requested, session.provider)} → Effective ${effective}`;
|
|
108
|
+
return [
|
|
109
|
+
`${session.step} · invocation ${session.attempt} · ${session.outcome} · ${duration(session.startedAt, session.finishedAt, now)}`,
|
|
110
|
+
profile,
|
|
111
|
+
];
|
|
112
|
+
})
|
|
113
|
+
: ["not recorded"]),
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
function executionLines(run, now) {
|
|
117
|
+
const executions = [...(run.executions ?? [])].reverse();
|
|
118
|
+
if (!executions.length)
|
|
119
|
+
return ["CURRENT EXECUTION", "not started"];
|
|
120
|
+
return executions.flatMap((execution, index) => {
|
|
121
|
+
const executionTime = execution.startedAt
|
|
122
|
+
? executionDuration(execution, now).replace("—", "unavailable")
|
|
123
|
+
: "not started";
|
|
124
|
+
const queueWait = duration(execution.createdAt, execution.startedAt ?? execution.finishedAt, now);
|
|
125
|
+
const recovery = execution.recoveryOf
|
|
126
|
+
? [
|
|
127
|
+
label("Recovery", "continued from prior execution"),
|
|
128
|
+
label("Recovery gap", duration(run.executions?.find((candidate) => candidate.id === execution.recoveryOf)?.finishedAt, execution.createdAt, now).replace("—", "unavailable")),
|
|
129
|
+
label("Reused steps", execution.reusedSteps?.length
|
|
130
|
+
? execution.reusedSteps.join(", ")
|
|
131
|
+
: "none recorded"),
|
|
132
|
+
]
|
|
133
|
+
: [];
|
|
134
|
+
const lines = index === 0
|
|
135
|
+
? [
|
|
136
|
+
"CURRENT EXECUTION",
|
|
137
|
+
label("Outcome", execution.outcome),
|
|
138
|
+
label("Phase", execution.phase),
|
|
139
|
+
label("Execution duration", executionTime),
|
|
140
|
+
label("Queue wait", queueWait),
|
|
141
|
+
label("Created", localTimestamp(execution.createdAt)),
|
|
142
|
+
label("Started", localTimestamp(execution.startedAt)),
|
|
143
|
+
label("Finished", localTimestamp(execution.finishedAt)),
|
|
144
|
+
...recovery,
|
|
145
|
+
]
|
|
146
|
+
: [
|
|
147
|
+
`PRIOR EXECUTION ${executions.length - index}`,
|
|
148
|
+
`${execution.outcome} · ${execution.phase} · duration ${executionTime} · queue ${queueWait}`,
|
|
149
|
+
`${localTimestamp(execution.startedAt)} → ${localTimestamp(execution.finishedAt)}`,
|
|
150
|
+
...recovery,
|
|
151
|
+
];
|
|
152
|
+
return index < executions.length - 1 ? [...lines, ""] : lines;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function padToWidth(value, width) {
|
|
156
|
+
return `${value}${" ".repeat(Math.max(0, width - stringWidth(value)))}`;
|
|
157
|
+
}
|
|
158
|
+
function composeDetailColumns(left, right, width) {
|
|
159
|
+
const leftWidth = Math.floor((width - detailColumnGap.length) * 0.58);
|
|
160
|
+
const rightWidth = width - leftWidth - detailColumnGap.length;
|
|
161
|
+
const leftLines = wrapDetailLines(left, leftWidth);
|
|
162
|
+
const rightLines = wrapDetailLines(right, rightWidth);
|
|
163
|
+
return Array.from({ length: Math.max(leftLines.length, rightLines.length) }, (_, index) => `${padToWidth(leftLines[index] ?? "", leftWidth)}${detailColumnGap}${rightLines[index] ?? ""}`);
|
|
164
|
+
}
|
|
165
|
+
function stepText(event) {
|
|
166
|
+
if (!event)
|
|
167
|
+
return "No steps recorded";
|
|
168
|
+
const payload = event.payload;
|
|
169
|
+
return `${payload.name ?? "step"} · ${payload.status ?? "recorded"} · attempt ${payload.attempt ?? "—"}`;
|
|
170
|
+
}
|
|
171
|
+
export function summaryLines(run, now, event) {
|
|
172
|
+
return [
|
|
173
|
+
`#${run.issue.number} ${run.issue.title}`,
|
|
174
|
+
`${run.outcome.toUpperCase()} · ${run.phase}`,
|
|
175
|
+
`Attempt ${run.attempt} · Execution ${executionDuration(run.executions?.at(-1), now)}`,
|
|
176
|
+
"",
|
|
177
|
+
"PROGRESS",
|
|
178
|
+
stepText(event),
|
|
179
|
+
"",
|
|
180
|
+
"VALIDATION",
|
|
181
|
+
...(run.validation?.map((check) => `${check.command}: exit ${check.exitCode}`) ?? ["No checks recorded"]),
|
|
182
|
+
"",
|
|
183
|
+
"NEXT ACTION",
|
|
184
|
+
...(run.error
|
|
185
|
+
? [
|
|
186
|
+
`Error: ${run.error.split("\n")[0]}`,
|
|
187
|
+
"Enter details for the full error and recovery evidence",
|
|
188
|
+
]
|
|
189
|
+
: [
|
|
190
|
+
run.outcome === "running"
|
|
191
|
+
? "Workflow is running"
|
|
192
|
+
: run.outcome === "queued"
|
|
193
|
+
? "Waiting for runner and project intake"
|
|
194
|
+
: ["failed", "blocked", "cancelled"].includes(run.outcome)
|
|
195
|
+
? "Inspect details before retry or recovery"
|
|
196
|
+
: "Workflow finished; inspect the outcome",
|
|
197
|
+
]),
|
|
198
|
+
...(run.change ? [`Change request: ${run.change.url}`] : []),
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
export function detailLines(run, sessions, now, recoveryReason = recoveryUnavailable(run), width = 76, available = {
|
|
202
|
+
stop: false,
|
|
203
|
+
retry: false,
|
|
204
|
+
recover: false,
|
|
205
|
+
}) {
|
|
206
|
+
const validation = validationSummary(run);
|
|
207
|
+
const recovery = run.outcome === "failed"
|
|
208
|
+
? recoveryReason
|
|
209
|
+
? "restricted"
|
|
210
|
+
: "eligible"
|
|
211
|
+
: "not applicable";
|
|
212
|
+
const attention = ["failed", "blocked", "cancelled"].includes(run.outcome)
|
|
213
|
+
? [
|
|
214
|
+
"",
|
|
215
|
+
"ATTENTION",
|
|
216
|
+
label("Problem", conciseError(run)),
|
|
217
|
+
label("Recovery", recoveryReason ?? "Eligible; runner checks still apply"),
|
|
218
|
+
label("Action", [
|
|
219
|
+
available.recover ? "c recover" : "",
|
|
220
|
+
available.retry ? "r retry" : "",
|
|
221
|
+
available.stop ? "s stop" : "",
|
|
222
|
+
]
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
.join(" · ") || "none available"),
|
|
225
|
+
]
|
|
226
|
+
: [];
|
|
227
|
+
const executions = executionLines(run, now);
|
|
228
|
+
const evidence = [
|
|
229
|
+
...validationLines(run, now),
|
|
230
|
+
"",
|
|
231
|
+
...sessionLines(sessions, now),
|
|
232
|
+
];
|
|
233
|
+
const evidenceArea = width >= detailWideBreakpoint
|
|
234
|
+
? composeDetailColumns(executions, evidence, width)
|
|
235
|
+
: [...executions, "", ...evidence];
|
|
236
|
+
const executionsByRecency = [...(run.executions ?? [])].reverse();
|
|
237
|
+
const executionErrors = executionsByRecency.filter((execution) => execution.error);
|
|
238
|
+
const diagnostics = run.error || executionErrors.length
|
|
239
|
+
? [
|
|
240
|
+
"",
|
|
241
|
+
"DIAGNOSTICS",
|
|
242
|
+
...(run.error ? [label("Run error", run.error)] : []),
|
|
243
|
+
...executionErrors.map((execution, index) => label(index === 0 ? "Current error" : "Prior error", execution.error)),
|
|
244
|
+
]
|
|
245
|
+
: [];
|
|
246
|
+
const technical = [
|
|
247
|
+
"",
|
|
248
|
+
"TECHNICAL DETAILS",
|
|
249
|
+
label("Run ID", run.id),
|
|
250
|
+
label("Issue URL", run.issue.url || "unavailable"),
|
|
251
|
+
label("Checkout", run.checkout || "unavailable"),
|
|
252
|
+
label("Task key", run.taskKey),
|
|
253
|
+
label("Created ISO", run.createdAt),
|
|
254
|
+
label("Updated ISO", run.updatedAt),
|
|
255
|
+
...(run.retryOf ? [label("Retry of", run.retryOf)] : []),
|
|
256
|
+
...(run.base ? [label("Base revision", run.base)] : []),
|
|
257
|
+
...(run.head ? [label("Head revision", run.head)] : []),
|
|
258
|
+
...(run.change ? [label("Change URL", run.change.url)] : []),
|
|
259
|
+
...executionsByRecency.flatMap((execution, index) => [
|
|
260
|
+
"",
|
|
261
|
+
`Execution ${executionsByRecency.length - index}`,
|
|
262
|
+
label("Execution ID", execution.id),
|
|
263
|
+
label("Created ISO", execution.createdAt),
|
|
264
|
+
label("Started ISO", execution.startedAt ?? "not recorded"),
|
|
265
|
+
label("Finished ISO", execution.finishedAt ?? "not recorded"),
|
|
266
|
+
...(execution.recoveryOf
|
|
267
|
+
? [label("Recovery source", execution.recoveryOf)]
|
|
268
|
+
: []),
|
|
269
|
+
]),
|
|
270
|
+
...(run.validation?.flatMap((check, index) => [
|
|
271
|
+
"",
|
|
272
|
+
`Validation ${index + 1}`,
|
|
273
|
+
label("Log path", check.log),
|
|
274
|
+
label("Started ISO", check.startedAt),
|
|
275
|
+
label("Finished ISO", check.finishedAt),
|
|
276
|
+
]) ?? []),
|
|
277
|
+
...sessions.flatMap((session, index) => [
|
|
278
|
+
"",
|
|
279
|
+
`Agent session ${index + 1}`,
|
|
280
|
+
label("Invocation ID", session.id),
|
|
281
|
+
label("Session ID", session.sessionId ?? session.sessionState),
|
|
282
|
+
label("Provider", session.provider),
|
|
283
|
+
label("Log path", session.log),
|
|
284
|
+
label("Started ISO", session.startedAt),
|
|
285
|
+
label("Finished ISO", session.finishedAt ?? "not recorded"),
|
|
286
|
+
label("Requested profile", json(session.requested)),
|
|
287
|
+
label("Effective profile", json(session.effective)),
|
|
288
|
+
]),
|
|
289
|
+
];
|
|
290
|
+
return [
|
|
291
|
+
`#${run.issue.number} ${run.issue.title}`,
|
|
292
|
+
label("Outcome", run.outcome),
|
|
293
|
+
label("Phase", run.phase),
|
|
294
|
+
label("Attempt", String(run.attempt)),
|
|
295
|
+
label("Total elapsed", elapsedRun(run, now).replace("—", "unavailable")),
|
|
296
|
+
label("Branch", run.branch || "unavailable"),
|
|
297
|
+
label("Health", `Error ${run.error || run.executions?.some((execution) => execution.error) ? "recorded" : "none"} · Validation ${validation} · Recovery ${recovery}`),
|
|
298
|
+
...attention,
|
|
299
|
+
"",
|
|
300
|
+
...evidenceArea,
|
|
301
|
+
...diagnostics,
|
|
302
|
+
...technical,
|
|
303
|
+
];
|
|
304
|
+
}
|
|
305
|
+
export function Lines({ lines, width, height, offset = 0, outcome, }) {
|
|
306
|
+
const wrapped = wrapDetailLines(lines, width);
|
|
307
|
+
const start = Math.min(offset, Math.max(0, wrapped.length - height));
|
|
308
|
+
const summaryOutcomeIndex = wrapped.findIndex((line) => line.startsWith("Outcome"));
|
|
309
|
+
return (_jsx(Box, { flexDirection: "column", width: width, height: height, overflow: "hidden", children: wrapped.slice(start, start + height).map((line, index) => {
|
|
310
|
+
const absoluteIndex = start + index;
|
|
311
|
+
const divider = line.indexOf(detailColumnGap);
|
|
312
|
+
const left = divider >= 0 ? line.slice(0, divider) : line;
|
|
313
|
+
const right = divider >= 0
|
|
314
|
+
? line.slice(divider + detailColumnGap.length)
|
|
315
|
+
: undefined;
|
|
316
|
+
const isHeading = (value) => /^[A-Z][A-Z0-9 ]+$/.test(value.trim());
|
|
317
|
+
return (_jsx(Box, { height: 1, flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", color: absoluteIndex === summaryOutcomeIndex
|
|
318
|
+
? colorFor(outcome ?? "")
|
|
319
|
+
: undefined, children: [_jsx(Text, { bold: isHeading(left), children: left || " " }), right !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: detailColumnGap }), _jsx(Text, { bold: isHeading(right), children: right })] })) : null] }) }, `${absoluteIndex}`));
|
|
320
|
+
}) }));
|
|
321
|
+
}
|
|
322
|
+
export function RunList({ runs, selected, width, height, now, }) {
|
|
323
|
+
const count = Math.max(1, Math.floor(height / 3));
|
|
324
|
+
const current = Math.max(0, runs.findIndex((run) => run.id === selected));
|
|
325
|
+
const start = Math.max(0, Math.min(current - Math.floor(count / 2), runs.length - count));
|
|
326
|
+
return (_jsx(Box, { flexDirection: "column", height: height, overflow: "hidden", children: runs.length ? (runs.slice(start, start + count).map((run) => (_jsxs(Box, { flexDirection: "column", height: 3, children: [_jsx(Text, { inverse: run.id === selected, bold: run.id === selected, wrap: "truncate", children: cells(`${run.id === selected ? ">" : " "} #${run.issue.number} ${run.issue.title}`, width) }), _jsx(Text, { color: colorFor(run.outcome), wrap: "truncate", children: cells(` ${run.outcome} · ${run.phase} · ${executionDuration(run.executions?.at(-1), now)}`, width) }), _jsx(Text, { dimColor: true, children: cells(` attempt ${run.attempt}`, width) })] }, run.id)))) : (_jsx(Text, { children: "No runs yet. Waiting for eligible issues." })) }));
|
|
327
|
+
}
|
package/dist/src/workspace.js
CHANGED
|
@@ -3,6 +3,12 @@ import { access, lstat, mkdir, readFile, realpath } from "node:fs/promises";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { BlockedError, } from "./domain.js";
|
|
5
5
|
import { command } from "./runtime/process.js";
|
|
6
|
+
function fileStateDigest(content, mode) {
|
|
7
|
+
return createHash("sha256")
|
|
8
|
+
.update(content)
|
|
9
|
+
.update(String(mode))
|
|
10
|
+
.digest("hex");
|
|
11
|
+
}
|
|
6
12
|
export class ExistingCheckout {
|
|
7
13
|
processDirectories = new Map();
|
|
8
14
|
processDirectory(project) {
|
|
@@ -97,7 +103,7 @@ export class ExistingCheckout {
|
|
|
97
103
|
if (stat.isSymbolicLink())
|
|
98
104
|
throw new BlockedError(`Changed symlink requires manual handling: ${path}`);
|
|
99
105
|
const content = await readFile(absolute);
|
|
100
|
-
files[path] =
|
|
106
|
+
files[path] = fileStateDigest(content, stat.mode);
|
|
101
107
|
hash.update(path).update(content).update(String(stat.mode));
|
|
102
108
|
if (untracked.split("\0").includes(path))
|
|
103
109
|
fullDiff += `\n--- /dev/null\n+++ b/${path}\n${content.toString()}`;
|
|
@@ -125,12 +131,19 @@ export class ExistingCheckout {
|
|
|
125
131
|
}
|
|
126
132
|
async commit(project, expected, publication, runId, signal) {
|
|
127
133
|
signal?.throwIfAborted();
|
|
134
|
+
const runMarkers = publication.commitMessage
|
|
135
|
+
.split(/\r?\n/)
|
|
136
|
+
.map((line) => line.trim())
|
|
137
|
+
.filter((line) => line.startsWith("Agent-Workflows-Run:"));
|
|
138
|
+
if (runMarkers.length !== 1 ||
|
|
139
|
+
runMarkers[0] !== `Agent-Workflows-Run: ${runId}`)
|
|
140
|
+
throw new BlockedError("Commit message must contain exactly one matching workflow run marker");
|
|
128
141
|
const current = await this.inspect(project);
|
|
129
142
|
if (current.head !== expected.head) {
|
|
130
143
|
const message = await this.git(project, "log", "-1", "--format=%B");
|
|
131
144
|
const parent = await this.git(project, "rev-parse", "HEAD^");
|
|
132
145
|
if (parent === expected.head &&
|
|
133
|
-
message.
|
|
146
|
+
message.trimEnd() === publication.commitMessage.trimEnd() &&
|
|
134
147
|
current.paths.length === 0) {
|
|
135
148
|
const changed = (await this.git(project, "diff", "HEAD^", "HEAD", "--no-renames", "--name-only", "-z"))
|
|
136
149
|
.split("\0")
|
|
@@ -139,7 +152,11 @@ export class ExistingCheckout {
|
|
|
139
152
|
if (JSON.stringify(changed) !== JSON.stringify(expected.paths))
|
|
140
153
|
throw new BlockedError("Reconciled commit has an unexpected change set");
|
|
141
154
|
for (const [path, digest] of Object.entries(expected.files)) {
|
|
142
|
-
const
|
|
155
|
+
const absolute = join(project.checkout, path);
|
|
156
|
+
const actual = await Promise.all([
|
|
157
|
+
readFile(absolute),
|
|
158
|
+
lstat(absolute),
|
|
159
|
+
]).then(([content, stat]) => fileStateDigest(content, stat.mode), (error) => {
|
|
143
160
|
if (error.code === "ENOENT")
|
|
144
161
|
return null;
|
|
145
162
|
throw error;
|
|
@@ -156,15 +173,11 @@ export class ExistingCheckout {
|
|
|
156
173
|
throw new BlockedError("No changes to commit");
|
|
157
174
|
await this.runGit(project, ["add", "--", ...expected.paths], { signal });
|
|
158
175
|
await this.runGit(project, [
|
|
159
|
-
"-c",
|
|
160
|
-
`user.name=${project.gitIdentity.name}`,
|
|
161
|
-
"-c",
|
|
162
|
-
`user.email=${project.gitIdentity.email}`,
|
|
163
176
|
"-c",
|
|
164
177
|
"core.hooksPath=/dev/null",
|
|
165
178
|
"commit",
|
|
166
179
|
"-m",
|
|
167
|
-
|
|
180
|
+
publication.commitMessage,
|
|
168
181
|
], { signal });
|
|
169
182
|
return this.git(project, "rev-parse", "HEAD");
|
|
170
183
|
}
|
package/docs/api.md
CHANGED
|
@@ -4,9 +4,15 @@ Exports are in `src/index.ts`; the built package resolves to `dist/src/index.js`
|
|
|
4
4
|
|
|
5
5
|
## Runner and controls
|
|
6
6
|
|
|
7
|
-
`new Runner({config,databaseUrl,hosting,agents,workspace?,workflow?,workflowVersion?})` injects hosting/agent adapters and optionally a workspace strategy or workflow. `hosting(project)` returns a host-qualified adapter. `agents` maps provider names to adapters. The default workspace uses the existing checkout. One DBOS runtime runs per Node process; one runner owns each configuration and checkout.
|
|
7
|
+
`new Runner({config,databaseUrl,hosting,agents,workspace?,workflow?,workflowVersion?,pathBaseDirectory?,promptBaseDirectory?})` injects hosting/agent adapters and optionally a workspace strategy or workflow. `hosting(project)` returns a host-qualified adapter. `agents` maps provider names to adapters. The default workspace uses the existing checkout. One DBOS runtime runs per Node process; one runner owns each configuration and checkout.
|
|
8
8
|
|
|
9
|
-
`
|
|
9
|
+
`pathBaseDirectory` must identify an existing directory. It resolves relative
|
|
10
|
+
state, checkout, and prompt-file paths once when the runner is constructed.
|
|
11
|
+
Omitting it preserves current-working-directory behavior for SDK callers.
|
|
12
|
+
`promptBaseDirectory` retains its narrower role and, when both are supplied,
|
|
13
|
+
overrides only relative prompt files.
|
|
14
|
+
|
|
15
|
+
`start()` validates registration, acquires ownership, launches DBOS, registers concurrency-one project queues, and starts polling. `poll(projectId?)` performs an immediate scan. `pause(projectId)` stops new starts while active work continues. `resume(projectId)` refuses blocked checkouts. `stop(runId)` waits for the active invocation/process to end, or cancels queued work. `retry(runId)` requires a terminal failed/blocked/cancelled run and a clean checkout, then returns a new linked run ID. `recover(runId)` returns a new execution ID for publication recovery of the same run. `shutdown()` stops intake, cancels and awaits active work, closes DBOS and releases ownership.
|
|
10
16
|
|
|
11
17
|
Use `try/finally` to call `shutdown()`, including failed startup. A custom `workflowVersion` must change when its durable step order changes; finish existing work before replacing an incompatible version.
|
|
12
18
|
|
|
@@ -29,24 +35,117 @@ commit together; failure preserves the blocked state.
|
|
|
29
35
|
| `prepare()` | Require clean Git state; fetch base and create unique branch |
|
|
30
36
|
| `implement()` | Fresh implementation session; reject unexpected commits or branch changes |
|
|
31
37
|
| `validate()` | Run commands and verify unchanged diff; false means no change |
|
|
32
|
-
| `writePublication()` |
|
|
38
|
+
| `writePublication()` | Validate text; finalize persisted run/co-author trailers from retained changes |
|
|
33
39
|
| `commit()` / `push()` | Separate reconciled Git effects using the verified change set |
|
|
34
40
|
| `publish()` | Find existing request by branch before creating a draft |
|
|
35
41
|
| `review()` | Fresh read-only reviewer; exact published diff, head and validation evidence |
|
|
36
42
|
| `publishReview()` | Reject stale head; reconcile review marker; map valid added-line findings |
|
|
37
43
|
| `complete(outcome?)` | Require clean checkout and persist terminal outcome |
|
|
38
44
|
| `step(name, operation)` | Custom durable operation receiving current `RunRecord` |
|
|
39
|
-
| `invoke(name, stage,
|
|
45
|
+
| `invoke(name, stage, task)` | Custom agentic step with profile resolution and session history |
|
|
46
|
+
|
|
47
|
+
`task` separates `defaultPrompt` from an optional `context(run)` supplier.
|
|
48
|
+
Optional `readOnly`, Zod `outputContract`, and captured `evidence` control runtime
|
|
49
|
+
checks. Stage overrides replace only `defaultPrompt`. Custom stages have no
|
|
50
|
+
inferred built-in default; resolve file-based custom stages before invocation
|
|
51
|
+
with `resolveStagePrompt(stage, defaultPrompt, baseDirectory)` if they must be
|
|
52
|
+
frozen alongside startup configuration. The runner resolves built-in prompt files
|
|
53
|
+
at construction using `promptBaseDirectory ?? pathBaseDirectory ?? process.cwd()`.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
await operations.invoke("report", stage, {
|
|
57
|
+
defaultPrompt: "Summarize the recorded validation.",
|
|
58
|
+
context: (run) => JSON.stringify(run.validation ?? []),
|
|
59
|
+
readOnly: true,
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
An output contract enables strict response validation and one format correction
|
|
64
|
+
within the original deadline. Both attempts retain separate invocation/session
|
|
65
|
+
records under the same durable step. The returned string is validated JSON when
|
|
66
|
+
a contract is supplied. Codex output contracts must use its supported JSON-schema
|
|
67
|
+
subset: every property is required; use nullable defaults for optional locations.
|
|
68
|
+
Built-in review parsing accepts omitted locations and normalizes them to `null`. Interrupted calls are never automatically replayed.
|
|
40
69
|
|
|
41
70
|
Put side effects inside `step`; custom effects must be idempotent or reconcile their own ambiguous results. A DBOS checkpoint does not snapshot a checkout. Returning from a custom workflow without calling a terminal operation is invalid. [Reporting workflow](../examples/custom-workflow.ts) inserts a validation report without editing provider code.
|
|
42
71
|
|
|
72
|
+
## DBOS SDK direct usage
|
|
73
|
+
|
|
74
|
+
A custom `workflow(operations)` runs inside the runner's registered DBOS workflow.
|
|
75
|
+
It can mix predefined operations with direct SDK calls; no additional workflow
|
|
76
|
+
registration or DBOS runtime is needed. In a consuming application, declare
|
|
77
|
+
`@dbos-inc/dbos-sdk` as a direct dependency compatible with this package's SDK
|
|
78
|
+
version, and ensure both resolve to the same runtime instance.
|
|
79
|
+
|
|
80
|
+
This example adds a checkpointed health check against a local service before the
|
|
81
|
+
standard workflow. The service must expose `/health` on port 8080.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
85
|
+
import {
|
|
86
|
+
defaultWorkflow,
|
|
87
|
+
type Operations,
|
|
88
|
+
} from "@mingchuno/agent-workflows";
|
|
89
|
+
|
|
90
|
+
export async function customWorkflow(operations: Operations): Promise<void> {
|
|
91
|
+
await DBOS.runStep(
|
|
92
|
+
async () => {
|
|
93
|
+
const signal = AbortSignal.any([
|
|
94
|
+
operations.dependencies.signal,
|
|
95
|
+
AbortSignal.timeout(5_000),
|
|
96
|
+
]);
|
|
97
|
+
signal.throwIfAborted();
|
|
98
|
+
const response = await fetch("http://127.0.0.1:8080/health", { signal });
|
|
99
|
+
await response.body?.cancel();
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw new Error(`Local service returned ${response.status}`);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{ name: "check-local-service", retriesAllowed: false },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
await defaultWorkflow(operations);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
In the [runner example](../examples/run.ts), import this function and replace its
|
|
112
|
+
`workflow` and `workflowVersion` options:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
workflow: customWorkflow,
|
|
116
|
+
workflowVersion: "local-health-v1",
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The successful check is checkpointed and skipped during recovery; it is not a
|
|
120
|
+
fresh health check on every restart. `defaultWorkflow` supplies the normal coding
|
|
121
|
+
operations and terminal outcome. See the [DBOS step API](https://docs.dbos.dev/typescript/reference/workflows-steps).
|
|
122
|
+
|
|
123
|
+
Caveats:
|
|
124
|
+
|
|
125
|
+
- `operations.step(name, callback)` adds cancellation and checkout checks, phase
|
|
126
|
+
updates, step events and error redaction around `DBOS.runStep`. Prefer it for
|
|
127
|
+
custom application operations. Raw steps retain DBOS history but bypass those
|
|
128
|
+
additions; pass the runner's abort signal to cancellable work, as above.
|
|
129
|
+
- Keep I/O, time reads and randomness inside durable steps. Call orchestration
|
|
130
|
+
APIs such as `DBOS.sleepms` at workflow level. Do not wrap predefined operations
|
|
131
|
+
or the whole workflow inside `runStep`; each operation owns its checkpoints.
|
|
132
|
+
- Disabling retries does not make external writes exactly-once. A crash after an
|
|
133
|
+
effect but before checkpointing can repeat it. Use stable idempotency keys or
|
|
134
|
+
reconcile ambiguous results. DBOS does not snapshot or restore checkout files.
|
|
135
|
+
- Finish custom paths with a terminal operation such as `operations.complete()`;
|
|
136
|
+
returning while a run remains queued/running is invalid.
|
|
137
|
+
- Change `workflowVersion` when durable step order changes, and finish or
|
|
138
|
+
explicitly resolve pending runs before deploying an incompatible workflow.
|
|
139
|
+
- Let `Runner` own `DBOS.setConfig`, `launch` and `shutdown`. Direct SDK use inside
|
|
140
|
+
a workflow does not grant another runtime or bypass checkout ownership rules.
|
|
141
|
+
|
|
43
142
|
## Extension contracts
|
|
44
143
|
|
|
45
|
-
`Workspace` separates `check`, `prepare`, `inspect`, `verify`, `commit`, `push` and `release`. `Snapshot` contains branch/head, changed paths, diff and a content fingerprint. Never implement release by discarding files. A future isolated workspace implementation can replace this interface without changing workflow composition.
|
|
144
|
+
`Workspace` separates `check`, `prepare`, `inspect`, `verify`, `commit`, `push` and `release`. `Snapshot` contains branch/head, changed paths, diff and a content fingerprint. `commit` receives the already-finalized message, including attribution and run marker, and must preserve native Git identity selection. Never implement release by discarding files. A future isolated workspace implementation can replace this interface without changing workflow composition.
|
|
46
145
|
|
|
47
146
|
`prepare`, `commit` and `push` receive an optional final `AbortSignal`. Custom workspaces must stop their subprocesses before settling a cancelled operation. The existing-checkout strategy journals Git processes under the Git directory; ownership acquisition rejects surviving process groups after a runner crash.
|
|
48
147
|
|
|
49
|
-
`AgentAdapter.validate(profile)` returns observable effective settings. `invoke(input)` receives working directory, prompt
|
|
148
|
+
`AgentAdapter.validate(profile)` returns observable effective settings. `invoke(input)` receives working directory, prompt, optional application-owned `outputSchema`, read-only intent, abort signal, stage `timeoutMs` and session/event callbacks. The abort signal also covers cancellation and time spent validating the profile. Call `session(id)` immediately when available. Await event persistence; invocation must not settle until its work has stopped. SDK adapters enforce process-group lifecycle; custom adapters must uphold the same contract. `processFile` is available for controlled subprocess ownership.
|
|
50
149
|
|
|
51
150
|
`HostingAdapter` provides issue pagination/revalidation, instance-qualified `identity`, change-request lookup/create, remote head, and idempotent review publication. `preflight` is optional. Reconciliation keys must be stable across response loss; providers must never infer successful publication from agent prose.
|
|
52
151
|
|
|
@@ -59,6 +158,31 @@ and process safety check, which runs under the project lock for new admissions
|
|
|
59
158
|
only. This callback must not mutate Store records. Operator tools should use
|
|
60
159
|
`retry` commands or `Runner.retry`, preserving those safety checks.
|
|
61
160
|
|
|
62
|
-
Invocation records include project/run IDs, stable DBOS step ID and name, invocation ID, attempt, timestamps, requested/effective profile, provider, prompt/
|
|
161
|
+
Invocation records include project/run IDs, stable DBOS step ID and name, invocation ID, attempt, timestamps, requested/effective profile, provider, effective task prompt/source/hash, output-contract and evidence identities, artifact path and session state (`pending`, `available`, `unavailable`). Repeated custom steps retain separate invocations. A retry has a separate run record linked to its predecessor.
|
|
162
|
+
|
|
163
|
+
Writable invocation before/after evidence is retained on the run. Publication
|
|
164
|
+
finalization persists contributing provider identities in first-contribution
|
|
165
|
+
order, independent of custom stage names. Recovery reuses that provenance and
|
|
166
|
+
the finalized publication message.
|
|
167
|
+
|
|
168
|
+
`request(kind,target)` queues the same `pause`, `resume`, `stop`, `retry`, or `recover` commands used by the CLI/TUI; `commands()` reports pending/success/failure. A runner must be active to execute them. `finishCommand` and record-writing methods support adapters and custom workflows; operator tools should prefer commands over direct mutation.
|
|
169
|
+
|
|
170
|
+
## Publication recovery
|
|
171
|
+
|
|
172
|
+
`Runner.recover(runId, commandId?)` supports failed publication steps in the
|
|
173
|
+
default workflow. It validates the source execution, checkout, configuration,
|
|
174
|
+
artifacts and remote state. `Store.admitRecovery` commits intent and its event
|
|
175
|
+
under the same project lock as retry admission; its safety callback must not
|
|
176
|
+
mutate Store records. Operator tools should use the runner or queued commands.
|
|
177
|
+
|
|
178
|
+
`RunRecord.executions` contains the initial execution and recovery executions,
|
|
179
|
+
including DBOS IDs, source execution, restart step, reused steps, configuration
|
|
180
|
+
fingerprint and outcomes. Run IDs remain stable for invocations and publication
|
|
181
|
+
markers. `Store.recoveryPlan(runId)` reports persisted eligibility and its reason;
|
|
182
|
+
live safety checks happen at admission and execution. Runs without execution
|
|
183
|
+
metadata remain readable and retryable, but cannot be recovered.
|
|
63
184
|
|
|
64
|
-
|
|
185
|
+
Recovery uses DBOS forks, retaining the original workflow input and checkpoint
|
|
186
|
+
prefix. The accepted command ID is the fork ID: dispatch adopts an existing fork
|
|
187
|
+
after an uncertain response or crash. Copied start gates do not replace live
|
|
188
|
+
checks in the first non-replayed operation. Successful prefixes cannot rerun.
|
package/docs/architecture.md
CHANGED
|
@@ -5,20 +5,37 @@ DBOS owns workflow execution, durable steps and concurrency-one project queues.
|
|
|
5
5
|
- `config.ts` / `domain.ts`: validated configuration, vocabulary and adapter contracts.
|
|
6
6
|
- `runner.ts`: local ownership, intake/deduplication, DBOS lifecycle and operator controls.
|
|
7
7
|
- `operations.ts`: reusable durable coding operations and the default workflow.
|
|
8
|
+
- `prompts.ts` / `invocation.ts`: resolved task text, output contracts and bounded format correction.
|
|
9
|
+
- `evidence.ts`: indexed, hashed change artifacts and capture limits.
|
|
10
|
+
- `recovery.ts`: publication recovery eligibility, input fingerprints and live safety checks.
|
|
8
11
|
- `workspace.ts`: existing-checkout Git operations and change verification.
|
|
9
12
|
- `store.ts`: typed Drizzle queries for run, invocation, project, command and event records; `db/schema.ts` and `drizzle/` own the application schema and migrations.
|
|
10
13
|
- `adapters/`: provider clients and isolated SDK workers.
|
|
11
14
|
- `runtime/`: process groups, ownership journals and redacted logging.
|
|
12
|
-
- `cli.ts` / `tui
|
|
15
|
+
- `cli.ts` / `tui/`: shared command/query interfaces.
|
|
13
16
|
|
|
14
|
-
Application records live in `agent_workflows`; DBOS maintains its own execution schema in the same PostgreSQL database. Large streamed agent/validation logs live under the configured state directory, referenced by records.
|
|
17
|
+
Application records live in `agent_workflows`; DBOS maintains its own execution schema in the same PostgreSQL database. Large streamed agent/validation logs live under the configured state directory, referenced by records. Interrupted or failed agent calls are never automatically retried. A returned response that fails its output contract may receive one fresh inspection-only format-correction attempt within the same stage deadline. Publication retries and explicit recovery reconcile external state first. Clean terminal state and terminal workflow outcome are deliberately separate.
|
|
15
18
|
|
|
16
19
|
Node/PostgreSQL/Git are the only runtime infrastructure; providers require their normal local authentication. Zod, Commander, Ink/React, Drizzle/node-postgres, Pino, Octokit and Gitbeaker handle standard infrastructure. Drizzle ORM and Codex SDK are Apache-2.0; the other listed runtime libraries and Copilot SDK are MIT-licensed. Exact dependency versions are pinned by the lockfile. No custom HTTP client, CLI parser or terminal renderer is introduced.
|
|
17
20
|
|
|
18
21
|
The test boundary is the public runner/workflow API using real PostgreSQL, real temporary Git repositories and controlled adapters. Separate adapter contracts exercise SDK argument/event mapping and HTTP behavior. Process-level recovery tests terminate a runner after external effects and restart it against the same state. Runtime/provider smoke calls are intentionally separate from deterministic acceptance tests.
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
The runner supports existing checkouts only. Higher per-project concurrency requires isolated workspaces and lifecycle design; changing the DBOS queue limit alone is unsafe.
|
|
21
24
|
|
|
22
25
|
## Package boundary
|
|
23
26
|
|
|
24
|
-
Keep one package while the SDK, CLI and TUI share a runtime, schema and release cycle. `src/adapters`, `src/runtime` and `src/
|
|
27
|
+
Keep one package while the SDK, CLI and TUI share a runtime, schema and release cycle. `src/adapters`, `src/runtime`, `src/db` and `src/tui` provide internal boundaries without workspace packages. Split into a monorepo when a separately deployed app or independently versioned package needs its own dependencies and build. `pnpm-workspace.yaml` currently configures installation policy only.
|
|
28
|
+
|
|
29
|
+
## Run and execution identity
|
|
30
|
+
|
|
31
|
+
A run owns the branch, commit and publication markers. Its initial DBOS execution
|
|
32
|
+
uses the run ID; publication recovery forks the failed execution at its failed
|
|
33
|
+
step under a new execution ID, preserving completed checkpoints and the original
|
|
34
|
+
run input. Execution history stays in the run record. Fresh retry creates a new
|
|
35
|
+
run and branch.
|
|
36
|
+
|
|
37
|
+
Recovery admission and retry share a project lock. Admission persists the fork ID
|
|
38
|
+
before dispatch so a restarted runner can adopt an existing fork. Recovery gates
|
|
39
|
+
run inside the first operation that actually executes, avoiding copied pause and
|
|
40
|
+
safety decisions. Recovery is limited to the default workflow's publication
|
|
41
|
+
steps; interrupted agents still require manual inspection.
|