@nodeint/migent 0.1.5 → 0.1.6
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 +191 -151
- package/dist/commands/agent.d.ts +8 -1
- package/dist/commands/agent.js +290 -17
- package/dist/commands/config.js +3 -1
- package/dist/commands/execute.d.ts +1 -2
- package/dist/commands/execute.js +93 -24
- package/dist/commands/sessions.js +10 -10
- package/dist/commands/skill.js +8 -1
- package/dist/display.d.ts +1 -0
- package/dist/display.js +3 -0
- package/dist/exits.d.ts +3 -1
- package/dist/exits.js +9 -2
- package/dist/program.js +62 -28
- package/dist/templates/migent/SKILL.md +155 -157
- package/dist/templates/migent/references/config.md +52 -0
- package/dist/templates/migent/references/inspect.md +32 -0
- package/dist/templates/migent/references/schema-workflow.yaml +11 -1
- package/dist/templates/migent/references/sessions.md +65 -0
- package/package.json +4 -4
package/dist/commands/agent.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
|
-
import { AgentToolRunner, CancellationRequested, ConfigurationError, createAgentSession, loadRoleInstructions, loadYamlMapping, log, prependRoleInstructions, requireAgentSession, isSessionLockLost, repairSessionUnlocked, SessionLock, resolveRoleTarget,
|
|
3
|
-
import { printAgent, printError, printNotice } from "../display.js";
|
|
3
|
+
import { AgentToolRunner, CancellationRequested, ConfigurationError, ProviderEventReporter, SessionStore, createAgentSession, loadRoleInstructions, loadYamlMapping, log, prependRoleInstructions, requireAgentSession, isSessionLockLost, repairSessionUnlocked, SessionLock, stringOptions, resolveRoleTarget, waitUntil, } from "@nodeint/migent-kernel";
|
|
4
|
+
import { printAgent, printError, printNotice, printResumeCommand } from "../display.js";
|
|
4
5
|
import { cliHooks } from "../hooks.js";
|
|
5
6
|
import { requireCurrentSchema } from "../migrate.js";
|
|
6
7
|
import { bindCancellationSignals } from "../signals.js";
|
|
@@ -65,6 +66,17 @@ export async function dispatchAgent(workspace, args) {
|
|
|
65
66
|
let resumedMany = null;
|
|
66
67
|
try {
|
|
67
68
|
const runner = args.runner ?? cliHooks.runner ?? new AgentToolRunner(adapterMap());
|
|
69
|
+
const waitedUntil = new Map();
|
|
70
|
+
const rerun = (prompt, resumeId) => runner.run({
|
|
71
|
+
provider: target.provider,
|
|
72
|
+
model: target.model,
|
|
73
|
+
options: { ...target.options },
|
|
74
|
+
prompt,
|
|
75
|
+
workspace,
|
|
76
|
+
sessionId: resolvedSessionId,
|
|
77
|
+
resumeExecutionId: resumeId,
|
|
78
|
+
stageId: args.role,
|
|
79
|
+
});
|
|
68
80
|
if (resumeCancelled) {
|
|
69
81
|
const reopened = new SessionStore(workspace).reopenCancelledExecutions(resolvedSessionId);
|
|
70
82
|
for (const item of reopened) {
|
|
@@ -78,32 +90,37 @@ export async function dispatchAgent(workspace, args) {
|
|
|
78
90
|
if (continued.length === 0) {
|
|
79
91
|
throw new ConfigurationError(`Session ${resolvedSessionId} is cancelled without an execution.`);
|
|
80
92
|
}
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
const finished = [];
|
|
94
|
+
for (const item of continued) {
|
|
95
|
+
finished.push(await rerunQuota(workspace, resolvedSessionId, item, (current) => lastUserPrompt(workspace, resolvedSessionId, current.execution_id), rerun, waitedUntil, args.waitHooks));
|
|
96
|
+
}
|
|
97
|
+
if (finished.length === 1)
|
|
98
|
+
result = finished[0];
|
|
83
99
|
else
|
|
84
|
-
resumedMany =
|
|
100
|
+
resumedMany = finished;
|
|
85
101
|
}
|
|
86
102
|
else {
|
|
87
|
-
|
|
103
|
+
const prompt = prependRoleInstructions(loadRoleInstructions(loadYamlMapping(path.join(workspace, ".migent", "config.yaml")), args.role), resolvePrompt({
|
|
104
|
+
prompt: args.prompt ?? null,
|
|
105
|
+
promptFile: args.promptFile ?? null,
|
|
106
|
+
promptStdin: args.promptStdin ?? false,
|
|
107
|
+
}));
|
|
108
|
+
result = await rerunQuota(workspace, resolvedSessionId, await runner.run({
|
|
88
109
|
provider: target.provider,
|
|
89
110
|
model: target.model,
|
|
90
111
|
options: { ...target.options },
|
|
91
|
-
prompt
|
|
92
|
-
prompt: args.prompt ?? null,
|
|
93
|
-
promptFile: args.promptFile ?? null,
|
|
94
|
-
promptStdin: args.promptStdin ?? false,
|
|
95
|
-
})),
|
|
112
|
+
prompt,
|
|
96
113
|
workspace,
|
|
97
114
|
sessionId: resolvedSessionId,
|
|
98
115
|
resumeExecutionId,
|
|
99
116
|
stageId: args.role,
|
|
100
|
-
});
|
|
117
|
+
}), () => prompt, rerun, waitedUntil, args.waitHooks);
|
|
101
118
|
}
|
|
102
119
|
}
|
|
103
120
|
catch (error) {
|
|
104
121
|
if (error instanceof CancellationRequested) {
|
|
105
122
|
log(error.message);
|
|
106
|
-
return 130;
|
|
123
|
+
return finishRole(resolvedSessionId, 130);
|
|
107
124
|
}
|
|
108
125
|
if (error instanceof ConfigurationError)
|
|
109
126
|
throw error;
|
|
@@ -123,9 +140,10 @@ export async function dispatchAgent(workspace, args) {
|
|
|
123
140
|
for (const item of resumedMany)
|
|
124
141
|
printAgent({ ...item }, args.role, target.provider, target.model);
|
|
125
142
|
}
|
|
126
|
-
if (resumedMany.some((item) => item.failure_reason === "verify_exhausted"))
|
|
127
|
-
return 6;
|
|
128
|
-
|
|
143
|
+
if (resumedMany.some((item) => item.failure_reason === "verify_exhausted")) {
|
|
144
|
+
return finishRole(resolvedSessionId, 6);
|
|
145
|
+
}
|
|
146
|
+
return finishRole(resolvedSessionId, resumedMany.every((item) => item.outcome_status === "complete") ? 0 : 1);
|
|
129
147
|
}
|
|
130
148
|
if (result === undefined)
|
|
131
149
|
throw new Error("Agent run produced no result.");
|
|
@@ -133,7 +151,262 @@ export async function dispatchAgent(workspace, args) {
|
|
|
133
151
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
134
152
|
else
|
|
135
153
|
printAgent({ ...result }, args.role, target.provider, target.model);
|
|
136
|
-
return result.outcome_status === "complete" ? 0 : 1;
|
|
154
|
+
return finishRole(resolvedSessionId, result.outcome_status === "complete" ? 0 : 1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export async function continueAgent(workspace, args) {
|
|
158
|
+
requireCurrentSchema(workspace);
|
|
159
|
+
requireAgentSession(workspace, args.sessionId);
|
|
160
|
+
let lock;
|
|
161
|
+
try {
|
|
162
|
+
lock = SessionLock.acquire(path.join(workspace, ".migent", "sessions", args.sessionId));
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (error instanceof Error) {
|
|
166
|
+
printError(error.message);
|
|
167
|
+
return 2;
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
return await continueLocked();
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (isSessionLockLost(error)) {
|
|
176
|
+
printError(error.message);
|
|
177
|
+
return 2;
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
lock.release();
|
|
183
|
+
}
|
|
184
|
+
async function continueLocked() {
|
|
185
|
+
repairSessionUnlocked(workspace, args.sessionId);
|
|
186
|
+
const store = new SessionStore(workspace);
|
|
187
|
+
const status = store.readSessionStatus(args.sessionId);
|
|
188
|
+
const sessionStatus = typeof status.status === "string" ? status.status : "";
|
|
189
|
+
const role = typeof status.role === "string" ? status.role : "";
|
|
190
|
+
if (role === "")
|
|
191
|
+
throw new ConfigurationError(`Session ${args.sessionId} has no role.`);
|
|
192
|
+
if (sessionStatus === "completed" && args.reply === null) {
|
|
193
|
+
printNotice(`Session ${args.sessionId} is completed.`);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
if (sessionStatus === "cancelled" && args.reply !== null) {
|
|
197
|
+
throw new ConfigurationError(`Session ${args.sessionId} was cancelled. session continue resumes that execution. Omit --reply.`);
|
|
198
|
+
}
|
|
199
|
+
const latest = store.latestExecution(args.sessionId);
|
|
200
|
+
if (latest === null)
|
|
201
|
+
throw new ConfigurationError(`Session ${args.sessionId} has no execution.`);
|
|
202
|
+
const profile = executionProfile(latest.directory, latest.executionId);
|
|
203
|
+
const optionLabel = Object.entries(profile.options)
|
|
204
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
205
|
+
.join(", ") || "default";
|
|
206
|
+
printNotice(`agent ${role} · ${profile.provider}/${profile.model} (${optionLabel}).`);
|
|
207
|
+
const unbind = bindCancellationSignals();
|
|
208
|
+
try {
|
|
209
|
+
const runner = args.runner ?? cliHooks.runner ?? new AgentToolRunner(adapterMap());
|
|
210
|
+
if (sessionStatus === "cancelled") {
|
|
211
|
+
return await runCancelledRole(workspace, args.sessionId, role, profile, runner, false, args.waitHooks);
|
|
212
|
+
}
|
|
213
|
+
let prompt;
|
|
214
|
+
if (args.reply !== null) {
|
|
215
|
+
prompt = rolePrompt(workspace, role, args.reply);
|
|
216
|
+
}
|
|
217
|
+
else if (sessionStatus === "blocked") {
|
|
218
|
+
const blocker = executionBlocker(latest.directory);
|
|
219
|
+
const resumed = store.resumeBlockedSession(args.sessionId);
|
|
220
|
+
for (const item of resumed) {
|
|
221
|
+
log(`session ${args.sessionId} was blocked by ${item.executionId}. Resuming ${role}.`);
|
|
222
|
+
}
|
|
223
|
+
const lines = ["Previous attempt:", `- execution: ${latest.executionId}`];
|
|
224
|
+
if (blocker)
|
|
225
|
+
lines.push(`- blocker: ${blocker}`);
|
|
226
|
+
lines.push("That attempt ended blocked. The blocker was handled outside this session. Continue.");
|
|
227
|
+
prompt = rolePrompt(workspace, role, lines.join("\n"));
|
|
228
|
+
}
|
|
229
|
+
else if (sessionStatus === "active") {
|
|
230
|
+
log(`session ${args.sessionId} is active. Resuming ${role} with its last prompt.`);
|
|
231
|
+
prompt = lastUserPrompt(workspace, args.sessionId, latest.executionId);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
throw new ConfigurationError(`Session ${args.sessionId} is ${sessionStatus || "unknown"}.`);
|
|
235
|
+
}
|
|
236
|
+
const waitedUntil = new Map();
|
|
237
|
+
const rerun = (nextPrompt, resumeId) => runner.run({
|
|
238
|
+
provider: profile.provider,
|
|
239
|
+
model: profile.model,
|
|
240
|
+
options: { ...profile.options },
|
|
241
|
+
prompt: nextPrompt,
|
|
242
|
+
workspace,
|
|
243
|
+
sessionId: args.sessionId,
|
|
244
|
+
resumeExecutionId: resumeId,
|
|
245
|
+
stageId: role,
|
|
246
|
+
});
|
|
247
|
+
const result = await rerunQuota(workspace, args.sessionId, await rerun(prompt, latest.executionId), () => prompt, rerun, waitedUntil, args.waitHooks);
|
|
248
|
+
printAgent({ ...result }, role, profile.provider, profile.model);
|
|
249
|
+
return finishRole(args.sessionId, result.outcome_status === "complete" ? 0 : 1);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (error instanceof CancellationRequested) {
|
|
253
|
+
log(error.message);
|
|
254
|
+
return finishRole(args.sessionId, 130);
|
|
255
|
+
}
|
|
256
|
+
if (error instanceof ConfigurationError)
|
|
257
|
+
throw error;
|
|
258
|
+
if (error instanceof Error) {
|
|
259
|
+
printError(error.message);
|
|
260
|
+
return 2;
|
|
261
|
+
}
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
unbind();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function runCancelledRole(workspace, sessionId, role, profile, runner, asJson, waitHooks) {
|
|
270
|
+
const reopened = new SessionStore(workspace).reopenCancelledExecutions(sessionId);
|
|
271
|
+
for (const item of reopened) {
|
|
272
|
+
log(`session ${sessionId} was cancelled during ${item.executionId}. Resuming that execution.`);
|
|
273
|
+
}
|
|
274
|
+
const waitedUntil = new Map();
|
|
275
|
+
const rerun = (prompt, resumeId) => runner.run({
|
|
276
|
+
provider: profile.provider,
|
|
277
|
+
model: profile.model,
|
|
278
|
+
options: { ...profile.options },
|
|
279
|
+
prompt,
|
|
280
|
+
workspace,
|
|
281
|
+
sessionId,
|
|
282
|
+
resumeExecutionId: resumeId,
|
|
283
|
+
stageId: role,
|
|
284
|
+
});
|
|
285
|
+
const settled = await Promise.allSettled(reopened.map((item) => runner.continueCancelled(workspace, sessionId, item.executionId)));
|
|
286
|
+
const rejected = settled.find((item) => item.status === "rejected");
|
|
287
|
+
if (rejected && rejected.status === "rejected")
|
|
288
|
+
throw rejected.reason;
|
|
289
|
+
const continued = settled.flatMap((item) => (item.status === "fulfilled" ? [item.value] : []));
|
|
290
|
+
if (continued.length === 0) {
|
|
291
|
+
throw new ConfigurationError(`Session ${sessionId} is cancelled without an execution.`);
|
|
292
|
+
}
|
|
293
|
+
const finished = [];
|
|
294
|
+
for (const item of continued) {
|
|
295
|
+
finished.push(await rerunQuota(workspace, sessionId, item, (current) => lastUserPrompt(workspace, sessionId, current.execution_id), rerun, waitedUntil, waitHooks));
|
|
296
|
+
}
|
|
297
|
+
if (finished.length === 1) {
|
|
298
|
+
const result = finished[0];
|
|
299
|
+
if (result === undefined)
|
|
300
|
+
throw new Error("Agent run produced no result.");
|
|
301
|
+
if (asJson)
|
|
302
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
303
|
+
else
|
|
304
|
+
printAgent({ ...result }, role, profile.provider, profile.model);
|
|
305
|
+
return finishRole(sessionId, result.outcome_status === "complete" ? 0 : 1);
|
|
306
|
+
}
|
|
307
|
+
if (asJson)
|
|
308
|
+
process.stdout.write(`${JSON.stringify(finished)}\n`);
|
|
309
|
+
else {
|
|
310
|
+
for (const item of finished)
|
|
311
|
+
printAgent({ ...item }, role, profile.provider, profile.model);
|
|
312
|
+
}
|
|
313
|
+
if (finished.some((item) => item.failure_reason === "verify_exhausted"))
|
|
314
|
+
return finishRole(sessionId, 6);
|
|
315
|
+
return finishRole(sessionId, finished.every((item) => item.outcome_status === "complete") ? 0 : 1);
|
|
316
|
+
}
|
|
317
|
+
async function rerunQuota(workspace, sessionId, initial, promptFor, run, waitedUntil, hooks) {
|
|
318
|
+
let current = initial;
|
|
319
|
+
let armed = null;
|
|
320
|
+
while (current.failure_kind === "quota" && current.reset_at !== "") {
|
|
321
|
+
if (armed !== null && !laterInstant(current.reset_at, armed))
|
|
322
|
+
return current;
|
|
323
|
+
const resetAt = current.reset_at;
|
|
324
|
+
const provider = current.provider;
|
|
325
|
+
const shared = waitedUntil.get(provider);
|
|
326
|
+
const now = hooks?.now ?? Date.now;
|
|
327
|
+
const past = !Number.isFinite(Date.parse(resetAt)) || now() >= Date.parse(resetAt);
|
|
328
|
+
if (!past && (shared === undefined || laterInstant(resetAt, shared))) {
|
|
329
|
+
emitRoleQuota(workspace, sessionId, current);
|
|
330
|
+
await waitUntil(resetAt, hooks).promise;
|
|
331
|
+
}
|
|
332
|
+
waitedUntil.set(provider, resetAt);
|
|
333
|
+
armed = resetAt;
|
|
334
|
+
current = await run(promptFor(current), current.execution_id);
|
|
335
|
+
}
|
|
336
|
+
return current;
|
|
337
|
+
}
|
|
338
|
+
function laterInstant(next, previous) {
|
|
339
|
+
const left = Date.parse(next);
|
|
340
|
+
const right = Date.parse(previous);
|
|
341
|
+
if (Number.isFinite(left) && Number.isFinite(right))
|
|
342
|
+
return left > right;
|
|
343
|
+
return next > previous;
|
|
344
|
+
}
|
|
345
|
+
function emitRoleQuota(workspace, sessionId, result) {
|
|
346
|
+
const store = new SessionStore(workspace);
|
|
347
|
+
const context = store.executionContext(sessionId, result.execution_id);
|
|
348
|
+
if (context === null)
|
|
349
|
+
return;
|
|
350
|
+
let model = "";
|
|
351
|
+
try {
|
|
352
|
+
const metadata = JSON.parse(readFileSync(path.join(context.directory, "execution.json"), "utf8"));
|
|
353
|
+
if (isRecord(metadata) && typeof metadata.model === "string")
|
|
354
|
+
model = metadata.model;
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
model = "";
|
|
358
|
+
}
|
|
359
|
+
const summary = [result.response, result.reset_at].filter((item) => item !== "").join(" ");
|
|
360
|
+
new ProviderEventReporter(store, context, result.provider, model).emit("provider.quota", summary, {
|
|
361
|
+
retry_at: result.reset_at,
|
|
362
|
+
provider: result.provider,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
function lastUserPrompt(workspace, sessionId, executionId) {
|
|
366
|
+
const context = new SessionStore(workspace).executionContext(sessionId, executionId);
|
|
367
|
+
if (context === null)
|
|
368
|
+
throw new ConfigurationError(`Execution ${executionId} was not found.`);
|
|
369
|
+
const raw = JSON.parse(readFileSync(context.conversationReadPath, "utf8"));
|
|
370
|
+
const history = Array.isArray(raw) ? raw : isRecord(raw) && Array.isArray(raw.history) ? raw.history : [];
|
|
371
|
+
for (let index = history.length - 1; index >= 0; index -= 1) {
|
|
372
|
+
const entry = history[index];
|
|
373
|
+
if (isRecord(entry) && entry.role === "user" && typeof entry.content === "string" && entry.content !== "") {
|
|
374
|
+
return entry.content;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
throw new ConfigurationError(`Execution ${executionId} has no prompt to resume.`);
|
|
378
|
+
}
|
|
379
|
+
function isRecord(value) {
|
|
380
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
381
|
+
}
|
|
382
|
+
function finishRole(sessionId, code) {
|
|
383
|
+
if (code === 1 || code === 6 || code === 130)
|
|
384
|
+
printResumeCommand(sessionId);
|
|
385
|
+
return code;
|
|
386
|
+
}
|
|
387
|
+
function rolePrompt(workspace, role, text) {
|
|
388
|
+
return prependRoleInstructions(loadRoleInstructions(loadYamlMapping(path.join(workspace, ".migent", "config.yaml")), role), text);
|
|
389
|
+
}
|
|
390
|
+
function executionProfile(directory, executionId) {
|
|
391
|
+
const raw = JSON.parse(readFileSync(path.join(directory, "execution.json"), "utf8"));
|
|
392
|
+
if (!isRecord(raw) ||
|
|
393
|
+
typeof raw.provider !== "string" ||
|
|
394
|
+
raw.provider === "" ||
|
|
395
|
+
typeof raw.model !== "string" ||
|
|
396
|
+
raw.model === "") {
|
|
397
|
+
throw new ConfigurationError(`Execution ${executionId} has no provider.`);
|
|
398
|
+
}
|
|
399
|
+
return { provider: raw.provider, model: raw.model, options: stringOptions(raw.options) };
|
|
400
|
+
}
|
|
401
|
+
function executionBlocker(directory) {
|
|
402
|
+
try {
|
|
403
|
+
const raw = JSON.parse(readFileSync(path.join(directory, "execution.json"), "utf8"));
|
|
404
|
+
if (!isRecord(raw) || !isRecord(raw.outcome))
|
|
405
|
+
return null;
|
|
406
|
+
return typeof raw.outcome.blocker === "string" && raw.outcome.blocker !== "" ? raw.outcome.blocker : null;
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return null;
|
|
137
410
|
}
|
|
138
411
|
}
|
|
139
412
|
function resolveSession(workspace, sessionId, role, task) {
|
package/dist/commands/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { confirm as clackConfirm, isCancel, select as clackSelect, text as clackText } from "@clack/prompts";
|
|
4
|
-
import { ConfigEditError, ConfigurationError, assignRoles, describeChange, diagnose, inspectConfig, loadModelTarget, loadWorkflowCatalog, loadYamlMapping, matchingModelName, patchConfigText, placeRoleModel, resolveStageSpec, setRoleOptions, upsertModel, whichSync, } from "@nodeint/migent-kernel";
|
|
4
|
+
import { ConfigEditError, ConfigurationError, assignRoles, describeChange, diagnose, inspectConfig, loadModelTarget, loadWorkflowCatalog, loadYamlMapping, matchingModelName, patchConfigText, placeRoleModel, isRoleStage, resolveStageSpec, setRoleOptions, upsertModel, whichSync, } from "@nodeint/migent-kernel";
|
|
5
5
|
import { printError } from "../display.js";
|
|
6
6
|
import { PromptCancelled } from "../errors.js";
|
|
7
7
|
import { cliHooks } from "../hooks.js";
|
|
@@ -316,6 +316,8 @@ function validateChange(before, after, workflows, adapters) {
|
|
|
316
316
|
}
|
|
317
317
|
for (const document of workflows) {
|
|
318
318
|
for (const stage of Object.values(document.stages)) {
|
|
319
|
+
if (!isRoleStage(stage))
|
|
320
|
+
continue;
|
|
319
321
|
let beforeTarget;
|
|
320
322
|
let afterTarget;
|
|
321
323
|
try {
|
|
@@ -5,11 +5,10 @@ export interface ExecuteWorkflowOptions {
|
|
|
5
5
|
task?: string | null;
|
|
6
6
|
sessionId?: string | null;
|
|
7
7
|
priorSessionId?: string | null;
|
|
8
|
-
maxAttempts?: number | null;
|
|
9
8
|
maxDispatches?: number | null;
|
|
9
|
+
continueFailed?: boolean;
|
|
10
10
|
stageId?: string | null;
|
|
11
11
|
runner?: AgentToolRunner | null;
|
|
12
|
-
verbose?: boolean;
|
|
13
12
|
itemProgress?: boolean;
|
|
14
13
|
startPrepared?: boolean;
|
|
15
14
|
refreshWorkflow?: boolean;
|
package/dist/commands/execute.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AgentToolRunner, DEFAULT_MAX_DISPATCHES, executeNamedWorkflow, formatDuration, log, } from "@nodeint/migent-kernel";
|
|
4
|
+
import { printResumeCommand } from "../display.js";
|
|
2
5
|
import { cliHooks } from "../hooks.js";
|
|
3
6
|
import { requireCurrentSchema } from "../migrate.js";
|
|
4
7
|
import { adapterMap } from "../provider.js";
|
|
@@ -9,7 +12,6 @@ export async function executeWorkflow(workspace, options) {
|
|
|
9
12
|
const startPrepared = options.startPrepared === true;
|
|
10
13
|
const sessionId = text(options.sessionId);
|
|
11
14
|
const creating = !startPrepared && (options.action === "start" || (once && sessionId === null));
|
|
12
|
-
const maxAttempts = options.maxAttempts === undefined ? 3 : options.maxAttempts;
|
|
13
15
|
const maxDispatches = options.maxDispatches === undefined ? DEFAULT_MAX_DISPATCHES : options.maxDispatches;
|
|
14
16
|
const runner = options.runner ?? cliHooks.runner ?? new AgentToolRunner(adapterMap());
|
|
15
17
|
runner.itemProgress = options.itemProgress === true;
|
|
@@ -19,37 +21,104 @@ export async function executeWorkflow(workspace, options) {
|
|
|
19
21
|
sessionId: creating ? null : sessionId,
|
|
20
22
|
priorSessionId: options.priorSessionId ?? null,
|
|
21
23
|
startPrepared,
|
|
22
|
-
maxAttempts: once ? null : maxAttempts,
|
|
23
24
|
maxDispatches: once ? null : maxDispatches,
|
|
25
|
+
continueFailed: options.continueFailed === true,
|
|
24
26
|
stageId: options.stageId ?? null,
|
|
25
27
|
once,
|
|
26
28
|
resetRevisions: options.action === "continue",
|
|
27
29
|
refreshWorkflow: options.refreshWorkflow === true,
|
|
28
30
|
reply: options.reply ?? null,
|
|
29
31
|
}, runner);
|
|
30
|
-
printResult(
|
|
31
|
-
|
|
32
|
-
result.stopReason === "revisions_exhausted" ||
|
|
33
|
-
result.stopReason === "dispatch_limit" ||
|
|
34
|
-
result.stopReason === "verify_exhausted") {
|
|
35
|
-
process.stderr.write(`Resume with: migent session continue ${result.sessionId}\n`);
|
|
36
|
-
}
|
|
32
|
+
printResult(workspace, result);
|
|
33
|
+
printResumeHint(result);
|
|
37
34
|
return result.code;
|
|
38
35
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
function printResumeHint(result) {
|
|
37
|
+
if (result.sessionId === "" || result.status === "completed" || result.stopReason === "completed")
|
|
38
|
+
return;
|
|
39
|
+
if (result.stopReason === "configuration")
|
|
40
|
+
return;
|
|
41
|
+
printResumeCommand(result.sessionId);
|
|
42
|
+
}
|
|
43
|
+
const INSPECT_EXIT_CODES = new Set([1, 3, 6, 8]);
|
|
44
|
+
function printResult(workspace, result) {
|
|
45
|
+
if (result.status === "completed" || result.stopReason === "completed") {
|
|
46
|
+
const duration = sessionDurationLabel(workspace, result.sessionId);
|
|
47
|
+
log(duration === null ? "completed" : `completed · ${duration}`);
|
|
48
|
+
for (const filePath of artifactPaths(workspace, result.sessionId, result.outputs)) {
|
|
49
|
+
log(`artifact: ${filePath}`);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
48
53
|
if (INSPECT_EXIT_CODES.has(result.code) && result.latestExecutionId) {
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
log(`response: executions/${result.latestExecutionId}/response.md`);
|
|
55
|
+
}
|
|
56
|
+
if (result.code === 7 && result.latestExecutionId) {
|
|
57
|
+
log(`verify: executions/${result.latestExecutionId}/turns/01/verify.md`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function artifactPaths(workspace, sessionId, outputs) {
|
|
61
|
+
if (sessionId === "")
|
|
62
|
+
return [];
|
|
63
|
+
const sessionDir = path.resolve(workspace, ".migent", "sessions", sessionId);
|
|
64
|
+
const paths = [];
|
|
65
|
+
for (const [name, value] of Object.entries(outputs)) {
|
|
66
|
+
const record = isRecord(value) ? value : {};
|
|
67
|
+
const relative = typeof record.path === "string" && record.path !== "" ? record.path : path.join("outputs", name);
|
|
68
|
+
paths.push(absoluteFromRoot(path.resolve(sessionDir, relative)));
|
|
69
|
+
}
|
|
70
|
+
return paths;
|
|
71
|
+
}
|
|
72
|
+
function absoluteFromRoot(target) {
|
|
73
|
+
try {
|
|
74
|
+
return realpathSync(target);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return path.resolve(target);
|
|
51
78
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
79
|
+
}
|
|
80
|
+
function sessionDurationLabel(workspace, sessionId) {
|
|
81
|
+
if (sessionId === "")
|
|
82
|
+
return null;
|
|
83
|
+
const executions = path.join(workspace, ".migent", "sessions", sessionId, "executions");
|
|
84
|
+
let names;
|
|
85
|
+
try {
|
|
86
|
+
names = readdirSync(executions);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
let total = 0;
|
|
92
|
+
let counted = false;
|
|
93
|
+
for (const name of names) {
|
|
94
|
+
const elapsed = executionElapsedMs(path.join(executions, name, "execution.json"));
|
|
95
|
+
if (elapsed === null)
|
|
96
|
+
continue;
|
|
97
|
+
total += elapsed;
|
|
98
|
+
counted = true;
|
|
99
|
+
}
|
|
100
|
+
return counted ? formatDuration(total) : null;
|
|
101
|
+
}
|
|
102
|
+
function executionElapsedMs(filePath) {
|
|
103
|
+
try {
|
|
104
|
+
const metadata = JSON.parse(readFileSync(filePath, "utf8"));
|
|
105
|
+
if (!isRecord(metadata))
|
|
106
|
+
return null;
|
|
107
|
+
const started = parsedTime(metadata.created_at);
|
|
108
|
+
if (started === null)
|
|
109
|
+
return null;
|
|
110
|
+
return (parsedTime(metadata.completed_at) ?? Date.now()) - started;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function parsedTime(value) {
|
|
117
|
+
if (typeof value !== "string" || value === "")
|
|
118
|
+
return null;
|
|
119
|
+
const parsed = Date.parse(value);
|
|
120
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
121
|
+
}
|
|
122
|
+
function isRecord(value) {
|
|
123
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
124
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { listSessionMetrics, listSessionRows, sessionMetrics, ConfigurationError } from "@nodeint/migent-kernel";
|
|
1
|
+
import { listSealedMetrics, listSessionRows, readSealedMetrics, ConfigurationError } from "@nodeint/migent-kernel";
|
|
4
2
|
import { printError, printSessions } from "../display.js";
|
|
5
|
-
import { adapterMap } from "../provider.js";
|
|
6
3
|
import { SESSION_COLUMN, sessionFields } from "../session-row.js";
|
|
7
4
|
import { optionalText } from "../text.js";
|
|
8
5
|
import { followSession } from "./watch.js";
|
|
@@ -36,9 +33,8 @@ export function runSessionMetrics(workspace, session, options) {
|
|
|
36
33
|
printError("List filters cannot be combined with a session.");
|
|
37
34
|
return 2;
|
|
38
35
|
}
|
|
39
|
-
const adapters = adapterMap();
|
|
40
36
|
if (sessionId === null) {
|
|
41
|
-
const rows =
|
|
37
|
+
const rows = listSealedMetrics(workspace, {
|
|
42
38
|
workflow: selection.workflow ?? null,
|
|
43
39
|
status: selection.status ?? null,
|
|
44
40
|
limit: selection.limit ?? null,
|
|
@@ -47,12 +43,16 @@ export function runSessionMetrics(workspace, session, options) {
|
|
|
47
43
|
process.stdout.write(`${JSON.stringify(row)}\n`);
|
|
48
44
|
return 0;
|
|
49
45
|
}
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
printError(`No
|
|
46
|
+
const sealed = readSealedMetrics(workspace, sessionId);
|
|
47
|
+
if (sealed.state === "missing") {
|
|
48
|
+
printError(`No metrics for session: ${sessionId}. Metrics are recorded when a workflow session completes.`);
|
|
53
49
|
return 2;
|
|
54
50
|
}
|
|
55
|
-
|
|
51
|
+
if (sealed.state === "corrupt") {
|
|
52
|
+
printError(`metrics.json for session ${sessionId} is not valid JSON.`);
|
|
53
|
+
return 2;
|
|
54
|
+
}
|
|
55
|
+
process.stdout.write(`${JSON.stringify(sealed.metrics)}\n`);
|
|
56
56
|
return 0;
|
|
57
57
|
}
|
|
58
58
|
export async function runSessions(workspace, session, options) {
|
package/dist/commands/skill.js
CHANGED
|
@@ -7,7 +7,14 @@ import { isDirectory, isFile } from "../fs-utils.js";
|
|
|
7
7
|
const SKILL_RELATIVE = path.join(".agents", "skills", "migent");
|
|
8
8
|
const STATE_FILENAME = ".migent-template.json";
|
|
9
9
|
const TEMPLATE_VERSION = 1;
|
|
10
|
-
const MANAGED = [
|
|
10
|
+
const MANAGED = [
|
|
11
|
+
"SKILL.md",
|
|
12
|
+
"references/schema-config.yaml",
|
|
13
|
+
"references/schema-workflow.yaml",
|
|
14
|
+
"references/sessions.md",
|
|
15
|
+
"references/inspect.md",
|
|
16
|
+
"references/config.md",
|
|
17
|
+
];
|
|
11
18
|
export function installSkill(workspace) {
|
|
12
19
|
const skillDirectory = path.join(workspace, SKILL_RELATIVE);
|
|
13
20
|
if (existsSync(skillDirectory)) {
|
package/dist/display.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Check } from "@nodeint/migent-kernel";
|
|
2
2
|
export declare function printError(message: string): void;
|
|
3
3
|
export declare function printNotice(message: string): void;
|
|
4
|
+
export declare function printResumeCommand(sessionId: string): void;
|
|
4
5
|
export declare function printCreated(paths: readonly string[], nextCommand: string): void;
|
|
5
6
|
export declare function printAlreadyInitialized(): void;
|
|
6
7
|
export declare function printInitConflicts(paths: readonly string[]): void;
|
package/dist/display.js
CHANGED
|
@@ -12,6 +12,9 @@ export function printNotice(message) {
|
|
|
12
12
|
const prefix = tty ? pc.cyan(pc.bold("[migent] ")) : "[migent] ";
|
|
13
13
|
process.stderr.write(`${prefix}${message}\n`);
|
|
14
14
|
}
|
|
15
|
+
export function printResumeCommand(sessionId) {
|
|
16
|
+
process.stderr.write(`Resume with:\nmigent session continue ${sessionId}\n`);
|
|
17
|
+
}
|
|
15
18
|
export function printCreated(paths, nextCommand) {
|
|
16
19
|
process.stdout.write("Created:\n");
|
|
17
20
|
for (const filePath of paths)
|
package/dist/exits.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export declare const EXIT_OK = 0;
|
|
2
2
|
export declare const EXIT_BLOCKED = 1;
|
|
3
3
|
export declare const EXIT_CONFIG = 2;
|
|
4
|
-
export declare const
|
|
4
|
+
export declare const EXIT_STAGE_FAILED = 3;
|
|
5
|
+
export declare const EXIT_QUOTA = 8;
|
|
5
6
|
export declare const EXIT_REVISIONS = 4;
|
|
6
7
|
export declare const EXIT_DISPATCHES = 5;
|
|
7
8
|
export declare const EXIT_VERIFY = 6;
|
|
9
|
+
export declare const EXIT_VERIFY_FAILED = 7;
|
|
8
10
|
export declare const EXIT_CANCELLED = 130;
|
|
9
11
|
export declare const LOOP_EXITS: string[];
|
|
10
12
|
export declare const STAGE_EXITS: string[];
|
package/dist/exits.js
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
export const EXIT_OK = 0;
|
|
2
2
|
export const EXIT_BLOCKED = 1;
|
|
3
3
|
export const EXIT_CONFIG = 2;
|
|
4
|
-
export const
|
|
4
|
+
export const EXIT_STAGE_FAILED = 3;
|
|
5
|
+
export const EXIT_QUOTA = 8;
|
|
5
6
|
export const EXIT_REVISIONS = 4;
|
|
6
7
|
export const EXIT_DISPATCHES = 5;
|
|
7
8
|
export const EXIT_VERIFY = 6;
|
|
9
|
+
export const EXIT_VERIFY_FAILED = 7;
|
|
8
10
|
export const EXIT_CANCELLED = 130;
|
|
9
11
|
export const LOOP_EXITS = [
|
|
10
12
|
`Exit ${EXIT_OK} when the session is completed.`,
|
|
11
13
|
`Exit ${EXIT_BLOCKED} when the session is blocked. session continue runs that stage again.`,
|
|
12
14
|
`Exit ${EXIT_CONFIG} on a configuration error.`,
|
|
13
|
-
`Exit ${
|
|
15
|
+
`Exit ${EXIT_STAGE_FAILED} when a stage fails and the command does not wait. The session stays active.`,
|
|
16
|
+
`Exit ${EXIT_QUOTA} when --once or --stage hits a provider quota and does not wait. The session stays active.`,
|
|
14
17
|
`Exit ${EXIT_REVISIONS} when a route would exceed max_revisions. The session stays active.`,
|
|
15
18
|
`Exit ${EXIT_DISPATCHES} when --max-dispatches is spent. The session stays active.`,
|
|
16
19
|
`Exit ${EXIT_VERIFY} when verify fails max_verify times. The session stays active.`,
|
|
20
|
+
`Exit ${EXIT_VERIFY_FAILED} when a stage with no role fails verify. The session stays active.`,
|
|
17
21
|
`Exit ${EXIT_CANCELLED} when cancelled. session continue resumes that execution.`,
|
|
18
22
|
];
|
|
19
23
|
export const STAGE_EXITS = [
|
|
20
24
|
`Exit ${EXIT_OK} when the stage outcome is complete, or the session is already completed.`,
|
|
21
25
|
`Exit ${EXIT_BLOCKED} when the stage is not complete, or the session is blocked.`,
|
|
22
26
|
`Exit ${EXIT_CONFIG} on a configuration error.`,
|
|
27
|
+
`Exit ${EXIT_STAGE_FAILED} when a non-continue --once or --stage finds a failed stage with no resume marker.`,
|
|
28
|
+
`Exit ${EXIT_QUOTA} when --once or --stage hits a provider quota and does not wait.`,
|
|
23
29
|
`Exit ${EXIT_REVISIONS} when a route would exceed max_revisions. The session stays active.`,
|
|
24
30
|
`Exit ${EXIT_VERIFY} when verify fails max_verify times. The session stays active.`,
|
|
31
|
+
`Exit ${EXIT_VERIFY_FAILED} when a stage with no role fails verify. The session stays active.`,
|
|
25
32
|
`Exit ${EXIT_CANCELLED} when cancelled. session continue resumes that execution.`,
|
|
26
33
|
];
|