@yolk-sdk/harness 0.1.0-canary.77

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,159 @@
1
+ import { Effect, Stream } from "effect";
2
+ import { addAgentUsage, zeroAgentUsage } from "@yolk-sdk/agent/protocol";
3
+ import { LLMError, collectModelTurnAttempt, runModelTurn, runToolBatch } from "@yolk-sdk/agent/loop";
4
+ import { applyOverflowCompaction, isOverflowCompactionAttemptCount } from "@yolk-sdk/agent/compaction";
5
+ //#region src/outcome.ts
6
+ const completedOutcome = (result) => ({
7
+ _tag: "Completed",
8
+ needsContinuation: result.stopReason === "tool_use",
9
+ assistantMessage: result.assistantMessage,
10
+ toolCalls: result.toolCalls,
11
+ usage: result.usage,
12
+ stopReason: result.stopReason
13
+ });
14
+ const modelTurnResult = (result) => ({
15
+ assistantMessage: result.assistantMessage,
16
+ toolCalls: result.toolCalls,
17
+ usage: result.usage,
18
+ stopReason: result.stopReason
19
+ });
20
+ const invalidOverflowCompactionAttemptError = () => new LLMError({
21
+ cause: "validation_error",
22
+ message: "overflowCompactionAttempt must be a finite integer >= 0",
23
+ retryable: false
24
+ });
25
+ const isAgentLoopError = (error) => {
26
+ if (typeof error !== "object" || error === null || !("_tag" in error)) return false;
27
+ const tag = error._tag;
28
+ return tag === "LLMError" || tag === "FauxExhaustedError" || tag === "ToolError" || tag === "ContextTransformError" || tag === "AbortError";
29
+ };
30
+ const isOverflow = (error) => (error._tag === "LLMError" || error._tag === "ContextTransformError") && error.cause === "context_overflow";
31
+ const isMissingDone = (error) => error._tag === "LLMError" && error.responseIssue === "missing_done";
32
+ const isRetryableLlm = (error) => error._tag === "LLMError" && error.retryable;
33
+ const classifyModelTurnFailure = (input) => {
34
+ if (isOverflow(input.error)) {
35
+ if (input.compact === void 0) return Effect.fail(input.error);
36
+ return applyOverflowCompaction({
37
+ compact: input.compact,
38
+ messages: input.messages,
39
+ attempt: input.overflowCompactionAttempt,
40
+ outputStarted: input.outputStarted
41
+ }).pipe(Effect.flatMap((result) => result._tag === "Compacted" ? Effect.succeed({
42
+ _tag: "Compacted",
43
+ messages: result.messages,
44
+ overflowCompactionAttempt: input.overflowCompactionAttempt + 1
45
+ }) : Effect.fail(input.error)));
46
+ }
47
+ if (isMissingDone(input.error) && !input.outputStarted) return Effect.succeed({
48
+ _tag: "RecoverFull",
49
+ error: input.error
50
+ });
51
+ if (isMissingDone(input.error) && input.outputStarted) return Effect.succeed({
52
+ _tag: "Continue",
53
+ error: input.error,
54
+ ...modelTurnResult(input.collected)
55
+ });
56
+ if (isRetryableLlm(input.error) && !input.outputStarted) {
57
+ if (input.error.cause === "invalid_response") return Effect.succeed({
58
+ _tag: "RecoverFull",
59
+ error: input.error
60
+ });
61
+ return Effect.succeed({
62
+ _tag: "Retry",
63
+ error: input.error
64
+ });
65
+ }
66
+ if (isRetryableLlm(input.error) && input.outputStarted) return Effect.succeed({
67
+ _tag: "Continue",
68
+ error: input.error,
69
+ ...modelTurnResult(input.collected)
70
+ });
71
+ return Effect.fail(input.error);
72
+ };
73
+ const attemptModelTurn = (config, options) => Effect.gen(function* () {
74
+ const overflowCompactionAttempt = options?.overflowCompactionAttempt ?? 0;
75
+ if (!isOverflowCompactionAttemptCount(overflowCompactionAttempt)) return yield* Effect.fail(invalidOverflowCompactionAttemptError());
76
+ const outcome = yield* collectModelTurnAttempt(runModelTurn(config), {
77
+ onEvent: options?.onEvent,
78
+ initialUsage: options?.initialUsage
79
+ });
80
+ switch (outcome._tag) {
81
+ case "Collected": return completedOutcome(outcome.collection);
82
+ case "SinkFailed": return yield* Effect.fail(outcome.error);
83
+ case "StreamFailed":
84
+ if (!isAgentLoopError(outcome.error)) return yield* Effect.fail(outcome.error);
85
+ return yield* classifyModelTurnFailure({
86
+ error: outcome.error,
87
+ collected: {
88
+ assistantMessage: outcome.collection.assistantMessage ?? outcome.collection.partialAssistantMessage,
89
+ toolCalls: outcome.collection.toolCalls,
90
+ usage: outcome.collection.usage,
91
+ stopReason: outcome.collection.stopReason
92
+ },
93
+ outputStarted: outcome.collection.outputStarted,
94
+ messages: config.messages,
95
+ overflowCompactionAttempt,
96
+ compact: options?.compact
97
+ });
98
+ }
99
+ });
100
+ const attemptToolBatch = (config, options) => {
101
+ const onEvent = options?.onEvent;
102
+ return runToolBatch(config).pipe(Stream.runFoldEffect(() => ({
103
+ requests: [],
104
+ usage: config.usage ?? zeroAgentUsage,
105
+ toolCalls: []
106
+ }), (acc, event) => {
107
+ const next = event._tag === "AgentAwaitingInput" ? {
108
+ ...acc,
109
+ requests: event.requests,
110
+ usage: event.usage
111
+ } : event._tag === "UsageUpdate" ? {
112
+ ...acc,
113
+ usage: addAgentUsage(acc.usage, event.usage)
114
+ } : event._tag === "ToolExecutionCompleted" || event._tag === "ToolExecutionAccepted" ? {
115
+ ...acc,
116
+ toolCalls: [...acc.toolCalls, event.call]
117
+ } : acc;
118
+ return onEvent === void 0 ? Effect.succeed(next) : onEvent(event).pipe(Effect.as(next));
119
+ }), Effect.map((result) => {
120
+ if (result.requests.length === 0) {
121
+ const needsContinuation = result.toolCalls.length > 0;
122
+ return {
123
+ _tag: "Completed",
124
+ needsContinuation,
125
+ assistantMessage: void 0,
126
+ toolCalls: result.toolCalls,
127
+ usage: result.usage,
128
+ stopReason: needsContinuation ? "tool_use" : "stop"
129
+ };
130
+ }
131
+ return {
132
+ _tag: "AwaitingInput",
133
+ requests: result.requests,
134
+ usage: result.usage
135
+ };
136
+ }));
137
+ };
138
+ const hitlResponseMatchesRequest = (response, request) => {
139
+ switch (response._tag) {
140
+ case "ToolApprovalResponse": return request._tag === "ToolApprovalRequest" && response.requestId === request.requestId && response.toolCallId === request.toolCallId;
141
+ case "QuestionResponse": return request._tag === "QuestionRequest" && response.requestId === request.requestId && response.toolCallId === request.toolCallId;
142
+ }
143
+ };
144
+ const matchHitlResponse = (pending, response) => {
145
+ const matched = pending.find((request) => hitlResponseMatchesRequest(response, request));
146
+ return matched === void 0 ? { _tag: "Mismatch" } : {
147
+ _tag: "Match",
148
+ requestId: matched.requestId
149
+ };
150
+ };
151
+ const resumeHitlIfMatched = (input) => {
152
+ const matched = matchHitlResponse(input.pending, input.response);
153
+ if (matched._tag === "Mismatch") return Effect.succeed(matched);
154
+ return input.resume(matched.requestId);
155
+ };
156
+ //#endregion
157
+ export { attemptModelTurn, attemptToolBatch, matchHitlResponse, resumeHitlIfMatched };
158
+
159
+ //# sourceMappingURL=outcome.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outcome.mjs","names":[],"sources":["../src/outcome.ts"],"sourcesContent":["import { Effect, Stream } from 'effect'\nimport {\n addAgentUsage,\n zeroAgentUsage,\n type AgentEvent,\n type AgentMessage,\n type AgentUsage,\n type HitlRequest,\n type HitlResponse\n} from '@yolk-sdk/agent/protocol'\nimport {\n collectModelTurnAttempt,\n LLMError,\n runModelTurn,\n runToolBatch,\n type AgentLoopError,\n type ContextTransformer,\n type LLMProvider,\n type LoopConfig,\n type ModelTurnConfig,\n type ModelTurnResult,\n type ToolBatchConfig,\n type ToolExecutor\n} from '@yolk-sdk/agent/loop'\nimport {\n applyOverflowCompaction,\n isOverflowCompactionAttemptCount\n} from '@yolk-sdk/agent/compaction'\n\nexport type OverflowCompactionResult =\n | {\n readonly _tag: 'Compacted'\n readonly messages: ReadonlyArray<AgentMessage>\n }\n | {\n readonly _tag: 'Skipped'\n }\n\nexport type CompletedTurn = {\n readonly _tag: 'Completed'\n readonly needsContinuation: boolean\n} & ModelTurnResult\n\nexport type ModelTurnOutcome =\n | CompletedTurn\n | { readonly _tag: 'Retry'; readonly error: AgentLoopError }\n | ({ readonly _tag: 'Continue'; readonly error: AgentLoopError } & ModelTurnResult)\n | { readonly _tag: 'RecoverFull'; readonly error: AgentLoopError }\n | {\n readonly _tag: 'Compacted'\n readonly messages: ReadonlyArray<AgentMessage>\n readonly overflowCompactionAttempt: number\n }\n\nexport type ToolBatchOutcome =\n | CompletedTurn\n | {\n readonly _tag: 'AwaitingInput'\n readonly requests: ReadonlyArray<HitlRequest>\n readonly usage: AgentUsage\n }\n\nexport type StepOutcome = ModelTurnOutcome | ToolBatchOutcome\n\nconst completedOutcome = (result: ModelTurnResult): CompletedTurn => ({\n _tag: 'Completed',\n needsContinuation: result.stopReason === 'tool_use',\n assistantMessage: result.assistantMessage,\n toolCalls: result.toolCalls,\n usage: result.usage,\n stopReason: result.stopReason\n})\n\nconst modelTurnResult = (result: ModelTurnResult): ModelTurnResult => ({\n assistantMessage: result.assistantMessage,\n toolCalls: result.toolCalls,\n usage: result.usage,\n stopReason: result.stopReason\n})\n\nconst invalidOverflowCompactionAttemptError = () =>\n new LLMError({\n cause: 'validation_error',\n message: 'overflowCompactionAttempt must be a finite integer >= 0',\n retryable: false\n })\n\nconst isAgentLoopError = (error: unknown): error is AgentLoopError => {\n if (typeof error !== 'object' || error === null || !('_tag' in error)) {\n return false\n }\n const tag = error._tag\n return (\n tag === 'LLMError' ||\n tag === 'FauxExhaustedError' ||\n tag === 'ToolError' ||\n tag === 'ContextTransformError' ||\n tag === 'AbortError'\n )\n}\n\nconst isOverflow = (error: AgentLoopError) =>\n (error._tag === 'LLMError' || error._tag === 'ContextTransformError') &&\n error.cause === 'context_overflow'\n\nconst isMissingDone = (error: AgentLoopError) =>\n error._tag === 'LLMError' && error.responseIssue === 'missing_done'\n\nconst isRetryableLlm = (\n error: AgentLoopError\n): error is Extract<AgentLoopError, { _tag: 'LLMError' }> =>\n error._tag === 'LLMError' && error.retryable\n\nconst classifyModelTurnFailure = <E2, R2>(input: {\n readonly error: AgentLoopError\n readonly collected: ModelTurnResult\n readonly outputStarted: boolean\n readonly messages: ReadonlyArray<AgentMessage>\n readonly overflowCompactionAttempt: number\n readonly compact?: (\n messages: ReadonlyArray<AgentMessage>\n ) => Effect.Effect<OverflowCompactionResult, E2, R2>\n}): Effect.Effect<ModelTurnOutcome, AgentLoopError | E2, R2> => {\n if (isOverflow(input.error)) {\n if (input.compact === undefined) {\n return Effect.fail(input.error)\n }\n\n return applyOverflowCompaction({\n compact: input.compact,\n messages: input.messages,\n attempt: input.overflowCompactionAttempt,\n outputStarted: input.outputStarted\n }).pipe(\n Effect.flatMap(result =>\n result._tag === 'Compacted'\n ? Effect.succeed({\n _tag: 'Compacted' as const,\n messages: result.messages,\n overflowCompactionAttempt: input.overflowCompactionAttempt + 1\n })\n : Effect.fail(input.error)\n )\n )\n }\n\n if (isMissingDone(input.error) && !input.outputStarted) {\n return Effect.succeed({ _tag: 'RecoverFull', error: input.error })\n }\n\n if (isMissingDone(input.error) && input.outputStarted) {\n return Effect.succeed({\n _tag: 'Continue',\n error: input.error,\n ...modelTurnResult(input.collected)\n })\n }\n\n if (isRetryableLlm(input.error) && !input.outputStarted) {\n if (input.error.cause === 'invalid_response') {\n return Effect.succeed({ _tag: 'RecoverFull', error: input.error })\n }\n return Effect.succeed({ _tag: 'Retry', error: input.error })\n }\n\n if (isRetryableLlm(input.error) && input.outputStarted) {\n return Effect.succeed({\n _tag: 'Continue',\n error: input.error,\n ...modelTurnResult(input.collected)\n })\n }\n\n return Effect.fail(input.error)\n}\n\nexport const attemptModelTurn = <E2 = never, R2 = never>(\n config: ModelTurnConfig,\n options?: {\n readonly onEvent?: (event: AgentEvent) => Effect.Effect<void, E2, R2>\n readonly initialUsage?: AgentUsage\n readonly overflowCompactionAttempt?: number\n readonly compact?: (\n messages: ReadonlyArray<AgentMessage>\n ) => Effect.Effect<OverflowCompactionResult, E2, R2>\n }\n): Effect.Effect<\n ModelTurnOutcome,\n AgentLoopError | E2,\n ContextTransformer | LLMProvider | LoopConfig | R2\n> =>\n Effect.gen(function* () {\n const overflowCompactionAttempt = options?.overflowCompactionAttempt ?? 0\n if (!isOverflowCompactionAttemptCount(overflowCompactionAttempt)) {\n return yield* Effect.fail(invalidOverflowCompactionAttemptError())\n }\n\n const outcome = yield* collectModelTurnAttempt(runModelTurn(config), {\n onEvent: options?.onEvent,\n initialUsage: options?.initialUsage\n })\n\n switch (outcome._tag) {\n case 'Collected':\n return completedOutcome(outcome.collection)\n case 'SinkFailed':\n return yield* Effect.fail(outcome.error)\n case 'StreamFailed':\n if (!isAgentLoopError(outcome.error)) {\n return yield* Effect.fail(outcome.error)\n }\n return yield* classifyModelTurnFailure({\n error: outcome.error,\n collected: {\n assistantMessage:\n outcome.collection.assistantMessage ?? outcome.collection.partialAssistantMessage,\n toolCalls: outcome.collection.toolCalls,\n usage: outcome.collection.usage,\n stopReason: outcome.collection.stopReason\n },\n outputStarted: outcome.collection.outputStarted,\n messages: config.messages,\n overflowCompactionAttempt,\n compact: options?.compact\n })\n }\n })\n\nexport const attemptToolBatch = <E2 = never, R2 = never>(\n config: ToolBatchConfig,\n options?: {\n readonly onEvent?: (event: AgentEvent) => Effect.Effect<void, E2, R2>\n }\n): Effect.Effect<ToolBatchOutcome, AgentLoopError | E2, LoopConfig | ToolExecutor | R2> => {\n const onEvent = options?.onEvent\n\n return runToolBatch(config).pipe(\n Stream.runFoldEffect(\n (): {\n requests: ReadonlyArray<HitlRequest>\n usage: AgentUsage\n toolCalls: ModelTurnResult['toolCalls']\n } => ({\n requests: [],\n usage: config.usage ?? zeroAgentUsage,\n toolCalls: []\n }),\n (acc, event) => {\n const next =\n event._tag === 'AgentAwaitingInput'\n ? { ...acc, requests: event.requests, usage: event.usage }\n : event._tag === 'UsageUpdate'\n ? { ...acc, usage: addAgentUsage(acc.usage, event.usage) }\n : event._tag === 'ToolExecutionCompleted' || event._tag === 'ToolExecutionAccepted'\n ? { ...acc, toolCalls: [...acc.toolCalls, event.call] }\n : acc\n return onEvent === undefined ? Effect.succeed(next) : onEvent(event).pipe(Effect.as(next))\n }\n ),\n Effect.map((result): ToolBatchOutcome => {\n if (result.requests.length === 0) {\n const needsContinuation = result.toolCalls.length > 0\n return {\n _tag: 'Completed',\n needsContinuation,\n assistantMessage: undefined,\n toolCalls: result.toolCalls,\n usage: result.usage,\n stopReason: needsContinuation ? 'tool_use' : 'stop'\n }\n }\n return { _tag: 'AwaitingInput', requests: result.requests, usage: result.usage }\n })\n )\n}\n\nexport type HitlMatch =\n | { readonly _tag: 'Match'; readonly requestId: string }\n | { readonly _tag: 'Mismatch' }\n\nconst hitlResponseMatchesRequest = (response: HitlResponse, request: HitlRequest) => {\n switch (response._tag) {\n case 'ToolApprovalResponse':\n return (\n request._tag === 'ToolApprovalRequest' &&\n response.requestId === request.requestId &&\n response.toolCallId === request.toolCallId\n )\n case 'QuestionResponse':\n return (\n request._tag === 'QuestionRequest' &&\n response.requestId === request.requestId &&\n response.toolCallId === request.toolCallId\n )\n }\n}\n\nexport const matchHitlResponse = (\n pending: ReadonlyArray<HitlRequest>,\n response: HitlResponse\n): HitlMatch => {\n const matched = pending.find(request => hitlResponseMatchesRequest(response, request))\n return matched === undefined\n ? { _tag: 'Mismatch' }\n : { _tag: 'Match', requestId: matched.requestId }\n}\n\nexport const resumeHitlIfMatched = <A, E, R>(input: {\n readonly pending: ReadonlyArray<HitlRequest>\n readonly response: HitlResponse\n readonly resume: (requestId: string) => Effect.Effect<A, E, R>\n}): Effect.Effect<A | { readonly _tag: 'Mismatch' }, E, R> => {\n const matched = matchHitlResponse(input.pending, input.response)\n if (matched._tag === 'Mismatch') return Effect.succeed(matched)\n return input.resume(matched.requestId)\n}\n"],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+
3
+ //#region src/store.d.ts
4
+ type RunStoreShape = {
5
+ readonly claim: (runId: string) => Effect.Effect<void>;
6
+ readonly release: (runId: string) => Effect.Effect<void>;
7
+ readonly isClaimed: (runId: string) => Effect.Effect<boolean>;
8
+ readonly claimed: Effect.Effect<ReadonlySet<string>>;
9
+ readonly incrementResumeCount: (runId: string) => Effect.Effect<number>;
10
+ readonly resumeCount: (runId: string) => Effect.Effect<number>;
11
+ };
12
+ declare const RunStore_base: Context.ServiceClass<RunStore, "@yolk-sdk/harness/RunStore", RunStoreShape>;
13
+ declare class RunStore extends RunStore_base {}
14
+ type DurableRunStoreSnapshot = {
15
+ readonly claimed: ReadonlyArray<string>;
16
+ readonly resumes: ReadonlyArray<readonly [string, number]>;
17
+ };
18
+ declare const makeSnapshotRunStoreLayer: (options: {
19
+ readonly load: Effect.Effect<DurableRunStoreSnapshot | undefined>;
20
+ readonly save: (snapshot: DurableRunStoreSnapshot) => Effect.Effect<void>;
21
+ }) => Layer.Layer<RunStore>;
22
+ declare const makeInMemoryRunStoreLayer: () => Layer.Layer<RunStore>;
23
+ //#endregion
24
+ export { DurableRunStoreSnapshot, RunStore, RunStoreShape, makeInMemoryRunStoreLayer, makeSnapshotRunStoreLayer };
25
+ //# sourceMappingURL=store.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.mts","names":[],"sources":["../src/store.ts"],"mappings":";;;KAEY,aAAA;EAAA,SACD,KAAA,GAAQ,KAAA,aAAkB,MAAA,CAAO,MAAA;EAAA,SACjC,OAAA,GAAU,KAAA,aAAkB,MAAA,CAAO,MAAA;EAAA,SACnC,SAAA,GAAY,KAAA,aAAkB,MAAA,CAAO,MAAA;EAAA,SACrC,OAAA,EAAS,MAAA,CAAO,MAAA,CAAO,WAAA;EAAA,SACvB,oBAAA,GAAuB,KAAA,aAAkB,MAAA,CAAO,MAAA;EAAA,SAChD,WAAA,GAAc,KAAA,aAAkB,MAAA,CAAO,MAAA;AAAA;AAAA,cACjD,aAAA;cAEY,QAAA,SAAiB,aAE7B;AAAA,KAEW,uBAAA;EAAA,SACD,OAAA,EAAS,aAAA;EAAA,SACT,OAAA,EAAS,aAAa;AAAA;AAAA,cAgFpB,yBAAA,GAA6B,OAAA;EAAA,SAC/B,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,uBAAA;EAAA,SACpB,IAAA,GAAO,QAAA,EAAU,uBAAA,KAA4B,MAAA,CAAO,MAAA;AAAA,MAC3D,KAAA,CAAM,KAAA,CAAM,QAAA;AAAA,cAEH,yBAAA,QAAgC,KAAA,CAAM,KAAK,CAAC,QAAA"}
package/dist/store.mjs ADDED
@@ -0,0 +1,86 @@
1
+ import { Context, Effect, Layer, Ref, Semaphore } from "effect";
2
+ //#region src/store.ts
3
+ var RunStore = class extends Context.Service()("@yolk-sdk/harness/RunStore") {};
4
+ const emptySnapshot = {
5
+ claimed: [],
6
+ resumes: []
7
+ };
8
+ const snapshotFromMemory = (memory) => ({
9
+ claimed: [...memory.claimed],
10
+ resumes: [...memory.resumes.entries()]
11
+ });
12
+ const memoryFromSnapshot = (snapshot) => ({
13
+ claimed: new Set(snapshot.claimed),
14
+ resumes: new Map(snapshot.resumes)
15
+ });
16
+ const makeSnapshotRunStore = (options) => Effect.gen(function* () {
17
+ const loaded = yield* options.load;
18
+ const memory = yield* Ref.make(memoryFromSnapshot(loaded ?? emptySnapshot));
19
+ const lock = yield* Semaphore.make(1);
20
+ const mutate = (effect) => Effect.uninterruptibleMask((restore) => restore(lock.take(1)).pipe(Effect.flatMap(() => effect.pipe(Effect.ensuring(lock.release(1))))));
21
+ const commit = (next) => options.save(snapshotFromMemory(next)).pipe(Effect.flatMap(() => Ref.set(memory, next)));
22
+ return RunStore.of({
23
+ claim: (runId) => mutate(Effect.gen(function* () {
24
+ const current = yield* Ref.get(memory);
25
+ yield* commit({
26
+ claimed: new Set(current.claimed).add(runId),
27
+ resumes: current.resumes
28
+ });
29
+ })),
30
+ release: (runId) => mutate(Effect.gen(function* () {
31
+ const current = yield* Ref.get(memory);
32
+ const claimed = new Set(current.claimed);
33
+ claimed.delete(runId);
34
+ const resumes = new Map(current.resumes);
35
+ resumes.delete(runId);
36
+ yield* commit({
37
+ claimed,
38
+ resumes
39
+ });
40
+ })),
41
+ isClaimed: (runId) => Ref.get(memory).pipe(Effect.map((current) => current.claimed.has(runId))),
42
+ claimed: Ref.get(memory).pipe(Effect.map((current) => new Set(current.claimed))),
43
+ incrementResumeCount: (runId) => mutate(Effect.gen(function* () {
44
+ const current = yield* Ref.get(memory);
45
+ const nextCount = (current.resumes.get(runId) ?? 0) + 1;
46
+ const resumes = new Map(current.resumes);
47
+ resumes.set(runId, nextCount);
48
+ yield* commit({
49
+ claimed: current.claimed,
50
+ resumes
51
+ });
52
+ return nextCount;
53
+ })),
54
+ resumeCount: (runId) => Ref.get(memory).pipe(Effect.map((current) => current.resumes.get(runId) ?? 0))
55
+ });
56
+ });
57
+ const makeSnapshotRunStoreLayer = (options) => Layer.effect(RunStore, makeSnapshotRunStore(options));
58
+ const makeInMemoryRunStoreLayer = () => Layer.effect(RunStore, Effect.gen(function* () {
59
+ const claimed = yield* Ref.make(/* @__PURE__ */ new Set());
60
+ const resumes = yield* Ref.make(/* @__PURE__ */ new Map());
61
+ return RunStore.of({
62
+ claim: (runId) => Ref.update(claimed, (current) => new Set(current).add(runId)),
63
+ release: (runId) => Effect.zip(Ref.update(claimed, (current) => {
64
+ const next = new Set(current);
65
+ next.delete(runId);
66
+ return next;
67
+ }), Ref.update(resumes, (current) => {
68
+ const next = new Map(current);
69
+ next.delete(runId);
70
+ return next;
71
+ })).pipe(Effect.asVoid),
72
+ isClaimed: (runId) => Ref.get(claimed).pipe(Effect.map((current) => current.has(runId))),
73
+ claimed: Ref.get(claimed).pipe(Effect.map((current) => new Set(current))),
74
+ incrementResumeCount: (runId) => Ref.modify(resumes, (current) => {
75
+ const nextCount = (current.get(runId) ?? 0) + 1;
76
+ const next = new Map(current);
77
+ next.set(runId, nextCount);
78
+ return [nextCount, next];
79
+ }),
80
+ resumeCount: (runId) => Ref.get(resumes).pipe(Effect.map((current) => current.get(runId) ?? 0))
81
+ });
82
+ }));
83
+ //#endregion
84
+ export { RunStore, makeInMemoryRunStoreLayer, makeSnapshotRunStoreLayer };
85
+
86
+ //# sourceMappingURL=store.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.mjs","names":[],"sources":["../src/store.ts"],"sourcesContent":["import { Context, Effect, Layer, Ref, Semaphore } from 'effect'\n\nexport type RunStoreShape = {\n readonly claim: (runId: string) => Effect.Effect<void>\n readonly release: (runId: string) => Effect.Effect<void>\n readonly isClaimed: (runId: string) => Effect.Effect<boolean>\n readonly claimed: Effect.Effect<ReadonlySet<string>>\n readonly incrementResumeCount: (runId: string) => Effect.Effect<number>\n readonly resumeCount: (runId: string) => Effect.Effect<number>\n}\n\nexport class RunStore extends Context.Service<RunStore, RunStoreShape>()(\n '@yolk-sdk/harness/RunStore'\n) {}\n\nexport type DurableRunStoreSnapshot = {\n readonly claimed: ReadonlyArray<string>\n readonly resumes: ReadonlyArray<readonly [string, number]>\n}\n\ntype MemorySnapshot = {\n readonly claimed: ReadonlySet<string>\n readonly resumes: ReadonlyMap<string, number>\n}\n\nconst emptySnapshot: DurableRunStoreSnapshot = {\n claimed: [],\n resumes: []\n}\n\nconst snapshotFromMemory = (memory: MemorySnapshot): DurableRunStoreSnapshot => ({\n claimed: [...memory.claimed],\n resumes: [...memory.resumes.entries()]\n})\n\nconst memoryFromSnapshot = (snapshot: DurableRunStoreSnapshot): MemorySnapshot => ({\n claimed: new Set(snapshot.claimed),\n resumes: new Map(snapshot.resumes)\n})\n\nconst makeSnapshotRunStore = (options: {\n readonly load: Effect.Effect<DurableRunStoreSnapshot | undefined>\n readonly save: (snapshot: DurableRunStoreSnapshot) => Effect.Effect<void>\n}): Effect.Effect<RunStore['Service']> =>\n Effect.gen(function* () {\n const loaded = yield* options.load\n const memory = yield* Ref.make(memoryFromSnapshot(loaded ?? emptySnapshot))\n const lock = yield* Semaphore.make(1)\n const mutate = <A, E, R>(effect: Effect.Effect<A, E, R>) =>\n Effect.uninterruptibleMask(restore =>\n restore(lock.take(1)).pipe(\n Effect.flatMap(() => effect.pipe(Effect.ensuring(lock.release(1))))\n )\n )\n const commit = (next: MemorySnapshot) =>\n options.save(snapshotFromMemory(next)).pipe(Effect.flatMap(() => Ref.set(memory, next)))\n\n return RunStore.of({\n claim: runId =>\n mutate(\n Effect.gen(function* () {\n const current = yield* Ref.get(memory)\n yield* commit({\n claimed: new Set(current.claimed).add(runId),\n resumes: current.resumes\n })\n })\n ),\n release: runId =>\n mutate(\n Effect.gen(function* () {\n const current = yield* Ref.get(memory)\n const claimed = new Set(current.claimed)\n claimed.delete(runId)\n const resumes = new Map(current.resumes)\n resumes.delete(runId)\n yield* commit({ claimed, resumes })\n })\n ),\n isClaimed: runId => Ref.get(memory).pipe(Effect.map(current => current.claimed.has(runId))),\n claimed: Ref.get(memory).pipe(Effect.map(current => new Set(current.claimed))),\n incrementResumeCount: runId =>\n mutate(\n Effect.gen(function* () {\n const current = yield* Ref.get(memory)\n const nextCount = (current.resumes.get(runId) ?? 0) + 1\n const resumes = new Map(current.resumes)\n resumes.set(runId, nextCount)\n yield* commit({ claimed: current.claimed, resumes })\n return nextCount\n })\n ),\n resumeCount: runId =>\n Ref.get(memory).pipe(Effect.map(current => current.resumes.get(runId) ?? 0))\n })\n })\n\nexport const makeSnapshotRunStoreLayer = (options: {\n readonly load: Effect.Effect<DurableRunStoreSnapshot | undefined>\n readonly save: (snapshot: DurableRunStoreSnapshot) => Effect.Effect<void>\n}): Layer.Layer<RunStore> => Layer.effect(RunStore, makeSnapshotRunStore(options))\n\nexport const makeInMemoryRunStoreLayer = (): Layer.Layer<RunStore> =>\n Layer.effect(\n RunStore,\n Effect.gen(function* () {\n const claimed = yield* Ref.make(new Set<string>())\n const resumes = yield* Ref.make(new Map<string, number>())\n\n return RunStore.of({\n claim: runId => Ref.update(claimed, current => new Set(current).add(runId)),\n release: runId =>\n Effect.zip(\n Ref.update(claimed, current => {\n const next = new Set(current)\n next.delete(runId)\n return next\n }),\n Ref.update(resumes, current => {\n const next = new Map(current)\n next.delete(runId)\n return next\n })\n ).pipe(Effect.asVoid),\n isClaimed: runId => Ref.get(claimed).pipe(Effect.map(current => current.has(runId))),\n claimed: Ref.get(claimed).pipe(Effect.map(current => new Set(current))),\n incrementResumeCount: runId =>\n Ref.modify(resumes, current => {\n const nextCount = (current.get(runId) ?? 0) + 1\n const next = new Map(current)\n next.set(runId, nextCount)\n return [nextCount, next] as const\n }),\n resumeCount: runId => Ref.get(resumes).pipe(Effect.map(current => current.get(runId) ?? 0))\n })\n })\n )\n"],"mappings":";;AAWA,IAAa,WAAb,cAA8B,QAAQ,QAAiC,EACrE,4BACF,EAAE,CAAC;AAYH,MAAM,gBAAyC;CAC7C,SAAS,CAAC;CACV,SAAS,CAAC;AACZ;AAEA,MAAM,sBAAsB,YAAqD;CAC/E,SAAS,CAAC,GAAG,OAAO,OAAO;CAC3B,SAAS,CAAC,GAAG,OAAO,QAAQ,QAAQ,CAAC;AACvC;AAEA,MAAM,sBAAsB,cAAuD;CACjF,SAAS,IAAI,IAAI,SAAS,OAAO;CACjC,SAAS,IAAI,IAAI,SAAS,OAAO;AACnC;AAEA,MAAM,wBAAwB,YAI5B,OAAO,IAAI,aAAa;CACtB,MAAM,SAAS,OAAO,QAAQ;CAC9B,MAAM,SAAS,OAAO,IAAI,KAAK,mBAAmB,UAAU,aAAa,CAAC;CAC1E,MAAM,OAAO,OAAO,UAAU,KAAK,CAAC;CACpC,MAAM,UAAmB,WACvB,OAAO,qBAAoB,YACzB,QAAQ,KAAK,KAAK,CAAC,CAAC,EAAE,KACpB,OAAO,cAAc,OAAO,KAAK,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CACpE,CACF;CACF,MAAM,UAAU,SACd,QAAQ,KAAK,mBAAmB,IAAI,CAAC,EAAE,KAAK,OAAO,cAAc,IAAI,IAAI,QAAQ,IAAI,CAAC,CAAC;CAEzF,OAAO,SAAS,GAAG;EACjB,QAAO,UACL,OACE,OAAO,IAAI,aAAa;GACtB,MAAM,UAAU,OAAO,IAAI,IAAI,MAAM;GACrC,OAAO,OAAO;IACZ,SAAS,IAAI,IAAI,QAAQ,OAAO,EAAE,IAAI,KAAK;IAC3C,SAAS,QAAQ;GACnB,CAAC;EACH,CAAC,CACH;EACF,UAAS,UACP,OACE,OAAO,IAAI,aAAa;GACtB,MAAM,UAAU,OAAO,IAAI,IAAI,MAAM;GACrC,MAAM,UAAU,IAAI,IAAI,QAAQ,OAAO;GACvC,QAAQ,OAAO,KAAK;GACpB,MAAM,UAAU,IAAI,IAAI,QAAQ,OAAO;GACvC,QAAQ,OAAO,KAAK;GACpB,OAAO,OAAO;IAAE;IAAS;GAAQ,CAAC;EACpC,CAAC,CACH;EACF,YAAW,UAAS,IAAI,IAAI,MAAM,EAAE,KAAK,OAAO,KAAI,YAAW,QAAQ,QAAQ,IAAI,KAAK,CAAC,CAAC;EAC1F,SAAS,IAAI,IAAI,MAAM,EAAE,KAAK,OAAO,KAAI,YAAW,IAAI,IAAI,QAAQ,OAAO,CAAC,CAAC;EAC7E,uBAAsB,UACpB,OACE,OAAO,IAAI,aAAa;GACtB,MAAM,UAAU,OAAO,IAAI,IAAI,MAAM;GACrC,MAAM,aAAa,QAAQ,QAAQ,IAAI,KAAK,KAAK,KAAK;GACtD,MAAM,UAAU,IAAI,IAAI,QAAQ,OAAO;GACvC,QAAQ,IAAI,OAAO,SAAS;GAC5B,OAAO,OAAO;IAAE,SAAS,QAAQ;IAAS;GAAQ,CAAC;GACnD,OAAO;EACT,CAAC,CACH;EACF,cAAa,UACX,IAAI,IAAI,MAAM,EAAE,KAAK,OAAO,KAAI,YAAW,QAAQ,QAAQ,IAAI,KAAK,KAAK,CAAC,CAAC;CAC/E,CAAC;AACH,CAAC;AAEH,MAAa,6BAA6B,YAGb,MAAM,OAAO,UAAU,qBAAqB,OAAO,CAAC;AAEjF,MAAa,kCACX,MAAM,OACJ,UACA,OAAO,IAAI,aAAa;CACtB,MAAM,UAAU,OAAO,IAAI,qBAAK,IAAI,IAAY,CAAC;CACjD,MAAM,UAAU,OAAO,IAAI,qBAAK,IAAI,IAAoB,CAAC;CAEzD,OAAO,SAAS,GAAG;EACjB,QAAO,UAAS,IAAI,OAAO,UAAS,YAAW,IAAI,IAAI,OAAO,EAAE,IAAI,KAAK,CAAC;EAC1E,UAAS,UACP,OAAO,IACL,IAAI,OAAO,UAAS,YAAW;GAC7B,MAAM,OAAO,IAAI,IAAI,OAAO;GAC5B,KAAK,OAAO,KAAK;GACjB,OAAO;EACT,CAAC,GACD,IAAI,OAAO,UAAS,YAAW;GAC7B,MAAM,OAAO,IAAI,IAAI,OAAO;GAC5B,KAAK,OAAO,KAAK;GACjB,OAAO;EACT,CAAC,CACH,EAAE,KAAK,OAAO,MAAM;EACtB,YAAW,UAAS,IAAI,IAAI,OAAO,EAAE,KAAK,OAAO,KAAI,YAAW,QAAQ,IAAI,KAAK,CAAC,CAAC;EACnF,SAAS,IAAI,IAAI,OAAO,EAAE,KAAK,OAAO,KAAI,YAAW,IAAI,IAAI,OAAO,CAAC,CAAC;EACtE,uBAAsB,UACpB,IAAI,OAAO,UAAS,YAAW;GAC7B,MAAM,aAAa,QAAQ,IAAI,KAAK,KAAK,KAAK;GAC9C,MAAM,OAAO,IAAI,IAAI,OAAO;GAC5B,KAAK,IAAI,OAAO,SAAS;GACzB,OAAO,CAAC,WAAW,IAAI;EACzB,CAAC;EACH,cAAa,UAAS,IAAI,IAAI,OAAO,EAAE,KAAK,OAAO,KAAI,YAAW,QAAQ,IAAI,KAAK,KAAK,CAAC,CAAC;CAC5F,CAAC;AACH,CAAC,CACH"}
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "@yolk-sdk/harness",
3
+ "version": "0.1.0-canary.77",
4
+ "description": "Domain-free agent run lifecycle: coordinator, store, inbox, and pluggable drivers.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/magoz/yolk-sdk.git",
11
+ "directory": "packages/harness"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/magoz/yolk-sdk/issues"
15
+ },
16
+ "homepage": "https://github.com/magoz/yolk-sdk#readme",
17
+ "keywords": [
18
+ "agent",
19
+ "harness",
20
+ "lifecycle",
21
+ "effect"
22
+ ],
23
+ "engines": {
24
+ "node": ">=22"
25
+ },
26
+ "exports": {
27
+ "./package.json": "./package.json",
28
+ ".": {
29
+ "types": "./dist/index.d.mts",
30
+ "import": "./dist/index.mjs",
31
+ "default": "./dist/index.mjs"
32
+ },
33
+ "./coordinator": {
34
+ "types": "./dist/coordinator.d.mts",
35
+ "import": "./dist/coordinator.mjs",
36
+ "default": "./dist/coordinator.mjs"
37
+ },
38
+ "./store": {
39
+ "types": "./dist/store.d.mts",
40
+ "import": "./dist/store.mjs",
41
+ "default": "./dist/store.mjs"
42
+ },
43
+ "./inbox": {
44
+ "types": "./dist/inbox.d.mts",
45
+ "import": "./dist/inbox.mjs",
46
+ "default": "./dist/inbox.mjs"
47
+ },
48
+ "./driver": {
49
+ "types": "./dist/driver.d.mts",
50
+ "import": "./dist/driver.mjs",
51
+ "default": "./dist/driver.mjs"
52
+ },
53
+ "./driver/memory": {
54
+ "types": "./dist/driver/memory.d.mts",
55
+ "import": "./dist/driver/memory.mjs",
56
+ "default": "./dist/driver/memory.mjs"
57
+ },
58
+ "./driver/durable-object": {
59
+ "types": "./dist/driver/durable-object.d.mts",
60
+ "import": "./dist/driver/durable-object.mjs",
61
+ "default": "./dist/driver/durable-object.mjs"
62
+ },
63
+ "./outcome": {
64
+ "types": "./dist/outcome.d.mts",
65
+ "import": "./dist/outcome.mjs",
66
+ "default": "./dist/outcome.mjs"
67
+ }
68
+ },
69
+ "files": [
70
+ "src/**/*.ts",
71
+ "!src/**/*.test.ts",
72
+ "!src/**/*.test.tsx",
73
+ "dist/**/*",
74
+ "README.md"
75
+ ],
76
+ "publishConfig": {
77
+ "access": "public",
78
+ "provenance": true
79
+ },
80
+ "dependencies": {
81
+ "effect": "4.0.0-beta.80",
82
+ "@yolk-sdk/agent": "^0.1.0-canary.77"
83
+ },
84
+ "scripts": {
85
+ "build": "tsdown",
86
+ "check": "tsc -p tsconfig.json --noEmit",
87
+ "test": "vitest run --passWithNoTests",
88
+ "test:run": "vitest run --passWithNoTests"
89
+ }
90
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Process-local run coordinator.
3
+ *
4
+ * Port of OpenCode v2 `SessionRunCoordinator` (MIT): one busy period per key,
5
+ * a doorbell that coalesces wakes, and interrupt that claims pending wakes so a
6
+ * dead intent cannot restart. Steering lands at the next drain boundary.
7
+ *
8
+ * @see https://github.com/sst/opencode (packages/core/src/session/run-coordinator.ts)
9
+ */
10
+ import { Deferred, Effect, Fiber, FiberSet } from 'effect'
11
+ import type { Exit, Scope } from 'effect'
12
+
13
+ // Private settlement receipt: succeed the Deferred with Exit as a value so
14
+ // interrupt fan-out does not skip Effect 4.0.0-beta.80 Deferred listeners.
15
+ // Public waiters flatten that Exit; this is not a global Deferred fix.
16
+ const awaitDone = <E>(done: Deferred.Deferred<Exit.Exit<void, E>>): Effect.Effect<void, E> =>
17
+ Deferred.await(done).pipe(Effect.flatMap(exit => exit))
18
+
19
+ /** `"input"` subsumes `"steer"` when coalescing wakes. */
20
+ export type Promotable = 'input' | 'steer'
21
+
22
+ export type CapturedRun<E> =
23
+ | { readonly _tag: 'Started'; readonly join: Effect.Effect<void, E> }
24
+ | { readonly _tag: 'Joined'; readonly join: Effect.Effect<void, E> }
25
+ | { readonly _tag: 'Stopping'; readonly awaitSettlement: Effect.Effect<void> }
26
+
27
+ export type StopReceipt =
28
+ | { readonly _tag: 'Idle' }
29
+ | { readonly _tag: 'Interrupted' }
30
+ | { readonly _tag: 'LiveStopping' }
31
+ | { readonly _tag: 'Settling' }
32
+
33
+ export type Coordinator<Key, E, Reason = never> = {
34
+ readonly active: Effect.Effect<ReadonlySet<Key>>
35
+ readonly isActive: (key: Key) => Effect.Effect<boolean>
36
+ /** Starts an execution while idle, or joins the active execution. */
37
+ readonly run: (key: Key) => Effect.Effect<void, E>
38
+ /**
39
+ * Captures a run waiter without awaiting settlement.
40
+ * Idle starts force=true (`Started`); an active owner is joined (`Joined`),
41
+ * including during natural settlement. A stopping owner yields `Stopping` with a
42
+ * settlement waiter and no start.
43
+ */
44
+ readonly captureRun: (key: Key) => Effect.Effect<CapturedRun<E>>
45
+ /** Rings the doorbell: idle starts; active drains again before settling. */
46
+ readonly wake: (key: Key, scope?: Promotable) => Effect.Effect<void>
47
+ /**
48
+ * Stops the active execution and clears its doorbell. No-op when idle.
49
+ * Resolves once the interruption request is delivered, not when cleanup settles.
50
+ * First accepted `reason` is kept; use `terminalStop` to escalate.
51
+ */
52
+ readonly interrupt: (
53
+ key: Key,
54
+ reason?: Reason,
55
+ options?: { readonly awaitSettlement?: boolean }
56
+ ) => Effect.Effect<boolean>
57
+ /**
58
+ * Terminal stop receipt. Always clears pendingWake. Settling means the owner
59
+ * is already gone and the settled callback is chosen; Driver may release the
60
+ * claim under the Inbox gate. LiveStopping/Interrupted leave release to that
61
+ * owner's future settled callback. LiveStopping joins the same outstanding
62
+ * interruption request rather than forking a second one.
63
+ */
64
+ readonly terminalStop: (key: Key, reason: Reason) => Effect.Effect<StopReceipt>
65
+ /** Resolves once no execution is active. Never starts work. */
66
+ readonly awaitIdle: (key: Key) => Effect.Effect<void>
67
+ }
68
+
69
+ type Execution<E, Reason> = {
70
+ readonly done: Deferred.Deferred<Exit.Exit<void, E>>
71
+ owner?: Fiber.Fiber<void>
72
+ request?: Fiber.Fiber<void>
73
+ scope: Promotable
74
+ pendingWake?: Promotable
75
+ stopping: boolean
76
+ interruptionReason?: Reason
77
+ }
78
+
79
+ const widerScope = (current: Promotable | undefined, next: Promotable): Promotable =>
80
+ current === 'input' || next === 'input' ? 'input' : 'steer'
81
+
82
+ export const makeCoordinator = <Key, E, Reason = never>(options: {
83
+ readonly drain: (key: Key, force: boolean, scope: Promotable) => Effect.Effect<void, E>
84
+ readonly started?: (key: Key) => Effect.Effect<void>
85
+ readonly settled?: (key: Key, exit: Exit.Exit<void, E>, reason?: Reason) => Effect.Effect<void>
86
+ }): Effect.Effect<Coordinator<Key, E, Reason>, never, Scope.Scope> =>
87
+ Effect.gen(function* () {
88
+ const executions = new Map<Key, Execution<E, Reason>>()
89
+ const fork = yield* FiberSet.makeRuntime<never, void, never>()
90
+
91
+ const loop = (
92
+ key: Key,
93
+ execution: Execution<E, Reason>,
94
+ force: boolean
95
+ ): Effect.Effect<void, E> =>
96
+ Effect.suspend(() => {
97
+ if (execution.stopping) return Effect.void
98
+ return options.drain(key, force, execution.scope)
99
+ }).pipe(
100
+ Effect.andThen(
101
+ Effect.suspend(() => {
102
+ if (execution.stopping || execution.pendingWake === undefined) return Effect.void
103
+ execution.scope = execution.pendingWake
104
+ execution.pendingWake = undefined
105
+ return Effect.yieldNow.pipe(Effect.andThen(loop(key, execution, false)))
106
+ })
107
+ )
108
+ )
109
+
110
+ const settle = (key: Key, execution: Execution<E, Reason>, exit: Exit.Exit<void, E>) => {
111
+ if (execution.pendingWake !== undefined) start(key, false, execution.pendingWake)
112
+ else executions.delete(key)
113
+ return Deferred.succeed(execution.done, exit).pipe(Effect.asVoid)
114
+ }
115
+
116
+ const start = (key: Key, force: boolean, scope: Promotable) => {
117
+ const execution: Execution<E, Reason> = {
118
+ done: Deferred.makeUnsafe<Exit.Exit<void, E>>(),
119
+ scope,
120
+ stopping: false
121
+ }
122
+ executions.set(key, execution)
123
+ execution.owner = fork(
124
+ Effect.yieldNow.pipe(
125
+ Effect.andThen(
126
+ Effect.uninterruptible(Effect.suspend(() => options.started?.(key) ?? Effect.void))
127
+ ),
128
+ Effect.andThen(loop(key, execution, force)),
129
+ Effect.onExit(exit =>
130
+ Effect.suspend(() => {
131
+ execution.owner = undefined
132
+ return options.settled?.(key, exit, execution.interruptionReason) ?? Effect.void
133
+ })
134
+ ),
135
+ Effect.onExit(exit => settle(key, execution, exit)),
136
+ Effect.exit,
137
+ Effect.asVoid
138
+ )
139
+ )
140
+ return execution
141
+ }
142
+
143
+ const forkInterruptionRequest = (owner: Fiber.Fiber<void>): Fiber.Fiber<void> =>
144
+ fork(Effect.yieldNow.pipe(Effect.andThen(Effect.sync(() => owner.interruptUnsafe()))))
145
+
146
+ const interruptNow = (key: Key, reason?: Reason): Effect.Effect<boolean> =>
147
+ Effect.suspend(() => {
148
+ const execution = executions.get(key)
149
+ if (execution === undefined || execution.stopping) return Effect.succeed(false)
150
+ if (execution.owner === undefined) {
151
+ execution.pendingWake = undefined
152
+ return Effect.succeed(false)
153
+ }
154
+ const owner = execution.owner
155
+ execution.stopping = true
156
+ execution.pendingWake = undefined
157
+ execution.interruptionReason = reason
158
+ // Capture the exact old owner and fork the request in this same lazy
159
+ // transition so cancellation cannot leave stopping=true with no request.
160
+ // Initial yield breaks owner -> resumed caller -> owner reentrancy.
161
+ // Join the request fiber only; interruptUnsafe may run interruptible
162
+ // finalizers inline until they suspend. Do not await owner settlement.
163
+ const request = forkInterruptionRequest(owner)
164
+ execution.request = request
165
+ return Fiber.join(request).pipe(Effect.as(true))
166
+ })
167
+
168
+ const awaitIdle = (key: Key): Effect.Effect<void> =>
169
+ Effect.suspend(() => {
170
+ const execution = executions.get(key)
171
+ if (execution === undefined) return Effect.void
172
+ return awaitDone(execution.done).pipe(Effect.ignoreCause, Effect.andThen(awaitIdle(key)))
173
+ })
174
+
175
+ return {
176
+ active: Effect.sync(() => new Set(executions.keys())),
177
+ isActive: key => Effect.sync(() => executions.has(key)),
178
+ run: key =>
179
+ Effect.suspend(() => {
180
+ const execution = executions.get(key)
181
+ if (execution === undefined) return awaitDone(start(key, true, 'input').done)
182
+ if (!execution.stopping) return awaitDone(execution.done)
183
+ return awaitDone(execution.done).pipe(
184
+ Effect.ignoreCause,
185
+ Effect.andThen(
186
+ Effect.suspend(() => {
187
+ const next = executions.get(key)
188
+ return next === undefined
189
+ ? awaitDone(start(key, true, 'input').done)
190
+ : awaitDone(next.done)
191
+ })
192
+ )
193
+ )
194
+ }),
195
+ captureRun: key =>
196
+ Effect.sync(() => {
197
+ const execution = executions.get(key)
198
+ if (execution === undefined) {
199
+ const started = start(key, true, 'input')
200
+ return { _tag: 'Started' as const, join: awaitDone(started.done) }
201
+ }
202
+ if (!execution.stopping) {
203
+ return { _tag: 'Joined' as const, join: awaitDone(execution.done) }
204
+ }
205
+ return {
206
+ _tag: 'Stopping' as const,
207
+ awaitSettlement: awaitDone(execution.done).pipe(Effect.ignoreCause)
208
+ }
209
+ }),
210
+ wake: (key, scope = 'input') =>
211
+ Effect.sync(() => {
212
+ const execution = executions.get(key)
213
+ if (execution !== undefined) {
214
+ execution.pendingWake = widerScope(execution.pendingWake, scope)
215
+ return
216
+ }
217
+ start(key, false, scope)
218
+ }),
219
+ interrupt: (key, reason, options) =>
220
+ Effect.suspend(() => {
221
+ const execution = executions.get(key)
222
+ return interruptNow(key, reason).pipe(
223
+ Effect.tap(() =>
224
+ options?.awaitSettlement === true && execution !== undefined
225
+ ? awaitDone(execution.done).pipe(Effect.ignoreCause)
226
+ : Effect.void
227
+ )
228
+ )
229
+ }),
230
+ terminalStop: (key, reason) =>
231
+ Effect.suspend((): Effect.Effect<StopReceipt> => {
232
+ const execution = executions.get(key)
233
+ if (execution === undefined) return Effect.succeed({ _tag: 'Idle' } as const)
234
+ execution.pendingWake = undefined
235
+ if (execution.owner === undefined) {
236
+ execution.stopping = true
237
+ return Effect.succeed({ _tag: 'Settling' } as const)
238
+ }
239
+ execution.interruptionReason = reason
240
+ if (execution.stopping) {
241
+ const request = execution.request
242
+ return request === undefined
243
+ ? Effect.succeed({ _tag: 'LiveStopping' } as const)
244
+ : Fiber.join(request).pipe(Effect.as({ _tag: 'LiveStopping' } as const))
245
+ }
246
+ const owner = execution.owner
247
+ execution.stopping = true
248
+ const request = forkInterruptionRequest(owner)
249
+ execution.request = request
250
+ return Fiber.join(request).pipe(Effect.as({ _tag: 'Interrupted' } as const))
251
+ }),
252
+ awaitIdle
253
+ }
254
+ })