@nodeint/migent 0.1.5 → 0.1.7

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.
@@ -1,10 +1,11 @@
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, SessionStore, } from "@nodeint/migent-kernel";
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";
7
- import { adapterMap, parseOptions } from "../provider.js";
8
+ import { providerMap, parseOptions } from "../provider.js";
8
9
  import { resolvePrompt } from "../workspace.js";
9
10
  export async function dispatchAgent(workspace, args) {
10
11
  requireCurrentSchema(workspace);
@@ -64,7 +65,18 @@ export async function dispatchAgent(workspace, args) {
64
65
  let result;
65
66
  let resumedMany = null;
66
67
  try {
67
- const runner = args.runner ?? cliHooks.runner ?? new AgentToolRunner(adapterMap());
68
+ const runner = args.runner ?? cliHooks.runner ?? new AgentToolRunner(providerMap());
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
- if (continued.length === 1)
82
- result = continued[0];
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 = continued;
100
+ resumedMany = finished;
85
101
  }
86
102
  else {
87
- result = await runner.run({
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: prependRoleInstructions(loadRoleInstructions(loadYamlMapping(path.join(workspace, ".migent", "config.yaml")), args.role), resolvePrompt({
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
- return resumedMany.every((item) => item.outcome_status === "complete") ? 0 : 1;
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(providerMap());
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) {
@@ -1,4 +1,4 @@
1
- import { type AgentAdapter } from "@nodeint/migent-kernel";
1
+ import { type Provider } from "@nodeint/migent-kernel";
2
2
  import { type RunCommand, type Which } from "../provider.js";
3
3
  export interface ConfigPrompter {
4
4
  select(message: string, choices: ReadonlyArray<readonly [string, string]>, defaultValue?: string | null): string | Promise<string>;
@@ -14,7 +14,7 @@ export declare function runConfigModel(workspace: string, options?: {
14
14
  clearOptions?: readonly string[];
15
15
  asJson?: boolean;
16
16
  interactive?: boolean;
17
- adapters?: Record<string, AgentAdapter> | null;
17
+ providers?: Record<string, Provider> | null;
18
18
  which?: Which | null;
19
19
  prompter?: ConfigPrompter | null;
20
20
  runCommand?: RunCommand | null;
@@ -28,7 +28,7 @@ export declare function runConfigRole(workspace: string, options?: {
28
28
  clearOptions?: readonly string[];
29
29
  asJson?: boolean;
30
30
  interactive?: boolean;
31
- adapters?: Record<string, AgentAdapter> | null;
31
+ providers?: Record<string, Provider> | null;
32
32
  which?: Which | null;
33
33
  prompter?: ConfigPrompter | null;
34
34
  runCommand?: RunCommand | null;