@agentxm/workspace-operations 0.28.5 → 0.28.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/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/plan/operation-events.d.ts +254 -44
- package/dist/src/plan/operation-events.js +278 -23
- package/dist/src/plan/operation-resolution.d.ts +1 -1
- package/dist/src/plan/operation-resolution.js +2 -0
- package/dist/src/plan/resolve-plan-interaction.d.ts +6 -26
- package/dist/src/plan/resolve-plan-interaction.js +6 -21
- package/dist/src/plan/resolve-plan.js +30 -21
- package/package.json +4 -4
package/dist/src/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export { AtomicityClassSchema, OperationOutcomeSchema, OperationPhaseSchema, Uni
|
|
|
16
16
|
export type { AtomicityClass, MakeOperationResolutionArgs, OperationAtomicity, OperationBlock, OperationFootprintEntry, OperationInterruption, OperationOutcome, OperationPhase, OperationRecovery, OperationResolution, ResolvedUnit, UnitDisposition, UnitState, UnitStateCounts, } from "./plan/operation-resolution.js";
|
|
17
17
|
export { OperationJournal, appendResolvedUnit, appendStartedUnit, recordJournalPhase, getOperationJournal, makeOperationJournal, recordOperationJournal, updateOperationJournal, type OperationJournalService, type OperationJournalState, } from "./plan/operation-journal.js";
|
|
18
18
|
export { applyPlan, type ApplyPlanOptions, type OperationHandler } from "./plan/apply-plan.js";
|
|
19
|
-
export { OperationLifecycle, makeOperationLifecycle,
|
|
19
|
+
export { CurrentOperationUnit, OperationEventSchema, OperationLifecycle, OperationModeSchema, ProgressUnitSchema, SettledOutcomeSchema, awaitDrained, lifecycleEvents, makeOperationLifecycle, makeThrottledUnitProgress, observeUnit, publishOperationEvent, publishPhaseStarted, publishUnitProgress, publishWaitEnded, publishWaiting, settleOperation, subscribeLossless, type ObservedUnit, type OperationEvent, type OperationEventEncoded, type OperationEventInput, type OperationLifecycleService, type OperationMode, type ProgressUnit, type SettledOutcome, } from "./plan/operation-events.js";
|
|
20
20
|
export { ApprovalRecoveryMissing, CandidateFingerprintFailed, OPERATION_ERROR_CATEGORIES, OperationErrorCategorySchema, PlanInteractionFailed, STALE_CANDIDATE_DETAIL, StaleExecutionCandidate, StepFailure, type OperationErrorCategory, } from "./plan/errors.js";
|
|
21
21
|
export { previewOrApplyPlan } from "./plan/resolve-plan.js";
|
|
22
22
|
export { ResolvePlanInteraction, type ApplyConfirmation, type ResolvePlanInteractionService, } from "./plan/resolve-plan-interaction.js";
|
package/dist/src/index.js
CHANGED
|
@@ -19,7 +19,8 @@ export { AtomicityClassSchema, OperationOutcomeSchema, OperationPhaseSchema, Uni
|
|
|
19
19
|
export { OperationJournal, appendResolvedUnit, appendStartedUnit, recordJournalPhase, getOperationJournal, makeOperationJournal, recordOperationJournal, updateOperationJournal, } from "./plan/operation-journal.js";
|
|
20
20
|
// Apply plan + operation handler registry
|
|
21
21
|
export { applyPlan } from "./plan/apply-plan.js";
|
|
22
|
-
|
|
22
|
+
// Operation lifecycle events — the live contract every observer subscribes to.
|
|
23
|
+
export { CurrentOperationUnit, OperationEventSchema, OperationLifecycle, OperationModeSchema, ProgressUnitSchema, SettledOutcomeSchema, awaitDrained, lifecycleEvents, makeOperationLifecycle, makeThrottledUnitProgress, observeUnit, publishOperationEvent, publishPhaseStarted, publishUnitProgress, publishWaitEnded, publishWaiting, settleOperation, subscribeLossless, } from "./plan/operation-events.js";
|
|
23
24
|
// Serialized error vocabulary and the plan-family tagged errors.
|
|
24
25
|
export { ApprovalRecoveryMissing, CandidateFingerprintFailed, OPERATION_ERROR_CATEGORIES, OperationErrorCategorySchema, PlanInteractionFailed, STALE_CANDIDATE_DETAIL, StaleExecutionCandidate, StepFailure, } from "./plan/errors.js";
|
|
25
26
|
// Interactive preview/apply orchestration over the workspace read model.
|
|
@@ -1,62 +1,272 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Operation lifecycle events
|
|
2
|
+
* Operation lifecycle events — the one live contract between a running
|
|
3
|
+
* operation and everything that observes it.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* transitions,
|
|
6
|
-
* invocation-scoped broadcast.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* An operation publishes schema-backed, typed lifecycle events (operation and
|
|
6
|
+
* phase transitions, unit start, progress, and settlement, waiting reasons, and
|
|
7
|
+
* exactly one terminal settled event) to an invocation-scoped broadcast.
|
|
8
|
+
* Observers — the human live frame, the machine event writer, telemetry —
|
|
9
|
+
* subscribe independently; none of them controls execution or keeps a second
|
|
10
|
+
* account of what happened. Events carry identifiers, labels, counts, and
|
|
11
|
+
* states, never presentation phrases; wording belongs to the observer.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
13
|
+
* Broadcast policy: the publisher is unbounded and never applies backpressure.
|
|
14
|
+
* Lifecycle events are discrete state transitions, so their count is bounded
|
|
15
|
+
* by the planned units; continuous measurements such as downloaded bytes are
|
|
16
|
+
* throttled at the producer and never published per chunk. Every operation
|
|
17
|
+
* ends with a settled event, and settled output waits on the drain latch that
|
|
18
|
+
* lossless subscribers complete. Publishing is a no-op when no broadcast is
|
|
19
|
+
* provided.
|
|
12
20
|
*/
|
|
13
21
|
import * as Effect from "effect/Effect";
|
|
22
|
+
import * as Latch from "effect/Latch";
|
|
14
23
|
import * as PubSub from "effect/PubSub";
|
|
24
|
+
import * as Schema from "effect/Schema";
|
|
15
25
|
import type * as Scope from "effect/Scope";
|
|
16
26
|
import * as ServiceMap from "effect/Context";
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
readonly
|
|
33
|
-
readonly
|
|
34
|
-
readonly
|
|
35
|
-
readonly
|
|
36
|
-
readonly
|
|
37
|
-
readonly
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
readonly
|
|
41
|
-
readonly
|
|
42
|
-
readonly
|
|
43
|
-
readonly
|
|
44
|
-
}
|
|
27
|
+
import * as Stream from "effect/Stream";
|
|
28
|
+
import { BlockingClassSchema } from "./plan.js";
|
|
29
|
+
import { OperationPhaseSchema } from "./operation-resolution.js";
|
|
30
|
+
export declare const OperationModeSchema: Schema.Literals<readonly ["preview", "apply"]>;
|
|
31
|
+
export type OperationMode = typeof OperationModeSchema.Type;
|
|
32
|
+
/**
|
|
33
|
+
* Terminal outcome carried by the settled event: a plan-family outcome, or
|
|
34
|
+
* `completed` for an operation without a plan-family resolution (a read, an
|
|
35
|
+
* upgrade step sequence, an authentication flow) that finished successfully.
|
|
36
|
+
*/
|
|
37
|
+
export declare const SettledOutcomeSchema: Schema.Literals<readonly ["previewed", "applied", "no-op", "partial", "failed", "blocked", "cancelled", "interrupted", "completed"]>;
|
|
38
|
+
export type SettledOutcome = typeof SettledOutcomeSchema.Type;
|
|
39
|
+
export declare const ProgressUnitSchema: Schema.Literals<readonly ["bytes", "files", "items"]>;
|
|
40
|
+
export type ProgressUnit = typeof ProgressUnitSchema.Type;
|
|
41
|
+
export declare const OperationStartedEventSchema: Schema.Struct<{
|
|
42
|
+
readonly _tag: Schema.tag<"OperationStarted">;
|
|
43
|
+
readonly operationId: Schema.String;
|
|
44
|
+
readonly name: Schema.String;
|
|
45
|
+
readonly mode: Schema.Literals<readonly ["preview", "apply"]>;
|
|
46
|
+
readonly seq: Schema.Number;
|
|
47
|
+
readonly atMs: Schema.Number;
|
|
48
|
+
}>;
|
|
49
|
+
export declare const PhaseStartedEventSchema: Schema.Struct<{
|
|
50
|
+
readonly _tag: Schema.tag<"PhaseStarted">;
|
|
51
|
+
readonly phase: Schema.Literals<readonly ["resolution", "planning", "preview", "confirmation", "validation", "apply", "restoration"]>;
|
|
52
|
+
readonly seq: Schema.Number;
|
|
53
|
+
readonly atMs: Schema.Number;
|
|
54
|
+
}>;
|
|
55
|
+
export declare const UnitStartedEventSchema: Schema.Struct<{
|
|
56
|
+
readonly _tag: Schema.tag<"UnitStarted">;
|
|
57
|
+
readonly unitId: Schema.String;
|
|
58
|
+
readonly label: Schema.String;
|
|
59
|
+
readonly index: Schema.Number;
|
|
60
|
+
readonly total: Schema.optional<Schema.Number>;
|
|
61
|
+
readonly parentUnitId: Schema.optional<Schema.String>;
|
|
62
|
+
readonly seq: Schema.Number;
|
|
63
|
+
readonly atMs: Schema.Number;
|
|
64
|
+
}>;
|
|
65
|
+
export declare const UnitProgressEventSchema: Schema.Struct<{
|
|
66
|
+
readonly _tag: Schema.tag<"UnitProgress">;
|
|
67
|
+
readonly unitId: Schema.String;
|
|
68
|
+
readonly done: Schema.Number;
|
|
69
|
+
readonly total: Schema.optional<Schema.Number>;
|
|
70
|
+
readonly unit: Schema.Literals<readonly ["bytes", "files", "items"]>;
|
|
71
|
+
readonly seq: Schema.Number;
|
|
72
|
+
readonly atMs: Schema.Number;
|
|
73
|
+
}>;
|
|
74
|
+
export declare const UnitResolvedEventSchema: Schema.Struct<{
|
|
75
|
+
readonly _tag: Schema.tag<"UnitResolved">;
|
|
76
|
+
readonly unitId: Schema.String;
|
|
77
|
+
readonly label: Schema.String;
|
|
78
|
+
readonly state: Schema.Literals<readonly ["planned", "ready", "committed", "unchanged", "failed", "rolled-back", "blocked", "skipped", "cancelled", "interrupted"]>;
|
|
79
|
+
readonly index: Schema.Number;
|
|
80
|
+
readonly total: Schema.optional<Schema.Number>;
|
|
81
|
+
readonly seq: Schema.Number;
|
|
82
|
+
readonly atMs: Schema.Number;
|
|
83
|
+
}>;
|
|
84
|
+
export declare const WaitingEventSchema: Schema.Struct<{
|
|
85
|
+
readonly _tag: Schema.tag<"Waiting">;
|
|
86
|
+
readonly blockingClass: Schema.Literals<readonly ["approval-required", "override-required", "precondition-unmet", "dependency-failed", "dependency-cycle", "stale-candidate", "policy-excluded", "resource-conflict", "external-blocked", "operation-aborted"]>;
|
|
87
|
+
readonly subject: Schema.String;
|
|
88
|
+
readonly detail: Schema.String;
|
|
89
|
+
readonly seq: Schema.Number;
|
|
90
|
+
readonly atMs: Schema.Number;
|
|
91
|
+
}>;
|
|
92
|
+
export declare const WaitEndedEventSchema: Schema.Struct<{
|
|
93
|
+
readonly _tag: Schema.tag<"WaitEnded">;
|
|
94
|
+
readonly subject: Schema.String;
|
|
95
|
+
readonly seq: Schema.Number;
|
|
96
|
+
readonly atMs: Schema.Number;
|
|
97
|
+
}>;
|
|
98
|
+
export declare const OperationSettledEventSchema: Schema.Struct<{
|
|
99
|
+
readonly _tag: Schema.tag<"OperationSettled">;
|
|
100
|
+
readonly outcome: Schema.Literals<readonly ["previewed", "applied", "no-op", "partial", "failed", "blocked", "cancelled", "interrupted", "completed"]>;
|
|
101
|
+
readonly seq: Schema.Number;
|
|
102
|
+
readonly atMs: Schema.Number;
|
|
103
|
+
}>;
|
|
104
|
+
/**
|
|
105
|
+
* The published lifecycle event union. `seq` is strictly increasing within one
|
|
106
|
+
* operation; `atMs` is wall-clock milliseconds. Exactly one `OperationSettled`
|
|
107
|
+
* event ends every operation.
|
|
108
|
+
*/
|
|
109
|
+
export declare const OperationEventSchema: Schema.Union<readonly [Schema.Struct<{
|
|
110
|
+
readonly _tag: Schema.tag<"OperationStarted">;
|
|
111
|
+
readonly operationId: Schema.String;
|
|
112
|
+
readonly name: Schema.String;
|
|
113
|
+
readonly mode: Schema.Literals<readonly ["preview", "apply"]>;
|
|
114
|
+
readonly seq: Schema.Number;
|
|
115
|
+
readonly atMs: Schema.Number;
|
|
116
|
+
}>, Schema.Struct<{
|
|
117
|
+
readonly _tag: Schema.tag<"PhaseStarted">;
|
|
118
|
+
readonly phase: Schema.Literals<readonly ["resolution", "planning", "preview", "confirmation", "validation", "apply", "restoration"]>;
|
|
119
|
+
readonly seq: Schema.Number;
|
|
120
|
+
readonly atMs: Schema.Number;
|
|
121
|
+
}>, Schema.Struct<{
|
|
122
|
+
readonly _tag: Schema.tag<"UnitStarted">;
|
|
123
|
+
readonly unitId: Schema.String;
|
|
124
|
+
readonly label: Schema.String;
|
|
125
|
+
readonly index: Schema.Number;
|
|
126
|
+
readonly total: Schema.optional<Schema.Number>;
|
|
127
|
+
readonly parentUnitId: Schema.optional<Schema.String>;
|
|
128
|
+
readonly seq: Schema.Number;
|
|
129
|
+
readonly atMs: Schema.Number;
|
|
130
|
+
}>, Schema.Struct<{
|
|
131
|
+
readonly _tag: Schema.tag<"UnitProgress">;
|
|
132
|
+
readonly unitId: Schema.String;
|
|
133
|
+
readonly done: Schema.Number;
|
|
134
|
+
readonly total: Schema.optional<Schema.Number>;
|
|
135
|
+
readonly unit: Schema.Literals<readonly ["bytes", "files", "items"]>;
|
|
136
|
+
readonly seq: Schema.Number;
|
|
137
|
+
readonly atMs: Schema.Number;
|
|
138
|
+
}>, Schema.Struct<{
|
|
139
|
+
readonly _tag: Schema.tag<"UnitResolved">;
|
|
140
|
+
readonly unitId: Schema.String;
|
|
141
|
+
readonly label: Schema.String;
|
|
142
|
+
readonly state: Schema.Literals<readonly ["planned", "ready", "committed", "unchanged", "failed", "rolled-back", "blocked", "skipped", "cancelled", "interrupted"]>;
|
|
143
|
+
readonly index: Schema.Number;
|
|
144
|
+
readonly total: Schema.optional<Schema.Number>;
|
|
145
|
+
readonly seq: Schema.Number;
|
|
146
|
+
readonly atMs: Schema.Number;
|
|
147
|
+
}>, Schema.Struct<{
|
|
148
|
+
readonly _tag: Schema.tag<"Waiting">;
|
|
149
|
+
readonly blockingClass: Schema.Literals<readonly ["approval-required", "override-required", "precondition-unmet", "dependency-failed", "dependency-cycle", "stale-candidate", "policy-excluded", "resource-conflict", "external-blocked", "operation-aborted"]>;
|
|
150
|
+
readonly subject: Schema.String;
|
|
151
|
+
readonly detail: Schema.String;
|
|
152
|
+
readonly seq: Schema.Number;
|
|
153
|
+
readonly atMs: Schema.Number;
|
|
154
|
+
}>, Schema.Struct<{
|
|
155
|
+
readonly _tag: Schema.tag<"WaitEnded">;
|
|
156
|
+
readonly subject: Schema.String;
|
|
157
|
+
readonly seq: Schema.Number;
|
|
158
|
+
readonly atMs: Schema.Number;
|
|
159
|
+
}>, Schema.Struct<{
|
|
160
|
+
readonly _tag: Schema.tag<"OperationSettled">;
|
|
161
|
+
readonly outcome: Schema.Literals<readonly ["previewed", "applied", "no-op", "partial", "failed", "blocked", "cancelled", "interrupted", "completed"]>;
|
|
162
|
+
readonly seq: Schema.Number;
|
|
163
|
+
readonly atMs: Schema.Number;
|
|
164
|
+
}>]>;
|
|
165
|
+
export type OperationEvent = typeof OperationEventSchema.Type;
|
|
166
|
+
export type OperationEventEncoded = typeof OperationEventSchema.Encoded;
|
|
167
|
+
/** Fields the service fills in for every event it publishes. */
|
|
168
|
+
export type OperationEventInput = (seq: number, atMs: number) => OperationEvent;
|
|
45
169
|
export interface OperationLifecycleService {
|
|
46
|
-
readonly
|
|
47
|
-
readonly
|
|
170
|
+
readonly operationId: string;
|
|
171
|
+
readonly name: string;
|
|
172
|
+
readonly mode: OperationMode;
|
|
173
|
+
/** Unbounded, invocation-scoped broadcast; subscribe before the operation runs. */
|
|
174
|
+
readonly events: PubSub.PubSub<OperationEvent>;
|
|
175
|
+
/** Publish one event; the service assigns `seq` and `atMs` atomically. */
|
|
176
|
+
readonly publish: (make: OperationEventInput) => Effect.Effect<void>;
|
|
177
|
+
/** Next index for a unit whose caller has no planned index. */
|
|
178
|
+
readonly nextUnitIndex: Effect.Effect<number>;
|
|
179
|
+
/** Publish the terminal event once; later calls are no-ops. */
|
|
180
|
+
readonly settle: (outcome: SettledOutcome) => Effect.Effect<void>;
|
|
181
|
+
/** Whether the terminal event was published. */
|
|
182
|
+
readonly settled: Effect.Effect<boolean>;
|
|
183
|
+
/** Opens when the operation settled and every registered lossless subscriber acknowledged. */
|
|
184
|
+
readonly drained: Latch.Latch;
|
|
185
|
+
/**
|
|
186
|
+
* Register a subscriber that must observe every event through settlement.
|
|
187
|
+
* Returns the acknowledgement; run it under `Effect.ensuring` so
|
|
188
|
+
* interruption also acknowledges.
|
|
189
|
+
*/
|
|
190
|
+
readonly registerLossless: Effect.Effect<Effect.Effect<void>>;
|
|
48
191
|
}
|
|
49
192
|
declare const OperationLifecycle_base: ServiceMap.ServiceClass<OperationLifecycle, "@agentxm/workspace-operations/plan/operation-events/OperationLifecycle", OperationLifecycleService>;
|
|
50
193
|
export declare class OperationLifecycle extends OperationLifecycle_base {
|
|
51
194
|
}
|
|
195
|
+
export declare const makeOperationLifecycle: (args: {
|
|
196
|
+
readonly name: string;
|
|
197
|
+
readonly mode: OperationMode;
|
|
198
|
+
}) => Effect.Effect<OperationLifecycleService>;
|
|
52
199
|
/** Publish one lifecycle event. No-op when no broadcast is provided. */
|
|
53
|
-
export declare const
|
|
54
|
-
export declare const publishPhaseStarted: (phase:
|
|
200
|
+
export declare const publishOperationEvent: (make: OperationEventInput) => Effect.Effect<void>;
|
|
201
|
+
export declare const publishPhaseStarted: (phase: typeof OperationPhaseSchema.Type) => Effect.Effect<void>;
|
|
202
|
+
export declare const publishWaiting: (wait: {
|
|
203
|
+
readonly blockingClass: typeof BlockingClassSchema.Type;
|
|
204
|
+
readonly subject: string;
|
|
205
|
+
readonly detail: string;
|
|
206
|
+
}) => Effect.Effect<void>;
|
|
207
|
+
export declare const publishWaitEnded: (subject: string) => Effect.Effect<void>;
|
|
208
|
+
declare const CurrentOperationUnit_base: ServiceMap.ServiceClass<CurrentOperationUnit, "@agentxm/workspace-operations/plan/operation-events/CurrentOperationUnit", {
|
|
209
|
+
readonly unitId: string;
|
|
210
|
+
}>;
|
|
211
|
+
/**
|
|
212
|
+
* The unit whose run is in progress, so nested producers (a download inside a
|
|
213
|
+
* step) can attribute continuous progress without threading identifiers.
|
|
214
|
+
*/
|
|
215
|
+
export declare class CurrentOperationUnit extends CurrentOperationUnit_base {
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Publish a continuous measurement for the current unit. Producers throttle
|
|
219
|
+
* before calling: a download publishes tens of events, never one per chunk.
|
|
220
|
+
* No-op without a broadcast or without a current unit.
|
|
221
|
+
*/
|
|
222
|
+
export declare const publishUnitProgress: (progress: {
|
|
223
|
+
readonly done: number;
|
|
224
|
+
readonly total?: number | undefined;
|
|
225
|
+
readonly unit: ProgressUnit;
|
|
226
|
+
}) => Effect.Effect<void>;
|
|
227
|
+
/**
|
|
228
|
+
* Time-gated progress publisher for a producer loop: at most one event per
|
|
229
|
+
* `intervalMs` of wall clock, plus the final measurement when `done` reaches
|
|
230
|
+
* `total`. Keeps continuous measurements to tens of events per unit.
|
|
231
|
+
*/
|
|
232
|
+
export declare const makeThrottledUnitProgress: (options: {
|
|
233
|
+
readonly unit: ProgressUnit;
|
|
234
|
+
readonly intervalMs?: number | undefined;
|
|
235
|
+
}) => Effect.Effect<(done: number, total?: number | undefined) => Effect.Effect<void>>;
|
|
236
|
+
export interface ObservedUnit {
|
|
237
|
+
readonly id: string;
|
|
238
|
+
readonly label: string;
|
|
239
|
+
/** Planned position; assigned by the service when the caller has none. */
|
|
240
|
+
readonly index?: number | undefined;
|
|
241
|
+
readonly total?: number | undefined;
|
|
242
|
+
readonly parentUnitId?: string | undefined;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Run one unit of work under the lifecycle: `UnitStarted` before, then
|
|
246
|
+
* `UnitResolved` with the state the exit proves (`committed`, `failed`, or
|
|
247
|
+
* `interrupted`). The unit identity is provided to the run so nested producers
|
|
248
|
+
* can publish progress. No-op wrapper without a broadcast.
|
|
249
|
+
*/
|
|
250
|
+
export declare const observeUnit: <A, E, R>(unit: ObservedUnit, effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
251
|
+
/** Publish the terminal event once. No-op without a broadcast. */
|
|
252
|
+
export declare const settleOperation: (outcome: SettledOutcome) => Effect.Effect<void>;
|
|
253
|
+
/**
|
|
254
|
+
* Wait until every lossless subscriber has consumed the terminal event.
|
|
255
|
+
* Returns immediately when no broadcast is provided or the operation has not
|
|
256
|
+
* settled, so callers cannot deadlock on an operation that never ends.
|
|
257
|
+
*/
|
|
258
|
+
export declare const awaitDrained: Effect.Effect<void>;
|
|
259
|
+
/**
|
|
260
|
+
* Subscribe to the broadcast within the current scope. The subscription is
|
|
261
|
+
* created immediately — before this effect returns — so events published
|
|
262
|
+
* afterwards are never missed; the stream ends with the terminal event.
|
|
263
|
+
*/
|
|
264
|
+
export declare const lifecycleEvents: (service: OperationLifecycleService) => Effect.Effect<Stream.Stream<OperationEvent>, never, Scope.Scope>;
|
|
55
265
|
/**
|
|
56
|
-
* Fork a subscriber that observes every
|
|
57
|
-
*
|
|
266
|
+
* Fork a subscriber that observes every event through settlement and holds
|
|
267
|
+
* the drain latch until it has processed the terminal event. The subscription
|
|
268
|
+
* attaches before this effect returns. No-op when no broadcast is provided.
|
|
58
269
|
*/
|
|
59
|
-
export declare const
|
|
60
|
-
export declare const makeOperationLifecycle: (mode: "preview" | "apply") => Effect.Effect<OperationLifecycleService>;
|
|
270
|
+
export declare const subscribeLossless: (service: OperationLifecycleService, observe: (event: OperationEvent) => Effect.Effect<void>) => Effect.Effect<void, never, Scope.Scope>;
|
|
61
271
|
export {};
|
|
62
272
|
//# sourceMappingURL=operation-events.d.ts.map
|
|
@@ -1,41 +1,296 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Operation lifecycle events
|
|
2
|
+
* Operation lifecycle events — the one live contract between a running
|
|
3
|
+
* operation and everything that observes it.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* transitions,
|
|
6
|
-
* invocation-scoped broadcast.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* An operation publishes schema-backed, typed lifecycle events (operation and
|
|
6
|
+
* phase transitions, unit start, progress, and settlement, waiting reasons, and
|
|
7
|
+
* exactly one terminal settled event) to an invocation-scoped broadcast.
|
|
8
|
+
* Observers — the human live frame, the machine event writer, telemetry —
|
|
9
|
+
* subscribe independently; none of them controls execution or keeps a second
|
|
10
|
+
* account of what happened. Events carry identifiers, labels, counts, and
|
|
11
|
+
* states, never presentation phrases; wording belongs to the observer.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
13
|
+
* Broadcast policy: the publisher is unbounded and never applies backpressure.
|
|
14
|
+
* Lifecycle events are discrete state transitions, so their count is bounded
|
|
15
|
+
* by the planned units; continuous measurements such as downloaded bytes are
|
|
16
|
+
* throttled at the producer and never published per chunk. Every operation
|
|
17
|
+
* ends with a settled event, and settled output waits on the drain latch that
|
|
18
|
+
* lossless subscribers complete. Publishing is a no-op when no broadcast is
|
|
19
|
+
* provided.
|
|
12
20
|
*/
|
|
21
|
+
import * as Cause from "effect/Cause";
|
|
13
22
|
import * as Clock from "effect/Clock";
|
|
14
23
|
import * as Effect from "effect/Effect";
|
|
24
|
+
import * as Exit from "effect/Exit";
|
|
25
|
+
import * as Latch from "effect/Latch";
|
|
26
|
+
import * as MutableRef from "effect/MutableRef";
|
|
15
27
|
import * as Option from "effect/Option";
|
|
16
28
|
import * as PubSub from "effect/PubSub";
|
|
29
|
+
import * as Ref from "effect/Ref";
|
|
30
|
+
import * as Schema from "effect/Schema";
|
|
17
31
|
import * as ServiceMap from "effect/Context";
|
|
32
|
+
import * as Stream from "effect/Stream";
|
|
33
|
+
import { BlockingClassSchema } from "./plan.js";
|
|
34
|
+
import { OperationPhaseSchema, UnitStateSchema } from "./operation-resolution.js";
|
|
35
|
+
// -----------------------------------------------------------------------------
|
|
36
|
+
// Event schema
|
|
37
|
+
// -----------------------------------------------------------------------------
|
|
38
|
+
export const OperationModeSchema = Schema.Literals(["preview", "apply"]).annotate({
|
|
39
|
+
identifier: "OperationMode",
|
|
40
|
+
title: "Operation Mode",
|
|
41
|
+
description: "Whether the operation previews or applies its plan.",
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* Terminal outcome carried by the settled event: a plan-family outcome, or
|
|
45
|
+
* `completed` for an operation without a plan-family resolution (a read, an
|
|
46
|
+
* upgrade step sequence, an authentication flow) that finished successfully.
|
|
47
|
+
*/
|
|
48
|
+
export const SettledOutcomeSchema = Schema.Literals([
|
|
49
|
+
"previewed",
|
|
50
|
+
"applied",
|
|
51
|
+
"no-op",
|
|
52
|
+
"partial",
|
|
53
|
+
"failed",
|
|
54
|
+
"blocked",
|
|
55
|
+
"cancelled",
|
|
56
|
+
"interrupted",
|
|
57
|
+
"completed",
|
|
58
|
+
]).annotate({
|
|
59
|
+
identifier: "SettledOutcome",
|
|
60
|
+
title: "Settled Outcome",
|
|
61
|
+
description: "Terminal outcome of an observed operation.",
|
|
62
|
+
});
|
|
63
|
+
export const ProgressUnitSchema = Schema.Literals(["bytes", "files", "items"]).annotate({
|
|
64
|
+
identifier: "ProgressUnit",
|
|
65
|
+
title: "Progress Unit",
|
|
66
|
+
description: "Unit of a continuous progress measurement.",
|
|
67
|
+
});
|
|
68
|
+
/** Per-operation monotonic order and wall-clock time, on every event. */
|
|
69
|
+
const EventBase = {
|
|
70
|
+
seq: Schema.Number,
|
|
71
|
+
atMs: Schema.Number,
|
|
72
|
+
};
|
|
73
|
+
export const OperationStartedEventSchema = Schema.TaggedStruct("OperationStarted", {
|
|
74
|
+
...EventBase,
|
|
75
|
+
operationId: Schema.String,
|
|
76
|
+
name: Schema.String,
|
|
77
|
+
mode: OperationModeSchema,
|
|
78
|
+
}).annotate({ identifier: "OperationStartedEvent" });
|
|
79
|
+
export const PhaseStartedEventSchema = Schema.TaggedStruct("PhaseStarted", {
|
|
80
|
+
...EventBase,
|
|
81
|
+
phase: OperationPhaseSchema,
|
|
82
|
+
}).annotate({ identifier: "PhaseStartedEvent" });
|
|
83
|
+
export const UnitStartedEventSchema = Schema.TaggedStruct("UnitStarted", {
|
|
84
|
+
...EventBase,
|
|
85
|
+
unitId: Schema.String,
|
|
86
|
+
label: Schema.String,
|
|
87
|
+
index: Schema.Number,
|
|
88
|
+
total: Schema.optional(Schema.Number),
|
|
89
|
+
parentUnitId: Schema.optional(Schema.String),
|
|
90
|
+
}).annotate({ identifier: "UnitStartedEvent" });
|
|
91
|
+
export const UnitProgressEventSchema = Schema.TaggedStruct("UnitProgress", {
|
|
92
|
+
...EventBase,
|
|
93
|
+
unitId: Schema.String,
|
|
94
|
+
done: Schema.Number,
|
|
95
|
+
total: Schema.optional(Schema.Number),
|
|
96
|
+
unit: ProgressUnitSchema,
|
|
97
|
+
}).annotate({ identifier: "UnitProgressEvent" });
|
|
98
|
+
export const UnitResolvedEventSchema = Schema.TaggedStruct("UnitResolved", {
|
|
99
|
+
...EventBase,
|
|
100
|
+
unitId: Schema.String,
|
|
101
|
+
label: Schema.String,
|
|
102
|
+
state: UnitStateSchema,
|
|
103
|
+
index: Schema.Number,
|
|
104
|
+
total: Schema.optional(Schema.Number),
|
|
105
|
+
}).annotate({ identifier: "UnitResolvedEvent" });
|
|
106
|
+
export const WaitingEventSchema = Schema.TaggedStruct("Waiting", {
|
|
107
|
+
...EventBase,
|
|
108
|
+
blockingClass: BlockingClassSchema,
|
|
109
|
+
subject: Schema.String,
|
|
110
|
+
detail: Schema.String,
|
|
111
|
+
}).annotate({ identifier: "WaitingEvent" });
|
|
112
|
+
export const WaitEndedEventSchema = Schema.TaggedStruct("WaitEnded", {
|
|
113
|
+
...EventBase,
|
|
114
|
+
subject: Schema.String,
|
|
115
|
+
}).annotate({ identifier: "WaitEndedEvent" });
|
|
116
|
+
export const OperationSettledEventSchema = Schema.TaggedStruct("OperationSettled", {
|
|
117
|
+
...EventBase,
|
|
118
|
+
outcome: SettledOutcomeSchema,
|
|
119
|
+
}).annotate({ identifier: "OperationSettledEvent" });
|
|
120
|
+
/**
|
|
121
|
+
* The published lifecycle event union. `seq` is strictly increasing within one
|
|
122
|
+
* operation; `atMs` is wall-clock milliseconds. Exactly one `OperationSettled`
|
|
123
|
+
* event ends every operation.
|
|
124
|
+
*/
|
|
125
|
+
export const OperationEventSchema = Schema.Union([
|
|
126
|
+
OperationStartedEventSchema,
|
|
127
|
+
PhaseStartedEventSchema,
|
|
128
|
+
UnitStartedEventSchema,
|
|
129
|
+
UnitProgressEventSchema,
|
|
130
|
+
UnitResolvedEventSchema,
|
|
131
|
+
WaitingEventSchema,
|
|
132
|
+
WaitEndedEventSchema,
|
|
133
|
+
OperationSettledEventSchema,
|
|
134
|
+
]).annotate({
|
|
135
|
+
identifier: "OperationEvent",
|
|
136
|
+
title: "Operation Lifecycle Event",
|
|
137
|
+
description: "One typed lifecycle event of a running AXM operation: operation, phase, unit, waiting, or settlement transition.",
|
|
138
|
+
});
|
|
18
139
|
export class OperationLifecycle extends ServiceMap.Service()("@agentxm/workspace-operations/plan/operation-events/OperationLifecycle") {
|
|
19
140
|
}
|
|
141
|
+
const operationCounter = MutableRef.make(0);
|
|
142
|
+
export const makeOperationLifecycle = (args) => Effect.gen(function* () {
|
|
143
|
+
const events = yield* PubSub.unbounded();
|
|
144
|
+
const sequence = yield* Ref.make(0);
|
|
145
|
+
const unitIndex = yield* Ref.make(0);
|
|
146
|
+
const drain = yield* Ref.make({ settled: false, pending: 0 });
|
|
147
|
+
const drained = yield* Latch.make(false);
|
|
148
|
+
const operationId = `operation-${String(MutableRef.incrementAndGet(operationCounter))}`;
|
|
149
|
+
const openWhenDrained = (state) => state.settled && state.pending === 0 ? Effect.asVoid(drained.open) : Effect.void;
|
|
150
|
+
const publish = (make) => Effect.gen(function* () {
|
|
151
|
+
const seq = yield* Ref.modify(sequence, (current) => [current + 1, current + 1]);
|
|
152
|
+
const atMs = yield* Clock.currentTimeMillis;
|
|
153
|
+
yield* PubSub.publish(events, make(seq, atMs));
|
|
154
|
+
});
|
|
155
|
+
const settle = (outcome) => Effect.gen(function* () {
|
|
156
|
+
const first = yield* Ref.modify(drain, (state) => state.settled
|
|
157
|
+
? [false, state]
|
|
158
|
+
: [true, { ...state, settled: true }]);
|
|
159
|
+
if (!first)
|
|
160
|
+
return;
|
|
161
|
+
yield* publish((seq, atMs) => ({ _tag: "OperationSettled", seq, atMs, outcome }));
|
|
162
|
+
yield* Effect.flatMap(Ref.get(drain), openWhenDrained);
|
|
163
|
+
});
|
|
164
|
+
const registerLossless = Ref.update(drain, (state) => ({
|
|
165
|
+
...state,
|
|
166
|
+
pending: state.pending + 1,
|
|
167
|
+
})).pipe(Effect.as(Ref.modify(drain, (state) => {
|
|
168
|
+
const next = { ...state, pending: Math.max(0, state.pending - 1) };
|
|
169
|
+
return [next, next];
|
|
170
|
+
}).pipe(Effect.flatMap(openWhenDrained))));
|
|
171
|
+
return {
|
|
172
|
+
operationId,
|
|
173
|
+
name: args.name,
|
|
174
|
+
mode: args.mode,
|
|
175
|
+
events,
|
|
176
|
+
publish,
|
|
177
|
+
nextUnitIndex: Ref.modify(unitIndex, (current) => [current, current + 1]),
|
|
178
|
+
settle,
|
|
179
|
+
settled: Effect.map(Ref.get(drain), (state) => state.settled),
|
|
180
|
+
drained,
|
|
181
|
+
registerLossless,
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
// -----------------------------------------------------------------------------
|
|
185
|
+
// Producers (no-ops without a broadcast)
|
|
186
|
+
// -----------------------------------------------------------------------------
|
|
187
|
+
const withLifecycle = (onSome, onNone) => Effect.flatMap(Effect.serviceOption(OperationLifecycle), (service) => Option.isNone(service) ? onNone() : onSome(service.value));
|
|
20
188
|
/** Publish one lifecycle event. No-op when no broadcast is provided. */
|
|
21
|
-
export const
|
|
22
|
-
|
|
23
|
-
|
|
189
|
+
export const publishOperationEvent = (make) => withLifecycle((service) => service.publish(make), () => Effect.void);
|
|
190
|
+
export const publishPhaseStarted = (phase) => publishOperationEvent((seq, atMs) => ({ _tag: "PhaseStarted", seq, atMs, phase }));
|
|
191
|
+
export const publishWaiting = (wait) => publishOperationEvent((seq, atMs) => ({ _tag: "Waiting", seq, atMs, ...wait }));
|
|
192
|
+
export const publishWaitEnded = (subject) => publishOperationEvent((seq, atMs) => ({ _tag: "WaitEnded", seq, atMs, subject }));
|
|
193
|
+
/**
|
|
194
|
+
* The unit whose run is in progress, so nested producers (a download inside a
|
|
195
|
+
* step) can attribute continuous progress without threading identifiers.
|
|
196
|
+
*/
|
|
197
|
+
export class CurrentOperationUnit extends ServiceMap.Service()("@agentxm/workspace-operations/plan/operation-events/CurrentOperationUnit") {
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Publish a continuous measurement for the current unit. Producers throttle
|
|
201
|
+
* before calling: a download publishes tens of events, never one per chunk.
|
|
202
|
+
* No-op without a broadcast or without a current unit.
|
|
203
|
+
*/
|
|
204
|
+
export const publishUnitProgress = (progress) => Effect.flatMap(Effect.serviceOption(CurrentOperationUnit), (unit) => Option.isNone(unit)
|
|
205
|
+
? Effect.void
|
|
206
|
+
: publishOperationEvent((seq, atMs) => ({
|
|
207
|
+
_tag: "UnitProgress",
|
|
208
|
+
seq,
|
|
209
|
+
atMs,
|
|
210
|
+
unitId: unit.value.unitId,
|
|
211
|
+
done: progress.done,
|
|
212
|
+
...(progress.total === undefined ? {} : { total: progress.total }),
|
|
213
|
+
unit: progress.unit,
|
|
214
|
+
})));
|
|
215
|
+
/**
|
|
216
|
+
* Time-gated progress publisher for a producer loop: at most one event per
|
|
217
|
+
* `intervalMs` of wall clock, plus the final measurement when `done` reaches
|
|
218
|
+
* `total`. Keeps continuous measurements to tens of events per unit.
|
|
219
|
+
*/
|
|
220
|
+
export const makeThrottledUnitProgress = (options) => Effect.map(Ref.make(-Infinity), (last) => (done, total) => Effect.gen(function* () {
|
|
221
|
+
const now = yield* Clock.currentTimeMillis;
|
|
222
|
+
const previous = yield* Ref.get(last);
|
|
223
|
+
const final = total !== undefined && done >= total;
|
|
224
|
+
if (!final && now - previous < (options.intervalMs ?? 100))
|
|
24
225
|
return;
|
|
25
|
-
|
|
26
|
-
yield*
|
|
27
|
-
});
|
|
28
|
-
|
|
226
|
+
yield* Ref.set(last, now);
|
|
227
|
+
yield* publishUnitProgress({ done, unit: options.unit, total });
|
|
228
|
+
}));
|
|
229
|
+
const unitStateForExit = (exit) => Exit.isSuccess(exit)
|
|
230
|
+
? "committed"
|
|
231
|
+
: Cause.hasInterruptsOnly(exit.cause)
|
|
232
|
+
? "interrupted"
|
|
233
|
+
: "failed";
|
|
29
234
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
235
|
+
* Run one unit of work under the lifecycle: `UnitStarted` before, then
|
|
236
|
+
* `UnitResolved` with the state the exit proves (`committed`, `failed`, or
|
|
237
|
+
* `interrupted`). The unit identity is provided to the run so nested producers
|
|
238
|
+
* can publish progress. No-op wrapper without a broadcast.
|
|
32
239
|
*/
|
|
33
|
-
export const
|
|
34
|
-
const service = yield* Effect.serviceOption(OperationLifecycle);
|
|
240
|
+
export const observeUnit = (unit, effect) => Effect.flatMap(Effect.serviceOption(OperationLifecycle), (service) => {
|
|
35
241
|
if (Option.isNone(service))
|
|
36
|
-
return;
|
|
37
|
-
const
|
|
38
|
-
|
|
242
|
+
return effect;
|
|
243
|
+
const lifecycle = service.value;
|
|
244
|
+
return Effect.gen(function* () {
|
|
245
|
+
const index = unit.index ?? (yield* lifecycle.nextUnitIndex);
|
|
246
|
+
const total = unit.total === undefined ? {} : { total: unit.total };
|
|
247
|
+
yield* lifecycle.publish((seq, atMs) => ({
|
|
248
|
+
_tag: "UnitStarted",
|
|
249
|
+
seq,
|
|
250
|
+
atMs,
|
|
251
|
+
unitId: unit.id,
|
|
252
|
+
label: unit.label,
|
|
253
|
+
index,
|
|
254
|
+
...total,
|
|
255
|
+
...(unit.parentUnitId === undefined ? {} : { parentUnitId: unit.parentUnitId }),
|
|
256
|
+
}));
|
|
257
|
+
return yield* effect.pipe(Effect.provideService(CurrentOperationUnit, { unitId: unit.id }), Effect.onExit((exit) => lifecycle.publish((seq, atMs) => ({
|
|
258
|
+
_tag: "UnitResolved",
|
|
259
|
+
seq,
|
|
260
|
+
atMs,
|
|
261
|
+
unitId: unit.id,
|
|
262
|
+
label: unit.label,
|
|
263
|
+
state: unitStateForExit(exit),
|
|
264
|
+
index,
|
|
265
|
+
...total,
|
|
266
|
+
}))));
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
/** Publish the terminal event once. No-op without a broadcast. */
|
|
270
|
+
export const settleOperation = (outcome) => withLifecycle((service) => service.settle(outcome), () => Effect.void);
|
|
271
|
+
/**
|
|
272
|
+
* Wait until every lossless subscriber has consumed the terminal event.
|
|
273
|
+
* Returns immediately when no broadcast is provided or the operation has not
|
|
274
|
+
* settled, so callers cannot deadlock on an operation that never ends.
|
|
275
|
+
*/
|
|
276
|
+
export const awaitDrained = withLifecycle((service) => Effect.flatMap(service.settled, (settled) => (settled ? service.drained.await : Effect.void)), () => Effect.void);
|
|
277
|
+
// -----------------------------------------------------------------------------
|
|
278
|
+
// Subscribers
|
|
279
|
+
// -----------------------------------------------------------------------------
|
|
280
|
+
/**
|
|
281
|
+
* Subscribe to the broadcast within the current scope. The subscription is
|
|
282
|
+
* created immediately — before this effect returns — so events published
|
|
283
|
+
* afterwards are never missed; the stream ends with the terminal event.
|
|
284
|
+
*/
|
|
285
|
+
export const lifecycleEvents = (service) => Effect.map(PubSub.subscribe(service.events), (subscription) => Stream.fromSubscription(subscription).pipe(Stream.takeUntil((event) => event._tag === "OperationSettled")));
|
|
286
|
+
/**
|
|
287
|
+
* Fork a subscriber that observes every event through settlement and holds
|
|
288
|
+
* the drain latch until it has processed the terminal event. The subscription
|
|
289
|
+
* attaches before this effect returns. No-op when no broadcast is provided.
|
|
290
|
+
*/
|
|
291
|
+
export const subscribeLossless = (service, observe) => Effect.gen(function* () {
|
|
292
|
+
const ack = yield* service.registerLossless;
|
|
293
|
+
const stream = yield* lifecycleEvents(service);
|
|
294
|
+
yield* stream.pipe(Stream.runForEach(observe), Effect.ensuring(ack), Effect.forkScoped);
|
|
39
295
|
});
|
|
40
|
-
export const makeOperationLifecycle = (mode) => PubSub.unbounded().pipe(Effect.map((pubsub) => ({ mode, pubsub })));
|
|
41
296
|
//# sourceMappingURL=operation-events.js.map
|
|
@@ -27,7 +27,7 @@ import type { ConfiguredAgentOutcome } from "@agentxm/workspace-state";
|
|
|
27
27
|
export declare const UnitStateSchema: Schema.Literals<readonly ["planned", "ready", "committed", "unchanged", "failed", "rolled-back", "blocked", "skipped", "cancelled", "interrupted"]>;
|
|
28
28
|
export type UnitState = typeof UnitStateSchema.Type;
|
|
29
29
|
/** Lifecycle phase in which an operation event (blocking, waiting) occurred. */
|
|
30
|
-
export declare const OperationPhaseSchema: Schema.Literals<readonly ["planning", "preview", "confirmation", "validation", "apply", "restoration"]>;
|
|
30
|
+
export declare const OperationPhaseSchema: Schema.Literals<readonly ["resolution", "planning", "preview", "confirmation", "validation", "apply", "restoration"]>;
|
|
31
31
|
export type OperationPhase = typeof OperationPhaseSchema.Type;
|
|
32
32
|
/** Canonical operation terminal outcomes. */
|
|
33
33
|
export declare const OperationOutcomeSchema: Schema.Literals<readonly ["previewed", "applied", "no-op", "partial", "failed", "blocked", "cancelled", "interrupted"]>;
|
|
@@ -41,6 +41,8 @@ export const UnitStateSchema = Schema.Literals([
|
|
|
41
41
|
});
|
|
42
42
|
/** Lifecycle phase in which an operation event (blocking, waiting) occurred. */
|
|
43
43
|
export const OperationPhaseSchema = Schema.Literals([
|
|
44
|
+
// Resolving requested sources into concrete packages, before planning.
|
|
45
|
+
"resolution",
|
|
44
46
|
"planning",
|
|
45
47
|
"preview",
|
|
46
48
|
"confirmation",
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Plan-resolution interaction port.
|
|
3
3
|
*
|
|
4
|
-
* `previewOrApplyPlan` presents candidates
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* `previewOrApplyPlan` presents candidates and obtains the apply confirmation
|
|
5
|
+
* exclusively through this service. The CLI runtime provides the renderer- and
|
|
6
|
+
* prompt-backed implementation; wording and verbosity gating belong to that
|
|
7
|
+
* implementation, never to the kernel. Progress is not an interaction: the
|
|
8
|
+
* kernel publishes typed lifecycle events (`plan/operation-events`) that
|
|
9
|
+
* observers render.
|
|
9
10
|
*
|
|
10
11
|
* @experimental This API is unstable and may change without notice.
|
|
11
12
|
*/
|
|
12
13
|
import * as Effect from "effect/Effect";
|
|
13
14
|
import * as Layer from "effect/Layer";
|
|
14
|
-
import * as Option from "effect/Option";
|
|
15
15
|
import * as ServiceMap from "effect/Context";
|
|
16
16
|
import type { PlanInteractionFailed } from "./errors.js";
|
|
17
17
|
import type { ConfirmationRecovery } from "./plan-execution.js";
|
|
@@ -34,19 +34,6 @@ export interface ResolvePlanInteractionService {
|
|
|
34
34
|
readonly presentPlan: (plan: Plan<unknown, unknown>, options: {
|
|
35
35
|
readonly mode: "preview" | "apply";
|
|
36
36
|
}) => Effect.Effect<void>;
|
|
37
|
-
/** Progress envelope for lockfile reconciliation. */
|
|
38
|
-
readonly withPlanningProgress: <A, E, R>(planName: string, run: () => Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
39
|
-
/**
|
|
40
|
-
* Progress envelope for apply. The implementation subscribes to the
|
|
41
|
-
* operation lifecycle stream itself (`plan/operation-events`) and maps unit
|
|
42
|
-
* and restoration events to progress updates.
|
|
43
|
-
*/
|
|
44
|
-
readonly withApplyProgress: <A, E, R>(planName: string, run: () => Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
45
|
-
/** Transition-lock contention notice. */
|
|
46
|
-
readonly noteTransitionWait: (holder: Option.Option<{
|
|
47
|
-
readonly command: string;
|
|
48
|
-
readonly pid: number;
|
|
49
|
-
}>) => Effect.Effect<void>;
|
|
50
37
|
}
|
|
51
38
|
declare const ResolvePlanInteraction_base: ServiceMap.ServiceClass<ResolvePlanInteraction, "@agentxm/workspace-operations/plan/resolve-plan-interaction/ResolvePlanInteraction", ResolvePlanInteractionService>;
|
|
52
39
|
export declare class ResolvePlanInteraction extends ResolvePlanInteraction_base {
|
|
@@ -57,9 +44,6 @@ export interface ResolvePlanInteractionTestState {
|
|
|
57
44
|
readonly planName: string;
|
|
58
45
|
readonly mode: "preview" | "apply";
|
|
59
46
|
}>;
|
|
60
|
-
readonly planningProgress: Array<string>;
|
|
61
|
-
readonly applyProgress: Array<string>;
|
|
62
|
-
transitionWaits: number;
|
|
63
47
|
}
|
|
64
48
|
export declare const ResolvePlanInteractionTest: (overrides?: {
|
|
65
49
|
readonly isConfirmationAvailable?: boolean;
|
|
@@ -67,10 +51,6 @@ export declare const ResolvePlanInteractionTest: (overrides?: {
|
|
|
67
51
|
readonly presentPlan?: (plan: Plan<unknown, unknown>, options: {
|
|
68
52
|
readonly mode: "preview" | "apply";
|
|
69
53
|
}) => Effect.Effect<void>;
|
|
70
|
-
readonly noteTransitionWait?: (holder: Option.Option<{
|
|
71
|
-
readonly command: string;
|
|
72
|
-
readonly pid: number;
|
|
73
|
-
}>) => Effect.Effect<void>;
|
|
74
54
|
}) => {
|
|
75
55
|
layer: Layer.Layer<ResolvePlanInteraction, never, never>;
|
|
76
56
|
state: ResolvePlanInteractionTestState;
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Plan-resolution interaction port.
|
|
3
3
|
*
|
|
4
|
-
* `previewOrApplyPlan` presents candidates
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* `previewOrApplyPlan` presents candidates and obtains the apply confirmation
|
|
5
|
+
* exclusively through this service. The CLI runtime provides the renderer- and
|
|
6
|
+
* prompt-backed implementation; wording and verbosity gating belong to that
|
|
7
|
+
* implementation, never to the kernel. Progress is not an interaction: the
|
|
8
|
+
* kernel publishes typed lifecycle events (`plan/operation-events`) that
|
|
9
|
+
* observers render.
|
|
9
10
|
*
|
|
10
11
|
* @experimental This API is unstable and may change without notice.
|
|
11
12
|
*/
|
|
12
13
|
import * as Effect from "effect/Effect";
|
|
13
14
|
import * as Layer from "effect/Layer";
|
|
14
|
-
import * as Option from "effect/Option";
|
|
15
15
|
import * as ServiceMap from "effect/Context";
|
|
16
16
|
export class ResolvePlanInteraction extends ServiceMap.Service()("@agentxm/workspace-operations/plan/resolve-plan-interaction/ResolvePlanInteraction") {
|
|
17
17
|
}
|
|
@@ -19,9 +19,6 @@ export const ResolvePlanInteractionTest = (overrides) => {
|
|
|
19
19
|
const state = {
|
|
20
20
|
confirmApplyChangesCalls: [],
|
|
21
21
|
presentPlanCalls: [],
|
|
22
|
-
planningProgress: [],
|
|
23
|
-
applyProgress: [],
|
|
24
|
-
transitionWaits: 0,
|
|
25
22
|
};
|
|
26
23
|
const layer = Layer.succeed(ResolvePlanInteraction, {
|
|
27
24
|
isConfirmationAvailable: Effect.succeed(overrides?.isConfirmationAvailable ?? false),
|
|
@@ -34,18 +31,6 @@ export const ResolvePlanInteractionTest = (overrides) => {
|
|
|
34
31
|
state.presentPlanCalls.push({ planName: plan.name, mode: options.mode });
|
|
35
32
|
yield* overrides?.presentPlan?.(plan, options) ?? Effect.void;
|
|
36
33
|
}),
|
|
37
|
-
withPlanningProgress: (planName, run) => Effect.suspend(() => {
|
|
38
|
-
state.planningProgress.push(planName);
|
|
39
|
-
return run();
|
|
40
|
-
}),
|
|
41
|
-
withApplyProgress: (planName, run) => Effect.suspend(() => {
|
|
42
|
-
state.applyProgress.push(planName);
|
|
43
|
-
return run();
|
|
44
|
-
}),
|
|
45
|
-
noteTransitionWait: (holder) => Effect.gen(function* () {
|
|
46
|
-
state.transitionWaits += 1;
|
|
47
|
-
yield* overrides?.noteTransitionWait?.(holder) ?? Effect.void;
|
|
48
|
-
}),
|
|
49
34
|
});
|
|
50
35
|
return { layer, state };
|
|
51
36
|
};
|
|
@@ -18,6 +18,7 @@ import * as Path from "effect/Path";
|
|
|
18
18
|
import * as Effect from "effect/Effect";
|
|
19
19
|
import * as Layer from "effect/Layer";
|
|
20
20
|
import * as Option from "effect/Option";
|
|
21
|
+
import * as Ref from "effect/Ref";
|
|
21
22
|
import { ApprovalRecoveryMissing, STALE_CANDIDATE_DETAIL, StaleExecutionCandidate, StepFailure, } from "./errors.js";
|
|
22
23
|
import { applyPlan } from "./apply-plan.js";
|
|
23
24
|
import { isExecutionCandidateFresh, makeExecutionCandidate, } from "./execution-candidate.js";
|
|
@@ -25,7 +26,7 @@ import { augmentPlanWithReconciliation } from "../operations/augment-plan.js";
|
|
|
25
26
|
import { scanPlanReadiness } from "../operations/scan-plan-readiness.js";
|
|
26
27
|
import { declaredAtomicity, executedUnits, makeOperationResolution, plannedUnits, unitIdOf, } from "./operation-resolution.js";
|
|
27
28
|
import { appendResolvedUnit, appendStartedUnit, recordJournalPhase, recordOperationJournal, } from "./operation-journal.js";
|
|
28
|
-
import {
|
|
29
|
+
import { CurrentOperationUnit, observeUnit, publishOperationEvent, publishPhaseStarted, publishWaitEnded, publishWaiting, } from "./operation-events.js";
|
|
29
30
|
import { WorkspaceMutations } from "@agentxm/workspace-state";
|
|
30
31
|
import { readPendingClosureRestorationFailures, WorkspaceRestorationIncomplete, } from "@agentxm/workspace-state";
|
|
31
32
|
import { rollbackWorkspaceClosure, settleWorkspaceClosure, withWorkspaceClosure, } from "../operations/transaction.js";
|
|
@@ -34,7 +35,6 @@ import { InterruptionSignalSource } from "./interruption-signal.js";
|
|
|
34
35
|
import { ResolvePlanInteraction } from "./resolve-plan-interaction.js";
|
|
35
36
|
import { confirmationRecoverySuggestions, namedPolicyRecoverySuggestions, } from "./plan-execution.js";
|
|
36
37
|
import { ConfiguredAgentOutcomesProvider } from "@agentxm/workspace-state";
|
|
37
|
-
import { isMcpServerApplicableToAgent } from "@agentxm/workspace-state";
|
|
38
38
|
import { configuredAgentLifecycleOutcomes } from "@agentxm/workspace-state";
|
|
39
39
|
import { candidateFingerprintFailedToStepFailure, configuredAgentOutcomesUnavailableToStepFailure, restorationIncompleteToStepFailure, workspaceStateReadFailureToStepFailure, workspaceTransactionFailureToStepFailure, } from "./step-failure-conversions.js";
|
|
40
40
|
/** Publish a phase transition to the lifecycle stream and the journal. */
|
|
@@ -107,19 +107,15 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
|
|
|
107
107
|
const fsLayer = Layer.mergeAll(Layer.succeed(FileSystem.FileSystem, fs), Layer.succeed(Path.Path, path));
|
|
108
108
|
const mode = options.execution.request.mode;
|
|
109
109
|
yield* publishPhaseStarted("planning");
|
|
110
|
-
// Step 1: Lockfile reconciliation
|
|
111
|
-
const augmented = yield*
|
|
110
|
+
// Step 1: Lockfile reconciliation, observed as one planning unit.
|
|
111
|
+
const augmented = yield* observeUnit({ id: "lockfile-reconciliation", label: "lockfile reconciliation" }, augmentPlanWithReconciliation(plan, () => ws.getLockfileState()));
|
|
112
112
|
const operations = options.execution.configuredAgentOperations ?? [];
|
|
113
113
|
const configuredAgents = operations.length === 0 ? [] : yield* ws.getConfiguredAgents();
|
|
114
|
-
const configuredMcpServers = operations.some(({ extensionType }) => extensionType === "mcp-server")
|
|
115
|
-
? yield* ws.getConfiguredMcpServerEntries()
|
|
116
|
-
: {};
|
|
117
114
|
const outcomesProvider = yield* Effect.serviceOption(ConfiguredAgentOutcomesProvider);
|
|
118
115
|
const outcomesOverrideFor = (extensionType) => Option.isSome(outcomesProvider)
|
|
119
116
|
? outcomesProvider.value.byExtensionType[extensionType]
|
|
120
117
|
: undefined;
|
|
121
118
|
const outcomesFor = (operation, state) => {
|
|
122
|
-
const mcpEntry = operation.extensionType === "mcp-server" ? configuredMcpServers[operation.name] : undefined;
|
|
123
119
|
const generic = configuredAgentLifecycleOutcomes({
|
|
124
120
|
type: operation.extensionType,
|
|
125
121
|
name: operation.name,
|
|
@@ -129,11 +125,6 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
|
|
|
129
125
|
targetState: operation.plannedState,
|
|
130
126
|
installed: state === "projected",
|
|
131
127
|
observedAgentIds: state === "projected" ? configuredAgents : [],
|
|
132
|
-
...(mcpEntry === undefined
|
|
133
|
-
? {}
|
|
134
|
-
: {
|
|
135
|
-
applicableAgentIds: configuredAgents.filter((agentId) => isMcpServerApplicableToAgent(mcpEntry, agentId)),
|
|
136
|
-
}),
|
|
137
128
|
});
|
|
138
129
|
const override = outcomesOverrideFor(operation.extensionType);
|
|
139
130
|
if (operation.plannedState === "enabled" && override !== undefined) {
|
|
@@ -350,19 +341,23 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
|
|
|
350
341
|
...job,
|
|
351
342
|
steps: job.steps.map((step) => step.readiness === "error"
|
|
352
343
|
? step
|
|
353
|
-
: {
|
|
344
|
+
: {
|
|
345
|
+
...step,
|
|
346
|
+
run: withWorkspaceClosure(unitIdOf(step))(step.run).pipe(Effect.provideService(CurrentOperationUnit, { unitId: unitIdOf(step) })),
|
|
347
|
+
}),
|
|
354
348
|
})),
|
|
355
349
|
};
|
|
356
350
|
return yield* applyPlan(closureScopedPlan, {
|
|
357
351
|
// The started fact is journaled before the run's first effect, so an
|
|
358
352
|
// interruption mid-run reports the unit in flight, never not attempted.
|
|
359
|
-
onStepStarted: (step) => appendStartedUnit(unitIdOf(step)).pipe(Effect.andThen(
|
|
353
|
+
onStepStarted: (step) => appendStartedUnit(unitIdOf(step)).pipe(Effect.andThen(publishOperationEvent((seq, atMs) => ({
|
|
360
354
|
_tag: "UnitStarted",
|
|
355
|
+
seq,
|
|
356
|
+
atMs,
|
|
361
357
|
unitId: unitIdOf(step),
|
|
362
358
|
label: step.label,
|
|
363
359
|
index: startedUnits++,
|
|
364
360
|
total: totalUnits,
|
|
365
|
-
atNanos,
|
|
366
361
|
})))),
|
|
367
362
|
// Settlement runs before the next interruptible boundary: the journal
|
|
368
363
|
// fact and the closure's snapshot disposition are recorded together —
|
|
@@ -370,14 +365,15 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
|
|
|
370
365
|
// itself, and later ready closures continue.
|
|
371
366
|
onStepCompleted: (step) => appendResolvedUnit(step).pipe(Effect.andThen(step.result.result === "error"
|
|
372
367
|
? rollbackWorkspaceClosure(unitIdOf(step))
|
|
373
|
-
: settleWorkspaceClosure(unitIdOf(step))), Effect.andThen(
|
|
368
|
+
: settleWorkspaceClosure(unitIdOf(step))), Effect.andThen(publishOperationEvent((seq, atMs) => ({
|
|
374
369
|
_tag: "UnitResolved",
|
|
370
|
+
seq,
|
|
371
|
+
atMs,
|
|
375
372
|
unitId: unitIdOf(step),
|
|
376
373
|
label: step.label,
|
|
377
374
|
state: resolvedUnitState(step),
|
|
378
375
|
index: resolvedUnits++,
|
|
379
376
|
total: totalUnits,
|
|
380
|
-
atNanos,
|
|
381
377
|
})))),
|
|
382
378
|
});
|
|
383
379
|
});
|
|
@@ -504,19 +500,32 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
|
|
|
504
500
|
? failure
|
|
505
501
|
: { error: workspaceTransactionFailureToStepFailure(failure) };
|
|
506
502
|
}));
|
|
503
|
+
const transitionSubject = "workspace-transition";
|
|
507
504
|
const applyResult = yield* Effect.scoped(Effect.gen(function* () {
|
|
505
|
+
const waited = yield* Ref.make(false);
|
|
508
506
|
const contention = yield* ws.acquireTransition({
|
|
509
507
|
command: applyExecution.approvalRecovery.command.join(" "),
|
|
510
508
|
candidateId: candidate.id,
|
|
511
|
-
|
|
509
|
+
// Contention is a first-class lifecycle fact: observers render the
|
|
510
|
+
// wait and its holder; nothing here decides how it is worded.
|
|
511
|
+
onWaiting: (holder) => Ref.set(waited, true).pipe(Effect.andThen(publishWaiting({
|
|
512
|
+
blockingClass: "resource-conflict",
|
|
513
|
+
subject: transitionSubject,
|
|
514
|
+
detail: Option.match(holder, {
|
|
515
|
+
onNone: () => "another operation",
|
|
516
|
+
onSome: (value) => `${value.command} (pid ${String(value.pid)})`,
|
|
517
|
+
}),
|
|
518
|
+
}))),
|
|
512
519
|
});
|
|
520
|
+
if (yield* Ref.get(waited))
|
|
521
|
+
yield* publishWaitEnded(transitionSubject);
|
|
513
522
|
if (Option.isSome(contention)) {
|
|
514
523
|
return { type: "contention", contention: contention.value };
|
|
515
524
|
}
|
|
516
|
-
return yield*
|
|
525
|
+
return yield* guardedApply.pipe(Effect.match({
|
|
517
526
|
onFailure: (error) => ({ type: "failure", error }),
|
|
518
527
|
onSuccess: (value) => ({ type: "success", value }),
|
|
519
|
-
}))
|
|
528
|
+
}));
|
|
520
529
|
}));
|
|
521
530
|
if (applyResult.type === "contention") {
|
|
522
531
|
const reference = Option.match(applyResult.contention.holder, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentxm/workspace-operations",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.6",
|
|
4
4
|
"description": "AXM workspace operations kernel: plans, execution candidates, operation resolutions, and workspace transactions for the axm CLI. Unstable and unsupported — use the axm.sh CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-MIT",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"effect": "4.0.0-rc.112",
|
|
50
50
|
"proper-lockfile": "^4.1.2",
|
|
51
|
-
"@agentxm/extension-model": "^0.28.
|
|
52
|
-
"@agentxm/
|
|
53
|
-
"@agentxm/
|
|
51
|
+
"@agentxm/extension-model": "^0.28.6",
|
|
52
|
+
"@agentxm/workspace-state": "^0.28.6",
|
|
53
|
+
"@agentxm/registry-protocol": "^0.28.6"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@effect/platform-node": "4.0.0-rc.112",
|