@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.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/coordinator.d.mts +62 -0
- package/dist/coordinator.d.mts.map +1 -0
- package/dist/coordinator.mjs +129 -0
- package/dist/coordinator.mjs.map +1 -0
- package/dist/driver/durable-object.d.mts +20 -0
- package/dist/driver/durable-object.d.mts.map +1 -0
- package/dist/driver/durable-object.mjs +12 -0
- package/dist/driver/durable-object.mjs.map +1 -0
- package/dist/driver/memory.d.mts +21 -0
- package/dist/driver/memory.d.mts.map +1 -0
- package/dist/driver/memory.mjs +15 -0
- package/dist/driver/memory.mjs.map +1 -0
- package/dist/driver.d.mts +67 -0
- package/dist/driver.d.mts.map +1 -0
- package/dist/driver.mjs +95 -0
- package/dist/driver.mjs.map +1 -0
- package/dist/inbox.d.mts +91 -0
- package/dist/inbox.d.mts.map +1 -0
- package/dist/inbox.mjs +224 -0
- package/dist/inbox.mjs.map +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -0
- package/dist/outcome.d.mts +61 -0
- package/dist/outcome.d.mts.map +1 -0
- package/dist/outcome.mjs +159 -0
- package/dist/outcome.mjs.map +1 -0
- package/dist/store.d.mts +25 -0
- package/dist/store.d.mts.map +1 -0
- package/dist/store.mjs +86 -0
- package/dist/store.mjs.map +1 -0
- package/package.json +90 -0
- package/src/coordinator.ts +254 -0
- package/src/driver/durable-object.ts +34 -0
- package/src/driver/memory.ts +38 -0
- package/src/driver.ts +250 -0
- package/src/inbox.ts +454 -0
- package/src/index.ts +2 -0
- package/src/outcome.ts +316 -0
- package/src/store.ts +137 -0
package/dist/outcome.mjs
ADDED
|
@@ -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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"}
|
package/dist/store.d.mts
ADDED
|
@@ -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
|
+
})
|