@zq-silk/yui 0.6.16 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commandCatalog.js +3 -7
- package/dist/cli.js +12 -33
- package/dist/commands/executionAuditCommands.js +19 -0
- package/dist/commands/globalRoleCommands.js +70 -0
- package/dist/commands/taskActor.js +3 -2
- package/dist/commands/taskCommands.js +160 -41
- package/dist/commands/taskContextCommand.js +1 -1
- package/dist/commands/taskInputCommands.js +3 -2
- package/dist/commands/taskRoleRuntimeStatus.js +3 -3
- package/dist/context/contextSnapshot.js +228 -0
- package/dist/context/roleSessionContext.js +3 -1
- package/dist/context/runContextContract.js +162 -0
- package/dist/context/runContextPack.js +322 -0
- package/dist/context/sessionBootstrapManifest.js +81 -0
- package/dist/context/sessionProtocolIdentity.js +23 -0
- package/dist/controller/agentRuntimeObserver.js +6 -1
- package/dist/controller/controller.js +4 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +446 -145
- package/dist/controller/jobControl.js +2 -1
- package/dist/controller/runtime.js +83 -0
- package/dist/controller/runtimeHookRunFence.js +6 -2
- package/dist/controller/sessionOwnerReconciliation.js +5 -0
- package/dist/executor/agentAdapter.js +7 -2
- package/dist/executor/agentExecutor.js +23 -0
- package/dist/executor/effectiveLaunch.js +24 -0
- package/dist/executor/executorRegistry.js +7 -1
- package/dist/executor/fileRoleLaunchPlanner.js +73 -27
- package/dist/lifecycle/exactRunTerminalization.js +2 -3
- package/dist/lifecycle/providerErrorClass.js +8 -3
- package/dist/observability/executionAudit.js +87 -2
- package/dist/repository/taskWorkspacePreparer.js +2 -2
- package/dist/run/agentRun.js +101 -16
- package/dist/run/providerRetry.js +167 -56
- package/dist/run/providerRetryConfig.js +5 -1
- package/dist/run/runControlRequest.js +50 -0
- package/dist/runtime/agentDriver.js +47 -0
- package/dist/runtime/agentHost.js +327 -0
- package/dist/runtime/builtinAgentDrivers.js +23 -1
- package/dist/runtime/builtinTranscriptObserver.js +4 -0
- package/dist/runtime/builtinTranscriptUsage.js +2 -0
- package/dist/runtime/exactControlPlane.js +2 -2
- package/dist/runtime/globalProcessExitStore.js +38 -0
- package/dist/runtime/launchBroker.js +95 -0
- package/dist/runtime/processExitObservation.js +60 -0
- package/dist/runtime/runtimeBinding.js +6 -0
- package/dist/runtime/runtimeObservation.js +27 -6
- package/dist/runtime/runtimeProjection.js +6 -3
- package/dist/runtime/runtimeStopReceipt.js +42 -0
- package/dist/runtime/sessionTerminationGuard.js +13 -0
- package/dist/runtime/tmuxAdapters.js +203 -220
- package/dist/scheduler/activeRoleRunDelivery.js +24 -3
- package/dist/scheduler/leaderWakeupProcessor.js +18 -60
- package/dist/scheduler/roleRunLiveness.js +61 -27
- package/dist/storage/migration/productionRegistry.js +264 -0
- package/dist/storage/sqliteSchema.js +23 -2
- package/dist/storage/sqliteStore.js +39 -2
- package/dist/storage/taskStore.js +54 -5
- package/dist/storage/upgrade/recordVersions.js +3 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +10 -0
- package/dist/task/taskRecordReference.js +1 -0
- package/dist/tmux/tmuxManager.js +15 -4
- package/dist/web/assets/client/components.js +1 -1
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +10 -5
- package/skills/yui-operator/SKILL.md +4 -0
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +61 -0
- package/skills/yui-worker/SKILL.md +82 -218
- package/dist/executor/managedClaudeRunner.js +0 -121
|
@@ -9,7 +9,7 @@ import { StorageConflictError } from "../storage/taskStore.js";
|
|
|
9
9
|
import { bindTaskWorkspaceIdentity } from "../task/task.js";
|
|
10
10
|
import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, workItemExecutionGroupById, recordWorkItemWorkspaceDisposition } from "../workItem/workItem.js";
|
|
11
11
|
import { createManagedWorkspace, managedWorkspaceKey, managedWorktreeName, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
|
|
12
|
-
import {
|
|
12
|
+
import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
|
|
13
13
|
import { NodeGitWorkspace, worktreeIdentity } from "./gitWorkspace.js";
|
|
14
14
|
import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
|
|
15
15
|
import { generateTaskWorkspaceIdentity, isLegacyTaskRef, taskArchiveRef, taskMainBranch, taskWorkspaceRefSegment, taskWorkspaceRefSegmentFromIdentity, TASK_WORKSPACE_TOKEN_PATTERN, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
|
|
@@ -2519,7 +2519,7 @@ function canCorrectActiveWorkItemRoleWorkspaceHint(store, taskId, role, item, wo
|
|
|
2519
2519
|
|| sessions.inFlight === null
|
|
2520
2520
|
|| sessions.inFlight.agentId !== role.activeAgentId
|
|
2521
2521
|
|| sessions.inFlight.runId !== run.id
|
|
2522
|
-
|| sessions.inFlight.receiptId !==
|
|
2522
|
+
|| sessions.inFlight.receiptId !== agentRunDeliveryReceiptId(run)
|
|
2523
2523
|
|| session === undefined
|
|
2524
2524
|
|| session.agentId !== role.activeAgentId
|
|
2525
2525
|
|| session.adapterId !== run.effective.adapterId
|
package/dist/run/agentRun.js
CHANGED
|
@@ -1,20 +1,42 @@
|
|
|
1
1
|
import { validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
|
|
2
|
-
import { validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
3
|
-
import {
|
|
2
|
+
import { formatAgentRunReceiptId, validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
3
|
+
import { prepareProviderRetryDispatch, validateAgentRunProviderRetry } from "./providerRetry.js";
|
|
4
4
|
import { validateYieldReceipt } from "./yieldReceipt.js";
|
|
5
|
+
import { validateAgentRunControlRequest } from "./runControlRequest.js";
|
|
5
6
|
import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
|
|
6
|
-
|
|
7
|
+
import { createRunAssignment, createRunBootstrapEnvelope, validateRunAssignment, validateRunBootstrapEnvelope } from "../context/runContextContract.js";
|
|
8
|
+
export function createAgentRun(id, taskId, roleName, mode, assignment, now, context) {
|
|
7
9
|
if (mode !== "new" && mode !== "resume") {
|
|
8
10
|
throw new Error(`Agent run dispatch mode is invalid: ${mode}.`);
|
|
9
11
|
}
|
|
10
12
|
const timestamp = now.toISOString();
|
|
13
|
+
const normalizedAssignment = typeof assignment === "string"
|
|
14
|
+
? createRunAssignment({
|
|
15
|
+
runId: id,
|
|
16
|
+
roleName,
|
|
17
|
+
purpose: context.purpose ?? "execution",
|
|
18
|
+
action: context.purpose === "review"
|
|
19
|
+
? "review-round"
|
|
20
|
+
: context.workItemId === undefined ? "leader-wake" : "execute-work-item",
|
|
21
|
+
subject: {
|
|
22
|
+
taskId,
|
|
23
|
+
...(context.workItemId === undefined ? {} : { workItemId: context.workItemId }),
|
|
24
|
+
...(context.purpose === "review"
|
|
25
|
+
? { reviewRoundId: context.reviewRoundId ?? `legacy-${id}` }
|
|
26
|
+
: context.reviewRoundId === undefined ? {} : { reviewRoundId: context.reviewRoundId })
|
|
27
|
+
},
|
|
28
|
+
directive: assignment,
|
|
29
|
+
deltaRefIds: []
|
|
30
|
+
})
|
|
31
|
+
: validateRunAssignment(assignment);
|
|
11
32
|
return {
|
|
12
|
-
schemaVersion:
|
|
33
|
+
schemaVersion: 9,
|
|
13
34
|
id: requireSafeIdentity(id, "Agent run id"),
|
|
14
35
|
taskId: requireSafeIdentity(taskId, "Task id"),
|
|
15
36
|
roleName: requireSafeIdentity(roleName, "Role name"),
|
|
16
37
|
mode,
|
|
17
|
-
|
|
38
|
+
assignment: normalizedAssignment,
|
|
39
|
+
bootstrapEnvelope: createRunBootstrapEnvelope(normalizedAssignment),
|
|
18
40
|
purpose: context.purpose ?? "execution",
|
|
19
41
|
...(context.workItemId === undefined
|
|
20
42
|
? {}
|
|
@@ -40,6 +62,22 @@ export function createAgentRun(id, taskId, roleName, mode, input, now, context)
|
|
|
40
62
|
export function isActiveAgentRun(run) {
|
|
41
63
|
return run.status === "active";
|
|
42
64
|
}
|
|
65
|
+
/** Binds a freshly created, not-yet-persisted Run to its frozen Context. */
|
|
66
|
+
export function withAgentRunContextSnapshot(run, snapshot, deltaRefIds = []) {
|
|
67
|
+
if (run.status !== "active" || run.pushedAt !== undefined || run.deliveredAt !== undefined) {
|
|
68
|
+
throw new Error(`Cannot bind Context Snapshot after Run delivery: ${run.id}.`);
|
|
69
|
+
}
|
|
70
|
+
const assignment = validateRunAssignment(Object.freeze({
|
|
71
|
+
...run.assignment,
|
|
72
|
+
contextSnapshotRef: snapshot,
|
|
73
|
+
deltaRefIds
|
|
74
|
+
}));
|
|
75
|
+
return validateAgentRun(Object.freeze({
|
|
76
|
+
...run,
|
|
77
|
+
assignment,
|
|
78
|
+
bootstrapEnvelope: createRunBootstrapEnvelope(assignment)
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
43
81
|
export function markAgentRunPushed(run, now) {
|
|
44
82
|
if (run.status !== "active") {
|
|
45
83
|
throw new Error(`Cannot push a terminal Agent run: ${run.id}.`);
|
|
@@ -66,14 +104,23 @@ export function markAgentRunDelivered(run, now) {
|
|
|
66
104
|
};
|
|
67
105
|
}
|
|
68
106
|
export function validateAgentRun(run) {
|
|
69
|
-
if (run.schemaVersion !==
|
|
70
|
-
throw new Error("Agent run must use schemaVersion
|
|
107
|
+
if (run.schemaVersion !== 9)
|
|
108
|
+
throw new Error("Agent run must use schemaVersion 9.");
|
|
71
109
|
validateTaskRecordReference({ taskId: run.taskId, localId: run.id }, "agentRun");
|
|
72
110
|
requireSafeIdentity(run.roleName, "Role name");
|
|
73
111
|
if (run.mode !== "new" && run.mode !== "resume") {
|
|
74
112
|
throw new Error(`Agent run dispatch mode is invalid: ${String(run.mode)}.`);
|
|
75
113
|
}
|
|
76
|
-
|
|
114
|
+
validateRunAssignment(run.assignment);
|
|
115
|
+
validateRunBootstrapEnvelope(run.bootstrapEnvelope);
|
|
116
|
+
if (run.assignment.runId !== run.id || run.assignment.roleName !== run.roleName
|
|
117
|
+
|| run.assignment.subject.taskId !== run.taskId) {
|
|
118
|
+
throw new Error("Agent run Assignment identity does not match the Run.");
|
|
119
|
+
}
|
|
120
|
+
if (JSON.stringify(createRunBootstrapEnvelope(run.assignment))
|
|
121
|
+
!== JSON.stringify(run.bootstrapEnvelope)) {
|
|
122
|
+
throw new Error("Agent run Bootstrap Envelope does not match its Assignment.");
|
|
123
|
+
}
|
|
77
124
|
if (!["execution", "review"].includes(run.purpose)) {
|
|
78
125
|
throw new Error(`Agent run purpose is invalid: ${String(run.purpose)}.`);
|
|
79
126
|
}
|
|
@@ -186,6 +233,12 @@ export function validateAgentRun(run) {
|
|
|
186
233
|
throw new Error("Agent run deliveredAt requires a prior pushedAt.");
|
|
187
234
|
}
|
|
188
235
|
}
|
|
236
|
+
if (run.deliveryReceiptId !== undefined) {
|
|
237
|
+
requireText(run.deliveryReceiptId, "Agent run deliveryReceiptId");
|
|
238
|
+
if (run.deliveryReceiptId.length > 256) {
|
|
239
|
+
throw new Error("Agent run deliveryReceiptId exceeds 256 characters.");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
189
242
|
if (run.status === "active") {
|
|
190
243
|
if (run.summary !== undefined || run.endedAt !== undefined) {
|
|
191
244
|
throw new Error("An active Agent run cannot have terminal metadata.");
|
|
@@ -219,6 +272,12 @@ export function validateAgentRun(run) {
|
|
|
219
272
|
throw new Error("A terminal Agent run cannot carry a providerRetry projection.");
|
|
220
273
|
}
|
|
221
274
|
}
|
|
275
|
+
if (run.controlRequest !== undefined) {
|
|
276
|
+
validateAgentRunControlRequest(run.controlRequest);
|
|
277
|
+
if (run.status !== "active") {
|
|
278
|
+
throw new Error("A terminal Agent run cannot carry a controlRequest projection.");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
222
281
|
if (run.yieldReceipt !== undefined) {
|
|
223
282
|
validateYieldReceipt(run.yieldReceipt);
|
|
224
283
|
if (run.status !== "yielded") {
|
|
@@ -250,6 +309,8 @@ function finishAgentRun(run, status, summary, now) {
|
|
|
250
309
|
if (terminal.providerRetry !== undefined) {
|
|
251
310
|
delete terminal.providerRetry;
|
|
252
311
|
}
|
|
312
|
+
if (terminal.controlRequest !== undefined)
|
|
313
|
+
delete terminal.controlRequest;
|
|
253
314
|
return terminal;
|
|
254
315
|
}
|
|
255
316
|
/**
|
|
@@ -264,23 +325,47 @@ export function withProviderRetry(run, retry) {
|
|
|
264
325
|
}
|
|
265
326
|
/**
|
|
266
327
|
* Reopens an active retry-waiting Run for another in-place attempt on its
|
|
267
|
-
* original Native Session
|
|
268
|
-
*
|
|
269
|
-
*
|
|
328
|
+
* original Native Session. The original Run transport/acceptance timestamps
|
|
329
|
+
* remain historical facts; the new delivery receipt and `dispatching` retry
|
|
330
|
+
* state make the delivery path send only the short continuation envelope.
|
|
270
331
|
*/
|
|
271
|
-
export function reopenRunForProviderRetry(run, now) {
|
|
332
|
+
export function reopenRunForProviderRetry(run, receiptId, now) {
|
|
272
333
|
if (run.status !== "active" || run.providerRetry === undefined) {
|
|
273
334
|
throw new Error(`Agent run is not waiting for a provider retry: ${run.id}.`);
|
|
274
335
|
}
|
|
275
336
|
const timestamp = now.toISOString();
|
|
276
|
-
|
|
277
|
-
// store rejects) so the delivery path sees an un-pushed Run.
|
|
278
|
-
const { providerRetry, pushedAt: _pushedAt, deliveredAt: _deliveredAt, ...rest } = run;
|
|
337
|
+
const { providerRetry, ...rest } = run;
|
|
279
338
|
return validateAgentRun({
|
|
280
339
|
...rest,
|
|
281
340
|
mode: "resume",
|
|
341
|
+
deliveryReceiptId: receiptId,
|
|
282
342
|
updatedAt: timestamp,
|
|
283
|
-
providerRetry:
|
|
343
|
+
providerRetry: prepareProviderRetryDispatch(providerRetry, receiptId, now)
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
/** Any exact correlated provider progress ends the active consecutive-failure episode. */
|
|
347
|
+
export function clearProviderRetryOnProgress(run) {
|
|
348
|
+
if (run.providerRetry === undefined)
|
|
349
|
+
return run;
|
|
350
|
+
const { providerRetry: _providerRetry, ...rest } = run;
|
|
351
|
+
return validateAgentRun(rest);
|
|
352
|
+
}
|
|
353
|
+
export function agentRunDeliveryReceiptId(run) {
|
|
354
|
+
return run.deliveryReceiptId
|
|
355
|
+
?? run.controlRequest?.receiptId
|
|
356
|
+
?? run.providerRetry?.lastRetryReceiptId
|
|
357
|
+
?? formatAgentRunReceiptId(run.taskId, run.id);
|
|
358
|
+
}
|
|
359
|
+
export function withAgentRunControlRequest(run, request, now) {
|
|
360
|
+
if (run.status !== "active" || run.providerRetry !== undefined) {
|
|
361
|
+
throw new Error(`Agent run cannot accept a control request: ${run.id}.`);
|
|
362
|
+
}
|
|
363
|
+
return validateAgentRun({
|
|
364
|
+
...run,
|
|
365
|
+
mode: "resume",
|
|
366
|
+
deliveryReceiptId: request.receiptId,
|
|
367
|
+
controlRequest: validateAgentRunControlRequest(request),
|
|
368
|
+
updatedAt: now.toISOString()
|
|
284
369
|
});
|
|
285
370
|
}
|
|
286
371
|
/** Attaches the Issue 04 yield receipt to a yielded Run. */
|
|
@@ -1,24 +1,17 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
|
|
2
|
-
export const
|
|
3
|
-
export const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* Bounded exponential backoff. The delay is capped in interval so a hot loop
|
|
13
|
-
* can never occupy the control plane; the total retry lineage is bounded by
|
|
14
|
-
* {@link PROVIDER_RETRY_MAX_WINDOW_MS} (see {@link providerRetryBudgetExhausted}).
|
|
15
|
-
*/
|
|
16
|
-
export function nextProviderRetryDelayMs(attempt) {
|
|
17
|
-
if (!Number.isSafeInteger(attempt) || attempt < 1) {
|
|
18
|
-
throw new Error(`Provider retry attempt must be a positive integer: ${String(attempt)}.`);
|
|
3
|
+
export const PROVIDER_RETRY_DELAYS_MS = Object.freeze([2_000, 5_000, 15_000]);
|
|
4
|
+
export const PROVIDER_RETRY_MAX_DISPATCHES = PROVIDER_RETRY_DELAYS_MS.length;
|
|
5
|
+
export const PROVIDER_RETRY_EPISODE_WINDOW_MS = 600_000;
|
|
6
|
+
/** Durable configuration keeps the historical name for compatibility. */
|
|
7
|
+
export const PROVIDER_RETRY_MAX_WINDOW_MS = PROVIDER_RETRY_EPISODE_WINDOW_MS;
|
|
8
|
+
export function nextProviderRetryDelayMs(retryIndex) {
|
|
9
|
+
if (!Number.isSafeInteger(retryIndex)
|
|
10
|
+
|| retryIndex < 1
|
|
11
|
+
|| retryIndex > PROVIDER_RETRY_MAX_DISPATCHES) {
|
|
12
|
+
throw new Error(`Provider retry index is out of range: ${String(retryIndex)}.`);
|
|
19
13
|
}
|
|
20
|
-
|
|
21
|
-
return Math.min(PROVIDER_RETRY_MAX_DELAY_MS, PROVIDER_RETRY_BASE_DELAY_MS * (2 ** exponent));
|
|
14
|
+
return PROVIDER_RETRY_DELAYS_MS[retryIndex - 1];
|
|
22
15
|
}
|
|
23
16
|
/**
|
|
24
17
|
* True when the retry lineage has used its total wall-clock budget. The
|
|
@@ -29,74 +22,192 @@ export function providerRetryBudgetExhausted(value, now, maxWindowMs = PROVIDER_
|
|
|
29
22
|
if (!Number.isSafeInteger(maxWindowMs) || maxWindowMs <= 0) {
|
|
30
23
|
throw new Error(`Provider retry max window must be a positive integer: ${String(maxWindowMs)}.`);
|
|
31
24
|
}
|
|
32
|
-
return Date.parse(value.firstFailureAt) + maxWindowMs <= now.getTime();
|
|
25
|
+
return Math.min(Date.parse(value.episodeDeadlineAt), Date.parse(value.firstFailureAt) + maxWindowMs) <= now.getTime();
|
|
33
26
|
}
|
|
34
27
|
export function validateAgentRunProviderRetry(value) {
|
|
35
|
-
if (value.schemaVersion !==
|
|
36
|
-
throw new Error("Agent run providerRetry must use schemaVersion
|
|
28
|
+
if (value.schemaVersion !== 2) {
|
|
29
|
+
throw new Error("Agent run providerRetry must use schemaVersion 2.");
|
|
30
|
+
}
|
|
31
|
+
if (!["scheduled", "dispatching", "awaiting-progress", "blocked"].includes(value.state)) {
|
|
32
|
+
throw new Error("Agent run providerRetry state is invalid.");
|
|
37
33
|
}
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
requireIdentity(value.episodeId, "Agent run providerRetry episodeId");
|
|
35
|
+
requireIdentity(value.failureEventId, "Agent run providerRetry failureEventId");
|
|
36
|
+
if (value.policyVersion !== 1)
|
|
37
|
+
throw new Error("Provider retry policy version is invalid.");
|
|
38
|
+
if (!Number.isSafeInteger(value.consecutiveFailures) || value.consecutiveFailures < 1) {
|
|
39
|
+
throw new Error("Agent run providerRetry consecutiveFailures must be positive.");
|
|
40
|
+
}
|
|
41
|
+
if (!Number.isSafeInteger(value.dispatchedRetries)
|
|
42
|
+
|| value.dispatchedRetries < 0
|
|
43
|
+
|| value.dispatchedRetries > value.maxRetries) {
|
|
44
|
+
throw new Error("Agent run providerRetry dispatchedRetries is invalid.");
|
|
45
|
+
}
|
|
46
|
+
if (value.maxRetries !== PROVIDER_RETRY_MAX_DISPATCHES) {
|
|
47
|
+
throw new Error("Agent run providerRetry maxRetries is invalid.");
|
|
40
48
|
}
|
|
41
49
|
requireTimestamp(value.firstFailureAt, "Agent run providerRetry firstFailureAt");
|
|
42
50
|
requireTimestamp(value.lastFailureAt, "Agent run providerRetry lastFailureAt");
|
|
51
|
+
requireTimestamp(value.episodeDeadlineAt, "Agent run providerRetry episodeDeadlineAt");
|
|
43
52
|
if (Date.parse(value.lastFailureAt) < Date.parse(value.firstFailureAt)) {
|
|
44
|
-
throw new Error("Agent run providerRetry lastFailureAt
|
|
53
|
+
throw new Error("Agent run providerRetry lastFailureAt precedes firstFailureAt.");
|
|
54
|
+
}
|
|
55
|
+
if (Date.parse(value.episodeDeadlineAt) <= Date.parse(value.firstFailureAt)) {
|
|
56
|
+
throw new Error("Agent run providerRetry deadline must follow firstFailureAt.");
|
|
57
|
+
}
|
|
58
|
+
if ((value.state === "scheduled") !== (value.nextAttemptAt !== undefined)) {
|
|
59
|
+
throw new Error("Only a scheduled providerRetry may carry nextAttemptAt.");
|
|
45
60
|
}
|
|
46
61
|
if (value.nextAttemptAt !== undefined) {
|
|
47
62
|
requireTimestamp(value.nextAttemptAt, "Agent run providerRetry nextAttemptAt");
|
|
63
|
+
if (Date.parse(value.nextAttemptAt) > Date.parse(value.episodeDeadlineAt)) {
|
|
64
|
+
throw new Error("Agent run providerRetry next attempt exceeds its episode deadline.");
|
|
65
|
+
}
|
|
48
66
|
}
|
|
49
|
-
if (value.
|
|
50
|
-
|
|
67
|
+
if ((value.state === "dispatching" || value.state === "awaiting-progress")
|
|
68
|
+
!== (value.lastRetryReceiptId !== undefined)) {
|
|
69
|
+
throw new Error("An in-flight providerRetry requires one retry receipt identity.");
|
|
70
|
+
}
|
|
71
|
+
if (value.lastRetryReceiptId !== undefined) {
|
|
72
|
+
requireIdentity(value.lastRetryReceiptId, "Agent run providerRetry receipt id");
|
|
51
73
|
}
|
|
74
|
+
if (value.launchId !== undefined)
|
|
75
|
+
requireIdentity(value.launchId, "Agent run providerRetry launchId");
|
|
52
76
|
if (value.nativeSessionId !== undefined) {
|
|
53
77
|
requireIdentity(value.nativeSessionId, "Agent run providerRetry nativeSessionId");
|
|
54
78
|
}
|
|
79
|
+
if (value.failedNativeTurnId !== undefined) {
|
|
80
|
+
requireIdentity(value.failedNativeTurnId, "Agent run providerRetry failed native Turn id");
|
|
81
|
+
}
|
|
55
82
|
requireText(value.lastErrorSummary, "Agent run providerRetry lastErrorSummary");
|
|
56
83
|
return value;
|
|
57
84
|
}
|
|
58
|
-
/**
|
|
59
|
-
* Records one more transient failure on a Run lineage. A fresh lineage starts
|
|
60
|
-
* at attempt 1; an existing projection increments its attempt and keeps the
|
|
61
|
-
* original `firstFailureAt`.
|
|
62
|
-
*/
|
|
85
|
+
/** Advance one failure episode without ever changing the native Session. */
|
|
63
86
|
export function scheduleProviderRetry(previous, input, now) {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
const at = now.toISOString();
|
|
88
|
+
const firstFailureAt = previous?.firstFailureAt ?? at;
|
|
89
|
+
const episodeDeadlineAt = previous?.episodeDeadlineAt
|
|
90
|
+
?? new Date(now.getTime() + PROVIDER_RETRY_EPISODE_WINDOW_MS).toISOString();
|
|
91
|
+
if (now.getTime() >= Date.parse(episodeDeadlineAt)) {
|
|
92
|
+
return Object.freeze({ outcome: "exhausted", reason: "window" });
|
|
93
|
+
}
|
|
94
|
+
const consecutiveFailures = (previous?.consecutiveFailures ?? 0) + 1;
|
|
95
|
+
const dispatchedRetries = previous?.dispatchedRetries ?? 0;
|
|
96
|
+
const schedule = input.scheduleNextAttempt ?? true;
|
|
97
|
+
if (schedule && dispatchedRetries >= PROVIDER_RETRY_MAX_DISPATCHES) {
|
|
98
|
+
return Object.freeze({ outcome: "exhausted", reason: "attempts" });
|
|
99
|
+
}
|
|
100
|
+
const state = schedule ? "scheduled" : "blocked";
|
|
101
|
+
const retryAfterMs = input.retryAfterMs;
|
|
102
|
+
if (retryAfterMs !== undefined && (!Number.isSafeInteger(retryAfterMs) || retryAfterMs <= 0)) {
|
|
103
|
+
throw new Error("Provider retry Retry-After must be a positive safe integer.");
|
|
104
|
+
}
|
|
105
|
+
const delayMs = schedule
|
|
106
|
+
? Math.max(nextProviderRetryDelayMs(dispatchedRetries + 1), retryAfterMs ?? 0)
|
|
107
|
+
: undefined;
|
|
108
|
+
const nextAttemptAt = delayMs === undefined
|
|
109
|
+
? undefined
|
|
110
|
+
: new Date(now.getTime() + delayMs).toISOString();
|
|
111
|
+
if (nextAttemptAt !== undefined && Date.parse(nextAttemptAt) > Date.parse(episodeDeadlineAt)) {
|
|
112
|
+
return Object.freeze({
|
|
113
|
+
outcome: "exhausted",
|
|
114
|
+
reason: retryAfterMs === undefined ? "window" : "retry-after-window"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const retry = validateAgentRunProviderRetry({
|
|
118
|
+
schemaVersion: 2,
|
|
119
|
+
episodeId: previous?.episodeId ?? createHash("sha256")
|
|
120
|
+
.update(`${firstFailureAt}\0${input.nativeSessionId ?? "unknown"}\0${input.launchId ?? "unknown"}`)
|
|
121
|
+
.digest("hex"),
|
|
122
|
+
failureEventId: requireIdentity(input.failureEventId, "Provider failure event id"),
|
|
123
|
+
policyVersion: 1,
|
|
124
|
+
state,
|
|
69
125
|
errorClass: input.errorClass,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
: {}),
|
|
126
|
+
consecutiveFailures,
|
|
127
|
+
dispatchedRetries,
|
|
128
|
+
maxRetries: PROVIDER_RETRY_MAX_DISPATCHES,
|
|
129
|
+
firstFailureAt,
|
|
130
|
+
lastFailureAt: at,
|
|
131
|
+
episodeDeadlineAt,
|
|
132
|
+
...(nextAttemptAt === undefined ? {} : { nextAttemptAt }),
|
|
78
133
|
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
79
134
|
...(input.nativeSessionId === undefined ? {} : { nativeSessionId: input.nativeSessionId }),
|
|
135
|
+
...(input.failedNativeTurnId === undefined
|
|
136
|
+
? {}
|
|
137
|
+
: { failedNativeTurnId: input.failedNativeTurnId }),
|
|
80
138
|
lastErrorSummary: input.lastErrorSummary
|
|
81
139
|
});
|
|
140
|
+
return Object.freeze({ outcome: schedule ? "scheduled" : "blocked", retry });
|
|
82
141
|
}
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
*/
|
|
87
|
-
export function markProviderRetryInFlight(value, now) {
|
|
88
|
-
if (value.nextAttemptAt === undefined)
|
|
142
|
+
/** Mark that one short continuation request was dispatched and now awaits any correlated progress. */
|
|
143
|
+
export function prepareProviderRetryDispatch(value, receiptId, now) {
|
|
144
|
+
if (value.state !== "scheduled" || value.nextAttemptAt === undefined)
|
|
89
145
|
return value;
|
|
90
|
-
|
|
91
|
-
|
|
146
|
+
if (now.getTime() > Date.parse(value.episodeDeadlineAt)) {
|
|
147
|
+
throw new Error("Provider retry episode expired before dispatch.");
|
|
148
|
+
}
|
|
92
149
|
const { nextAttemptAt: _nextAttemptAt, ...rest } = value;
|
|
93
150
|
return validateAgentRunProviderRetry({
|
|
94
151
|
...rest,
|
|
95
|
-
|
|
152
|
+
state: "dispatching",
|
|
153
|
+
lastRetryReceiptId: requireIdentity(receiptId, "Provider retry receipt id")
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
export function markProviderRetryDispatched(value) {
|
|
157
|
+
if (value.state !== "dispatching")
|
|
158
|
+
return value;
|
|
159
|
+
return validateAgentRunProviderRetry({
|
|
160
|
+
...value,
|
|
161
|
+
state: "awaiting-progress",
|
|
162
|
+
dispatchedRetries: value.dispatchedRetries + 1
|
|
96
163
|
});
|
|
97
164
|
}
|
|
98
|
-
/** True when the projection is due for another attempt at `now`. */
|
|
99
165
|
export function providerRetryIsDue(value, now) {
|
|
100
|
-
return value.
|
|
166
|
+
return value.state === "scheduled"
|
|
167
|
+
&& value.nextAttemptAt !== undefined
|
|
101
168
|
&& Date.parse(value.nextAttemptAt) <= now.getTime();
|
|
102
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Next Controller wake for an active automatic retry episode. Scheduled
|
|
172
|
+
* retries wake to dispatch; an in-flight retry wakes only at the episode
|
|
173
|
+
* deadline so silence cannot strand the Run forever. Blocked states are
|
|
174
|
+
* intentionally excluded because they require native/user evidence rather
|
|
175
|
+
* than an automatic lifecycle transition.
|
|
176
|
+
*/
|
|
177
|
+
export function providerRetryWakeAt(value) {
|
|
178
|
+
if (value.state === "scheduled")
|
|
179
|
+
return value.nextAttemptAt ?? null;
|
|
180
|
+
if (value.state === "dispatching" || value.state === "awaiting-progress") {
|
|
181
|
+
return value.episodeDeadlineAt;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
/** Delay a control-plane recovery gap without changing either failure counter. */
|
|
186
|
+
export function deferProviderRetry(value, now) {
|
|
187
|
+
const deadline = Date.parse(value.episodeDeadlineAt);
|
|
188
|
+
if (now.getTime() >= deadline)
|
|
189
|
+
return null;
|
|
190
|
+
const { lastRetryReceiptId: _receipt, nextAttemptAt: _next, ...rest } = value;
|
|
191
|
+
return validateAgentRunProviderRetry({
|
|
192
|
+
...rest,
|
|
193
|
+
state: "scheduled",
|
|
194
|
+
nextAttemptAt: new Date(Math.min(now.getTime() + 15_000, deadline)).toISOString()
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/** Short recovery instruction; it cannot repeat the Task Assignment body. */
|
|
198
|
+
export function serializeProviderRetryEnvelope(input) {
|
|
199
|
+
validateAgentRunProviderRetry(input.retry);
|
|
200
|
+
const retryOrdinal = input.retry.state === "dispatching"
|
|
201
|
+
? input.retry.dispatchedRetries + 1
|
|
202
|
+
: input.retry.dispatchedRetries;
|
|
203
|
+
return [
|
|
204
|
+
"Yui managed in-Session continuation retry.",
|
|
205
|
+
`task=${requireIdentity(input.taskId, "Provider retry task id")} run=${requireIdentity(input.runId, "Provider retry run id")} role=${requireIdentity(input.roleName, "Provider retry role")}`,
|
|
206
|
+
`episode=${input.retry.episodeId} retry=${retryOrdinal}/${PROVIDER_RETRY_MAX_DISPATCHES} receipt=${input.retry.lastRetryReceiptId ?? "pending"}`,
|
|
207
|
+
`failureEvent=${input.retry.failureEventId}`,
|
|
208
|
+
...(input.retry.failedNativeTurnId === undefined
|
|
209
|
+
? []
|
|
210
|
+
: [`retryOfTurn=${input.retry.failedNativeTurnId}`]),
|
|
211
|
+
"Continue the existing native conversation from its latest accepted state. Do not repeat completed work; load exact Run deltas if needed."
|
|
212
|
+
].join("\n");
|
|
213
|
+
}
|
|
@@ -17,5 +17,9 @@ export function providerRetryConfig(config) {
|
|
|
17
17
|
/** Whether the adapter has in-place retry enabled in the given mode. */
|
|
18
18
|
export function providerRetryEnabledForAdapter(config, adapterId, mode) {
|
|
19
19
|
return config.mode === mode
|
|
20
|
-
&& config
|
|
20
|
+
&& providerRetryAdapterEnabled(config, adapterId);
|
|
21
|
+
}
|
|
22
|
+
/** Default admission is capability-driven, not a hard-coded Provider list. */
|
|
23
|
+
export function providerRetryAdapterEnabled(config, adapterId) {
|
|
24
|
+
return config.adapters === "all-capable" || config.adapters.includes(adapterId);
|
|
21
25
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { requireIdentity, requireTimestamp } from "../domain/validation.js";
|
|
3
|
+
export const WORKFLOW_OUTCOME_REQUEST_TIMEOUT_MS = 30_000;
|
|
4
|
+
export function createWorkflowOutcomeRequest(input) {
|
|
5
|
+
const requestedAt = input.now.toISOString();
|
|
6
|
+
return validateAgentRunControlRequest({
|
|
7
|
+
schemaVersion: 1,
|
|
8
|
+
kind: "workflow-outcome",
|
|
9
|
+
requestId: createHash("sha256")
|
|
10
|
+
.update(`${input.runId}\0${input.nativeTurnId}\0${requestedAt}`)
|
|
11
|
+
.digest("hex"),
|
|
12
|
+
state: "dispatching",
|
|
13
|
+
nativeTurnId: input.nativeTurnId,
|
|
14
|
+
receiptId: `workflow-outcome-${input.runId}-${input.nativeTurnId}`,
|
|
15
|
+
requestedAt,
|
|
16
|
+
deadlineAt: new Date(input.now.getTime() + WORKFLOW_OUTCOME_REQUEST_TIMEOUT_MS).toISOString()
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export function validateAgentRunControlRequest(value) {
|
|
20
|
+
if (value.schemaVersion !== 1 || value.kind !== "workflow-outcome") {
|
|
21
|
+
throw new Error("Agent Run control request is unsupported.");
|
|
22
|
+
}
|
|
23
|
+
if (value.state !== "dispatching" && value.state !== "awaiting-outcome") {
|
|
24
|
+
throw new Error("Agent Run control request state is invalid.");
|
|
25
|
+
}
|
|
26
|
+
requireIdentity(value.requestId, "Agent Run control request id");
|
|
27
|
+
requireIdentity(value.nativeTurnId, "Agent Run control native Turn id");
|
|
28
|
+
requireIdentity(value.receiptId, "Agent Run control receipt id");
|
|
29
|
+
requireTimestamp(value.requestedAt, "Agent Run control requestedAt");
|
|
30
|
+
requireTimestamp(value.deadlineAt, "Agent Run control deadlineAt");
|
|
31
|
+
if (Date.parse(value.deadlineAt) <= Date.parse(value.requestedAt)) {
|
|
32
|
+
throw new Error("Agent Run control deadline must follow requestedAt.");
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
export function markWorkflowOutcomeRequestDispatched(value) {
|
|
37
|
+
validateAgentRunControlRequest(value);
|
|
38
|
+
return value.state === "dispatching"
|
|
39
|
+
? validateAgentRunControlRequest({ ...value, state: "awaiting-outcome" })
|
|
40
|
+
: value;
|
|
41
|
+
}
|
|
42
|
+
export function serializeWorkflowOutcomeRequestEnvelope(input) {
|
|
43
|
+
validateAgentRunControlRequest(input.request);
|
|
44
|
+
return [
|
|
45
|
+
"Yui managed workflow-outcome request for the existing Run.",
|
|
46
|
+
`task=${requireIdentity(input.taskId, "Task id")} run=${requireIdentity(input.runId, "Run id")} role=${requireIdentity(input.roleName, "Role name")}`,
|
|
47
|
+
`request=${input.request.requestId} completedTurn=${input.request.nativeTurnId}`,
|
|
48
|
+
"Record the required Yui workflow outcome now. Do not replay the Assignment or repeat completed work."
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
@@ -41,6 +41,36 @@ export function validateAgentDriverCapabilities(input) {
|
|
|
41
41
|
throw new Error(`Agent Driver control capability ${name} must be boolean.`);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
const lifecycle = input.lifecycle;
|
|
45
|
+
if (lifecycle === null || typeof lifecycle !== "object" || Array.isArray(lifecycle)) {
|
|
46
|
+
throw new Error("Agent Driver lifecycle capabilities must be an object.");
|
|
47
|
+
}
|
|
48
|
+
if (lifecycle.host !== "persistent") {
|
|
49
|
+
throw new Error("Managed Agent Host lifecycle must be persistent.");
|
|
50
|
+
}
|
|
51
|
+
if (lifecycle.providerProcess !== "persistent" && lifecycle.providerProcess !== "per-turn") {
|
|
52
|
+
throw new Error("Provider process lifecycle capability is invalid.");
|
|
53
|
+
}
|
|
54
|
+
if (lifecycle.nativeConversationResume !== "exact"
|
|
55
|
+
&& lifecycle.nativeConversationResume !== "unsupported") {
|
|
56
|
+
throw new Error("Native conversation resume capability is invalid.");
|
|
57
|
+
}
|
|
58
|
+
if (!["automatic", "native-explicit", "unsupported", "unknown"].includes(lifecycle.compaction)) {
|
|
59
|
+
throw new Error("Native compaction capability is invalid.");
|
|
60
|
+
}
|
|
61
|
+
if (lifecycle.compactionEvents !== "exact" && lifecycle.compactionEvents !== "unavailable") {
|
|
62
|
+
throw new Error("Native compaction event capability is invalid.");
|
|
63
|
+
}
|
|
64
|
+
if (!["per-request", "remaining-context", "cumulative-only", "unavailable"].includes(lifecycle.contextUsage)) {
|
|
65
|
+
throw new Error("Context usage capability is invalid.");
|
|
66
|
+
}
|
|
67
|
+
if (typeof lifecycle.inSessionContinuation !== "boolean") {
|
|
68
|
+
throw new Error("In-Session continuation capability must be boolean.");
|
|
69
|
+
}
|
|
70
|
+
if (lifecycle.deliveryDeduplication !== "exact"
|
|
71
|
+
&& lifecycle.deliveryDeduplication !== "unsupported") {
|
|
72
|
+
throw new Error("Delivery deduplication capability is invalid.");
|
|
73
|
+
}
|
|
44
74
|
const observation = input.observation;
|
|
45
75
|
if (observation === null || typeof observation !== "object" || Array.isArray(observation)) {
|
|
46
76
|
throw new Error("Agent Driver observation capabilities must be an object.");
|
|
@@ -92,6 +122,16 @@ export function validateAgentDriverCapabilities(input) {
|
|
|
92
122
|
}
|
|
93
123
|
return Object.freeze({
|
|
94
124
|
surfaces: Object.freeze(surfaces),
|
|
125
|
+
lifecycle: Object.freeze({
|
|
126
|
+
host: lifecycle.host,
|
|
127
|
+
providerProcess: lifecycle.providerProcess,
|
|
128
|
+
nativeConversationResume: lifecycle.nativeConversationResume,
|
|
129
|
+
compaction: lifecycle.compaction,
|
|
130
|
+
compactionEvents: lifecycle.compactionEvents,
|
|
131
|
+
contextUsage: lifecycle.contextUsage,
|
|
132
|
+
inSessionContinuation: lifecycle.inSessionContinuation,
|
|
133
|
+
deliveryDeduplication: lifecycle.deliveryDeduplication
|
|
134
|
+
}),
|
|
95
135
|
control: Object.freeze({
|
|
96
136
|
start: control.start,
|
|
97
137
|
resume: control.resume,
|
|
@@ -149,6 +189,13 @@ export function managedRuntimeAdmission(capabilities) {
|
|
|
149
189
|
if (actual.observation.promptAcceptance !== "exact"
|
|
150
190
|
|| actual.input.acceptance !== "exact")
|
|
151
191
|
missing.push("exact-prompt-acceptance");
|
|
192
|
+
if (actual.lifecycle.host !== "persistent")
|
|
193
|
+
missing.push("persistent-agent-host");
|
|
194
|
+
if (actual.lifecycle.nativeConversationResume !== "exact") {
|
|
195
|
+
missing.push("exact-native-conversation-resume");
|
|
196
|
+
}
|
|
197
|
+
if (!actual.lifecycle.inSessionContinuation)
|
|
198
|
+
missing.push("in-session-continuation");
|
|
152
199
|
if (actual.observation.turnLifecycle !== "exact")
|
|
153
200
|
missing.push("exact-turn-lifecycle");
|
|
154
201
|
return missing.length === 0
|