@effect-agent/workflow 0.1.0-beta.46

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.
@@ -0,0 +1,614 @@
1
+ import { AgentOutputError } from "@effect-agent/core/AgentError";
2
+ import { ThreadId } from "@effect-agent/core/Identifiers";
3
+ import { type BindingUnavailable } from "@effect-agent/thread/AgentRegistration";
4
+ import { digestJson } from "@effect-agent/thread/Digest";
5
+ import {
6
+ DurableAgentRuntime,
7
+ DurableRuntimeConfig,
8
+ Receipt,
9
+ type DurableAwaitFailure,
10
+ type DurableSubmitAgent,
11
+ type DurableSubmitFailure,
12
+ type DurableSubmitOptions,
13
+ } from "@effect-agent/thread/DurableAgentRuntime";
14
+ import { DeploymentId } from "@effect-agent/thread/Records";
15
+ import {
16
+ IdempotencyKey,
17
+ Principal,
18
+ SubmissionLedger,
19
+ SubmissionLookupById,
20
+ } from "@effect-agent/thread/SubmissionLedger";
21
+ import {
22
+ Cause,
23
+ Context,
24
+ Crypto,
25
+ Effect,
26
+ Exit,
27
+ Layer,
28
+ Option,
29
+ RcMap,
30
+ Ref,
31
+ Result,
32
+ Schema,
33
+ Semaphore,
34
+ Stream,
35
+ } from "effect";
36
+ import { DurableDeferred, Workflow, WorkflowEngine } from "effect/unstable/workflow";
37
+
38
+ import { workflowCompletion } from "./internal/completion.ts";
39
+ import {
40
+ WorkflowDispatchError,
41
+ WorkflowDispatchFailpoint,
42
+ WorkflowDispatchIntent,
43
+ WorkflowDispatchScan,
44
+ WorkflowDispatchStore,
45
+ WorkflowRepairReport,
46
+ WorkflowRepairTrigger,
47
+ WorkflowSettlementReference,
48
+ WorkflowSubmission,
49
+ } from "./WorkflowDispatch.ts";
50
+ import {
51
+ WorkflowExecutionFailure,
52
+ type WorkflowAgent,
53
+ type WorkflowExecuteOptions,
54
+ } from "./WorkflowExecution.ts";
55
+
56
+ const WorkflowHostConfig = Schema.Struct({
57
+ deploymentId: DeploymentId,
58
+ principal: Principal,
59
+ workflowName: Schema.NonEmptyString,
60
+ executionConcurrency: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 64 })),
61
+ repairBatchSize: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1000 })),
62
+ dispatchTimeoutMillis: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 300_000 })),
63
+ });
64
+
65
+ export class WorkflowHostConfigError extends Schema.TaggedError<WorkflowHostConfigError>()(
66
+ "WorkflowHostConfigError",
67
+ { message: Schema.String },
68
+ ) {}
69
+
70
+ export class WorkflowAdmissionClosed extends Schema.TaggedError<WorkflowAdmissionClosed>()(
71
+ "WorkflowAdmissionClosed",
72
+ { message: Schema.String },
73
+ ) {}
74
+
75
+ export interface WorkflowAgentHostOptions {
76
+ readonly deploymentId: string;
77
+ /** Application-owned identity for submissions from AgentWorkflow.execute. */
78
+ readonly principal: string;
79
+ /** Stable versioned name prefix. The native name also includes deploymentId. */
80
+ readonly workflowName?: string;
81
+ /** Concurrent Attempts within this host Layer instance, not a fleet-wide limit. Default 1. */
82
+ readonly executionConcurrency?: number;
83
+ /** Maximum entries per scan per repair invocation. Default 32. */
84
+ readonly repairBatchSize?: number;
85
+ /** Bound each dispatch or scan, including a retrying native engine. Default 10000 ms. */
86
+ readonly dispatchTimeoutMillis?: number;
87
+ }
88
+
89
+ export type WorkflowRepairFailure = WorkflowDispatchError | DurableAwaitFailure;
90
+
91
+ const nativeOperation = <A, E, R>(operation: string, effect: Effect.Effect<A, E, R>) =>
92
+ effect.pipe(
93
+ Effect.catchDefect((cause) =>
94
+ Effect.fail(
95
+ new WorkflowDispatchError({
96
+ operation,
97
+ message: "Workflow engine operation failed",
98
+ cause,
99
+ }),
100
+ ),
101
+ ),
102
+ );
103
+
104
+ const makeHost = Effect.fn("WorkflowAgentHost.make")(function* (options: WorkflowAgentHostOptions) {
105
+ const config = yield* Schema.decodeUnknownEffect(WorkflowHostConfig)({
106
+ deploymentId: options.deploymentId,
107
+ principal: options.principal,
108
+ workflowName: options.workflowName ?? "effect-agent/Submission/v1",
109
+ executionConcurrency: options.executionConcurrency ?? 1,
110
+ repairBatchSize: options.repairBatchSize ?? 32,
111
+ dispatchTimeoutMillis: options.dispatchTimeoutMillis ?? 10_000,
112
+ }).pipe(Effect.mapError((error) => new WorkflowHostConfigError({ message: error.message })));
113
+
114
+ const runtime = yield* DurableAgentRuntime;
115
+ const crypto = yield* Crypto.Crypto;
116
+ const runtimeConfig = yield* DurableRuntimeConfig;
117
+ const ledger = yield* SubmissionLedger;
118
+ const engine = yield* WorkflowEngine.WorkflowEngine;
119
+ const dispatch = yield* WorkflowDispatchStore;
120
+ const trigger = yield* WorkflowRepairTrigger;
121
+ const failpoint = yield* WorkflowDispatchFailpoint;
122
+
123
+ if (runtimeConfig.deploymentId !== config.deploymentId) {
124
+ return yield* new WorkflowHostConfigError({
125
+ message: "Workflow and durable runtime deploymentId must match",
126
+ });
127
+ }
128
+ const permits = yield* Semaphore.make(config.executionConcurrency);
129
+ const lanes = yield* RcMap.make({ lookup: (_threadId: ThreadId) => Semaphore.make(1) });
130
+ const repairPermit = yield* Semaphore.make(1);
131
+ const admission = yield* Ref.make(true);
132
+ const nativeName = `${config.workflowName}/deployment/${config.deploymentId.length}:${config.deploymentId}`;
133
+
134
+ const workflow = Workflow.make(nativeName, {
135
+ payload: WorkflowSubmission,
136
+ success: WorkflowSettlementReference,
137
+ idempotencyKey: ({ deploymentId, receipt }) =>
138
+ `v1/${deploymentId.length}:${deploymentId}/${receipt.submissionId}`,
139
+ }).annotate(Workflow.SuspendOnFailure, true);
140
+
141
+ const bounded = <A, E, R>(operation: string, effect: Effect.Effect<A, E, R>) =>
142
+ nativeOperation(operation, effect).pipe(
143
+ Effect.timeout(config.dispatchTimeoutMillis),
144
+ Effect.catchTag("TimeoutError", () =>
145
+ Effect.fail(
146
+ WorkflowDispatchError.make({
147
+ operation,
148
+ message: "Workflow dispatch operation timed out; accepted work remains recoverable",
149
+ }),
150
+ ),
151
+ ),
152
+ );
153
+
154
+ const validateReceipt = Effect.fn("WorkflowAgentHost.validateReceipt")(function* (
155
+ receipt: Receipt,
156
+ ) {
157
+ const found = yield* ledger.lookup(
158
+ SubmissionLookupById.make({ submissionId: receipt.submissionId }),
159
+ );
160
+
161
+ if (
162
+ Option.isNone(found) ||
163
+ found.value.deploymentId !== config.deploymentId ||
164
+ found.value.threadId !== receipt.threadId ||
165
+ found.value.receiptId !== receipt.receiptId ||
166
+ found.value.queueSequence !== receipt.queueSequence
167
+ ) {
168
+ return yield* WorkflowDispatchError.make({
169
+ operation: "identity",
170
+ message: "Workflow Receipt does not match its authoritative deployment and Submission",
171
+ });
172
+ }
173
+ });
174
+
175
+ // No Activities: rerun journal recovery directly on every native resume. The upstream
176
+ // failure annotation suspends infrastructure failures and defects instead of settling them.
177
+ yield* engine.register(
178
+ workflow,
179
+ Effect.fn("WorkflowAgentHost.execute")(function* (payload, executionId) {
180
+ yield* Effect.annotateCurrentSpan({
181
+ "workflow.execution.id": executionId,
182
+ "agent.submission.id": payload.receipt.submissionId,
183
+ "agent.thread.id": payload.receipt.threadId,
184
+ });
185
+ if (payload.deploymentId !== config.deploymentId) {
186
+ return yield* Effect.die("Workflow payload belongs to another deployment");
187
+ }
188
+ if (executionId !== (yield* workflow.executionId(payload))) {
189
+ return yield* Effect.die("Workflow execution identity does not match its Submission");
190
+ }
191
+ yield* validateReceipt(payload.receipt).pipe(Effect.orDie);
192
+ const initial = yield* runtime.inspectSubmissionStatus(payload.receipt).pipe(Effect.orDie);
193
+
194
+ if (initial._tag === "settled") {
195
+ return new WorkflowSettlementReference({
196
+ version: 1,
197
+ submissionId: initial.settlement.submissionId,
198
+ threadId: payload.receipt.threadId,
199
+ settlementId: initial.settlement.settlementId,
200
+ });
201
+ }
202
+
203
+ const status = yield* Effect.scoped(
204
+ Effect.gen(function* () {
205
+ // Serialize recovery and processing for one Thread even when a ledger allows
206
+ // same-producer takeover. Followers wait without consuming a global permit.
207
+ const lane = yield* RcMap.get(lanes, payload.receipt.threadId);
208
+
209
+ return yield* lane.withPermit(
210
+ permits.withPermit(
211
+ Effect.scoped(
212
+ Effect.gen(function* () {
213
+ yield* runtime.recoverSubmission(payload.receipt.submissionId);
214
+ const recovered = yield* runtime.inspectSubmissionStatus(payload.receipt);
215
+
216
+ if (recovered._tag === "settled") return recovered;
217
+ yield* runtime.processThreadHead(payload.receipt.threadId);
218
+
219
+ return yield* runtime.inspectSubmissionStatus(payload.receipt);
220
+ }),
221
+ ),
222
+ ),
223
+ );
224
+ }),
225
+ ).pipe(Effect.orDie);
226
+
227
+ if (status._tag === "settled") {
228
+ return new WorkflowSettlementReference({
229
+ version: 1,
230
+ submissionId: status.settlement.submissionId,
231
+ threadId: payload.receipt.threadId,
232
+ settlementId: status.settlement.settlementId,
233
+ });
234
+ }
235
+ // Release the permit and Attempt Scope before native suspension, whose lifetime may
236
+ // outlast this process. Completion is never inferred from an empty processing result.
237
+ const instance = yield* WorkflowEngine.WorkflowInstance;
238
+
239
+ return yield* Workflow.suspend(instance);
240
+ }),
241
+ );
242
+
243
+ const intentFor = Effect.fn("WorkflowAgentHost.intentFor")(function* (receipt: Receipt) {
244
+ const payload = new WorkflowSubmission({
245
+ version: 1,
246
+ deploymentId: config.deploymentId,
247
+ receipt,
248
+ });
249
+
250
+ const executionId = yield* workflow.executionId(payload);
251
+
252
+ return new WorkflowDispatchIntent({
253
+ ...payload,
254
+ workflowName: nativeName,
255
+ executionId,
256
+ });
257
+ });
258
+
259
+ const dispatchIntent = Effect.fn("WorkflowAgentHost.dispatch")(
260
+ function* (
261
+ requested: WorkflowDispatchIntent,
262
+ ): Effect.fn.Return<boolean, WorkflowRepairFailure> {
263
+ let intent = requested;
264
+ const expected = yield* intentFor(intent.receipt);
265
+
266
+ if (
267
+ intent.executionId !== expected.executionId ||
268
+ intent.deploymentId !== expected.deploymentId ||
269
+ intent.workflowName !== expected.workflowName
270
+ ) {
271
+ return yield* new WorkflowDispatchError({
272
+ operation: "dispatch",
273
+ message: "Dispatch identity does not match the configured Workflow",
274
+ });
275
+ }
276
+ yield* validateReceipt(intent.receipt);
277
+ yield* failpoint.hit("intent:before-persist", intent);
278
+ intent = yield* dispatch.put(intent);
279
+ yield* failpoint.hit("intent:after-persist", intent);
280
+ yield* failpoint.hit("launch:before", intent);
281
+ // Submission lifetime belongs to the durable host. Strip the optional parent
282
+ // instance so upstream child interruption cannot cancel this recovery workflow.
283
+ yield* nativeOperation(
284
+ "execute",
285
+ engine
286
+ .execute(workflow, {
287
+ executionId: intent.executionId,
288
+ payload: new WorkflowSubmission(intent),
289
+ discard: true,
290
+ })
291
+ .pipe(
292
+ Effect.updateContext((context: Context.Context<never>) =>
293
+ Context.omit(WorkflowEngine.WorkflowInstance)(context),
294
+ ),
295
+ ),
296
+ );
297
+ yield* failpoint.hit("launch:after", intent);
298
+ // Resume before suspension may be a no-op. Keep the intent and retry on a later trigger.
299
+ yield* nativeOperation("resume", engine.resume(workflow, intent.executionId));
300
+ yield* failpoint.hit("completion:before-observe", intent);
301
+ const result = yield* nativeOperation("poll", engine.poll(workflow, intent.executionId));
302
+
303
+ yield* failpoint.hit("completion:after-observe", intent);
304
+ if (Option.isNone(result) || result.value._tag !== "Complete") return false;
305
+ if (Exit.isFailure(result.value.exit)) {
306
+ return yield* new WorkflowDispatchError({
307
+ operation: "completion",
308
+ message: "Native Workflow completed without a Settlement reference; intent retained",
309
+ cause: result.value.exit.cause,
310
+ });
311
+ }
312
+ const status = yield* runtime.inspectSubmissionStatus(intent.receipt);
313
+ const reference = result.value.exit.value;
314
+
315
+ if (
316
+ status._tag !== "settled" ||
317
+ reference.version !== 1 ||
318
+ reference.submissionId !== intent.receipt.submissionId ||
319
+ reference.threadId !== intent.receipt.threadId ||
320
+ reference.settlementId !== status.settlement.settlementId
321
+ ) {
322
+ return yield* new WorkflowDispatchError({
323
+ operation: "completion",
324
+ message: "Native completion disagrees with canonical Settlement; intent retained",
325
+ });
326
+ }
327
+ if (intent.completionToken !== undefined) {
328
+ yield* failpoint.hit("completion:before-notify", intent);
329
+ yield* nativeOperation(
330
+ "notify",
331
+ DurableDeferred.succeed(workflowCompletion("settlement"), {
332
+ token: intent.completionToken,
333
+ value: reference,
334
+ }).pipe(Effect.provideService(WorkflowEngine.WorkflowEngine, engine)),
335
+ );
336
+ yield* failpoint.hit("completion:after-notify", intent);
337
+ }
338
+ yield* failpoint.hit("cleanup:before", intent);
339
+ yield* dispatch.remove(intent);
340
+ yield* failpoint.hit("cleanup:after", intent);
341
+
342
+ return true;
343
+ },
344
+ (effect) => bounded("dispatch", effect),
345
+ );
346
+
347
+ let submissionOffset = 0;
348
+ let intentCursor: string | undefined;
349
+
350
+ const repair = repairPermit.withPermit(
351
+ Effect.gen(function* () {
352
+ // Respect adapter ordering, which may differ from JavaScript string comparison.
353
+ // Deletions may defer a row until the next wrap; restarts repeat idempotent work.
354
+ const discovery = yield* bounded(
355
+ "scanSubmissions",
356
+ ledger.scanNonterminal.pipe(
357
+ Stream.filter((row) => row.deploymentId === config.deploymentId),
358
+ Stream.drop(submissionOffset),
359
+ Stream.take(config.repairBatchSize),
360
+ Stream.runCollect,
361
+ ),
362
+ ).pipe(Effect.result);
363
+
364
+ const submissions = Result.isSuccess(discovery) ? discovery.success : [];
365
+
366
+ if (Result.isSuccess(discovery)) {
367
+ submissionOffset =
368
+ submissions.length < config.repairBatchSize ? 0 : submissionOffset + submissions.length;
369
+ }
370
+
371
+ const discovered = yield* Effect.forEach(submissions, (row) =>
372
+ Effect.flatMap(intentFor(new Receipt(row)), dispatchIntent).pipe(Effect.result),
373
+ );
374
+
375
+ const outstanding = yield* bounded(
376
+ "scanIntents",
377
+ dispatch.scan(
378
+ new WorkflowDispatchScan({
379
+ deploymentId: config.deploymentId,
380
+ workflowName: nativeName,
381
+ ...(intentCursor === undefined ? {} : { after: intentCursor }),
382
+ limit: config.repairBatchSize,
383
+ }),
384
+ ),
385
+ ).pipe(Effect.result);
386
+
387
+ const intents = Result.isSuccess(outstanding) ? outstanding.success : [];
388
+
389
+ if (Result.isSuccess(outstanding))
390
+ intentCursor =
391
+ intents.length < config.repairBatchSize
392
+ ? undefined
393
+ : intents[intents.length - 1]?.executionId;
394
+
395
+ const inspected = yield* Effect.forEach(intents, (intent) =>
396
+ dispatchIntent(intent).pipe(Effect.result),
397
+ );
398
+
399
+ const results = [...discovered, ...inspected];
400
+
401
+ if (Result.isFailure(discovery)) return yield* discovery.failure;
402
+ if (Result.isFailure(outstanding)) return yield* outstanding.failure;
403
+ const failure = results.find(Result.isFailure);
404
+
405
+ if (failure !== undefined) return yield* Effect.fail(failure.failure);
406
+
407
+ return new WorkflowRepairReport({
408
+ discovered: submissions.length,
409
+ inspected: intents.length,
410
+ completed: results.filter((result) => Result.isSuccess(result) && result.success).length,
411
+ });
412
+ }),
413
+ );
414
+
415
+ yield* trigger.register(
416
+ repair.pipe(
417
+ Effect.asVoid,
418
+ Effect.catchCause((cause) =>
419
+ Cause.hasInterruptsOnly(cause)
420
+ ? Effect.interrupt
421
+ : Effect.logError("Workflow dispatch repair failed; durable obligations remain", cause),
422
+ ),
423
+ ),
424
+ );
425
+ yield* Effect.addFinalizer(() => Ref.set(admission, false));
426
+
427
+ const submit = Effect.fn("WorkflowAgentHost.submit")(function* <InputSchema extends Schema.Top>(
428
+ agent: DurableSubmitAgent<InputSchema>,
429
+ input: InputSchema["Type"],
430
+ options: DurableSubmitOptions,
431
+ ): Effect.fn.Return<
432
+ Receipt,
433
+ DurableSubmitFailure | WorkflowRepairFailure | WorkflowAdmissionClosed,
434
+ InputSchema["EncodingServices"]
435
+ > {
436
+ if (!(yield* Ref.get(admission))) {
437
+ return yield* new WorkflowAdmissionClosed({ message: "The Workflow host is shutting down" });
438
+ }
439
+ const receipt = yield* runtime.submit(agent, input, options);
440
+
441
+ yield* dispatchIntent(yield* intentFor(receipt));
442
+
443
+ return receipt;
444
+ });
445
+
446
+ const execute = Effect.fn("AgentWorkflow.execute")(function* <
447
+ Input extends Schema.Top,
448
+ Output extends Schema.Top,
449
+ >(agent: WorkflowAgent<Input, Output>, input: Input["Type"], options: WorkflowExecuteOptions) {
450
+ const parentEngine = yield* WorkflowEngine.WorkflowEngine;
451
+
452
+ if (parentEngine !== engine) {
453
+ return yield* new WorkflowExecutionFailure({
454
+ reason: "engine-mismatch",
455
+ message: "Agent host and parent workflow must share one WorkflowEngine Layer",
456
+ });
457
+ }
458
+
459
+ const name = yield* Schema.decodeUnknownEffect(Schema.NonEmptyString)(options.name).pipe(
460
+ Effect.mapError(
461
+ () =>
462
+ new WorkflowExecutionFailure({
463
+ reason: "invalid-name",
464
+ message: "Workflow agent steps need a nonempty stable name",
465
+ }),
466
+ ),
467
+ );
468
+
469
+ const completion = workflowCompletion(`effect-agent/${name}`);
470
+ const completionToken = yield* DurableDeferred.token(completion);
471
+
472
+ // Agent identity and input deliberately do not participate: changing either on replay
473
+ // must conflict with the original admission rather than launch new external work.
474
+ const identity = yield* digestJson([
475
+ config.deploymentId,
476
+ config.principal,
477
+ completionToken,
478
+ ]).pipe(Effect.provideService(Crypto.Crypto, crypto));
479
+
480
+ if (!(yield* Ref.get(admission))) {
481
+ return yield* new WorkflowAdmissionClosed({ message: "The Workflow host is shutting down" });
482
+ }
483
+
484
+ const receipt = yield* runtime.submitRegistered({ definition: agent }, input, {
485
+ threadId: ThreadId.make(`workflow/${identity}`),
486
+ idempotencyKey: IdempotencyKey.make(identity),
487
+ principal: config.principal,
488
+ });
489
+
490
+ yield* Effect.annotateCurrentSpan({
491
+ "agent.submission.id": receipt.submissionId,
492
+ "agent.workflow.step": name,
493
+ });
494
+ yield* runtime.submissionStatus(receipt);
495
+ yield* dispatchIntent(
496
+ new WorkflowDispatchIntent({ ...(yield* intentFor(receipt)), completionToken }),
497
+ );
498
+
499
+ // The deferred stores only a reference. Always reauthorize and read canonical data,
500
+ // including on replay after the workflow engine has cached the notification.
501
+ const reference = yield* DurableDeferred.await(completion);
502
+
503
+ const record = yield* runtime.settlementRecord(receipt);
504
+
505
+ if (
506
+ reference.submissionId !== receipt.submissionId ||
507
+ reference.threadId !== receipt.threadId ||
508
+ reference.settlementId !== record.settlementId
509
+ ) {
510
+ return yield* new WorkflowDispatchError({
511
+ operation: "result",
512
+ message: "Workflow completion disagrees with canonical Settlement",
513
+ });
514
+ }
515
+ if (record.outcome === "failed") {
516
+ return yield* new WorkflowExecutionFailure({
517
+ reason: "failed",
518
+ message: "Agent submission failed",
519
+ receipt,
520
+ failure: record.result,
521
+ });
522
+ }
523
+ if (record.outcome === "aborted") {
524
+ return yield* new WorkflowExecutionFailure({
525
+ reason: "aborted",
526
+ message: "Agent submission was aborted",
527
+ receipt,
528
+ });
529
+ }
530
+ if (record.result === undefined) {
531
+ return yield* new WorkflowExecutionFailure({
532
+ reason: "missing-output",
533
+ message: "Completed submission has no independent output",
534
+ receipt,
535
+ });
536
+ }
537
+
538
+ return yield* Schema.decodeUnknownEffect(agent.output)(record.result).pipe(
539
+ Effect.mapError(
540
+ (cause) =>
541
+ new AgentOutputError({
542
+ message: `Cannot decode canonical agent output: ${cause.message}`,
543
+ }),
544
+ ),
545
+ );
546
+ });
547
+
548
+ return WorkflowAgentHost.of({
549
+ execute,
550
+ submit,
551
+ awaitSettlement: runtime.awaitSettlement,
552
+ observe: runtime.observe,
553
+ abort: runtime.abort,
554
+ resolveApproval: runtime.resolveApproval,
555
+ resolveUnknown: runtime.resolveUnknown,
556
+ submissionStatus: runtime.submissionStatus,
557
+ repair,
558
+ executionId: Effect.fn("WorkflowAgentHost.executionId")((receipt: Receipt) =>
559
+ intentFor(receipt).pipe(Effect.map((intent) => intent.executionId)),
560
+ ),
561
+ });
562
+ });
563
+
564
+ /**
565
+ * Optional engine-independent host. Supply the upstream WorkflowEngine, the existing
566
+ * durable runtime and ledger, durable dispatch storage, and a host-owned repair trigger.
567
+ * Do not also start the ordinary Node worker loop. Waiter interruption only detaches;
568
+ * abort and resolutions retain the runtime's authorization and durable intent protocol.
569
+ */
570
+ export class WorkflowAgentHost extends Context.Service<
571
+ WorkflowAgentHost,
572
+ {
573
+ readonly execute: <Input extends Schema.Top, Output extends Schema.Top>(
574
+ agent: WorkflowAgent<Input, Output>,
575
+ input: Input["Type"],
576
+ options: WorkflowExecuteOptions,
577
+ ) => Effect.Effect<
578
+ Output["Type"],
579
+ | DurableSubmitFailure
580
+ | BindingUnavailable
581
+ | DurableAwaitFailure
582
+ | WorkflowDispatchError
583
+ | WorkflowAdmissionClosed
584
+ | WorkflowExecutionFailure
585
+ | AgentOutputError,
586
+ | Input["EncodingServices"]
587
+ | Output["DecodingServices"]
588
+ | WorkflowEngine.WorkflowInstance
589
+ | WorkflowEngine.WorkflowEngine
590
+ >;
591
+ readonly submit: <InputSchema extends Schema.Top>(
592
+ agent: DurableSubmitAgent<InputSchema>,
593
+ input: InputSchema["Type"],
594
+ options: DurableSubmitOptions,
595
+ ) => Effect.Effect<
596
+ Receipt,
597
+ DurableSubmitFailure | WorkflowRepairFailure | WorkflowAdmissionClosed,
598
+ InputSchema["EncodingServices"]
599
+ >;
600
+ readonly awaitSettlement: DurableAgentRuntime["Service"]["awaitSettlement"];
601
+ readonly observe: DurableAgentRuntime["Service"]["observe"];
602
+ readonly abort: DurableAgentRuntime["Service"]["abort"];
603
+ readonly resolveApproval: DurableAgentRuntime["Service"]["resolveApproval"];
604
+ readonly resolveUnknown: DurableAgentRuntime["Service"]["resolveUnknown"];
605
+ readonly submissionStatus: DurableAgentRuntime["Service"]["submissionStatus"];
606
+ readonly repair: Effect.Effect<WorkflowRepairReport, WorkflowRepairFailure>;
607
+ readonly executionId: (receipt: Receipt) => Effect.Effect<string>;
608
+ }
609
+ >()("@effect-agent/workflow/WorkflowAgentHost") {
610
+ /** Drive the injected runtime, whose Layer owns executable registrations and their services. */
611
+ static layer(options: WorkflowAgentHostOptions) {
612
+ return Layer.effect(WorkflowAgentHost)(makeHost(options));
613
+ }
614
+ }