@smithers-orchestrator/engine 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +50 -0
- package/src/AlertHumanRequestOptions.ts +8 -0
- package/src/AlertRuntimeServices.ts +10 -0
- package/src/ChildWorkflowDefinition.ts +5 -0
- package/src/ChildWorkflowExecuteOptions.ts +14 -0
- package/src/ContinuationRequest.ts +3 -0
- package/src/HijackState.ts +19 -0
- package/src/HumanRequestKind.ts +1 -0
- package/src/HumanRequestStatus.ts +1 -0
- package/src/PlanNode.ts +29 -0
- package/src/RalphMeta.ts +7 -0
- package/src/RalphState.ts +4 -0
- package/src/RalphStateMap.ts +3 -0
- package/src/ScheduleResult.ts +15 -0
- package/src/SignalRunOptions.ts +5 -0
- package/src/alert-runtime.js +22 -0
- package/src/approvals.js +220 -0
- package/src/child-workflow.js +163 -0
- package/src/effect/ApprovalDeferredResolution.ts +13 -0
- package/src/effect/ApprovalDurableDeferredResolution.ts +11 -0
- package/src/effect/ApprovalPayload.ts +7 -0
- package/src/effect/ApprovalResult.ts +6 -0
- package/src/effect/BuilderNode.ts +52 -0
- package/src/effect/BuilderStepHandle.ts +47 -0
- package/src/effect/CancelPayload.ts +3 -0
- package/src/effect/CancelResult.ts +4 -0
- package/src/effect/DeferredResolution.ts +7 -0
- package/src/effect/DiffBundle.ts +7 -0
- package/src/effect/ExecuteTaskActivityOptions.ts +7 -0
- package/src/effect/FilePatch.ts +6 -0
- package/src/effect/GetRunPayload.ts +3 -0
- package/src/effect/GetRunResult.ts +3 -0
- package/src/effect/LegacyExecuteTaskFn.ts +24 -0
- package/src/effect/ListRunsPayload.ts +6 -0
- package/src/effect/RunStatusSchema.ts +9 -0
- package/src/effect/RunSummary.ts +23 -0
- package/src/effect/SignalPayload.ts +7 -0
- package/src/effect/SignalResult.ts +6 -0
- package/src/effect/SmithersSqliteOptions.ts +3 -0
- package/src/effect/SqlMessageStorageEventHistoryQuery.ts +7 -0
- package/src/effect/TaggedWorkerError.ts +46 -0
- package/src/effect/TaskActivityContext.ts +4 -0
- package/src/effect/TaskActivityRetryOptions.ts +4 -0
- package/src/effect/TaskBridgeToolConfig.ts +6 -0
- package/src/effect/TaskFailure.ts +3 -0
- package/src/effect/TaskResult.ts +5 -0
- package/src/effect/UnknownWorkerError.ts +5 -0
- package/src/effect/WaitForEventDurableDeferredResolution.ts +11 -0
- package/src/effect/WorkerDispatchKind.ts +1 -0
- package/src/effect/WorkerTask.ts +14 -0
- package/src/effect/WorkerTaskError.ts +4 -0
- package/src/effect/WorkerTaskKind.ts +1 -0
- package/src/effect/WorkflowPatchDecisionRecord.ts +4 -0
- package/src/effect/WorkflowPatchDecisions.ts +1 -0
- package/src/effect/WorkflowVersioningRuntime.ts +7 -0
- package/src/effect/activity-bridge.js +131 -0
- package/src/effect/bridge-utils.js +45 -0
- package/src/effect/builder.js +837 -0
- package/src/effect/compute-task-bridge.js +734 -0
- package/src/effect/deferred-bridge.js +63 -0
- package/src/effect/deferred-state-bridge.js +1343 -0
- package/src/effect/diff-bundle.js +352 -0
- package/src/effect/durable-deferred-bridge.js +282 -0
- package/src/effect/entity-worker.js +154 -0
- package/src/effect/http-runner.js +86 -0
- package/src/effect/rpc-schema.js +101 -0
- package/src/effect/single-runner.js +189 -0
- package/src/effect/sql-message-storage.js +817 -0
- package/src/effect/static-task-bridge.js +308 -0
- package/src/effect/versioning.js +123 -0
- package/src/effect/workflow-bridge.js +260 -0
- package/src/effect/workflow-make-bridge.js +233 -0
- package/src/engine.js +6933 -0
- package/src/events.js +237 -0
- package/src/external/json-schema-to-zod.js +214 -0
- package/src/getDefinedToolMetadata.js +10 -0
- package/src/hot/HotReloadEvent.ts +21 -0
- package/src/hot/HotWorkflowController.js +220 -0
- package/src/hot/OverlayOptions.ts +4 -0
- package/src/hot/WatchTreeOptions.ts +6 -0
- package/src/hot/index.js +9 -0
- package/src/hot/overlay.js +177 -0
- package/src/hot/watch.js +174 -0
- package/src/human-requests.js +120 -0
- package/src/index.d.ts +1597 -0
- package/src/index.js +41 -0
- package/src/runtime-owner.js +36 -0
- package/src/scheduler.js +31 -0
- package/src/signals.js +82 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// @smithers-type-exports-begin
|
|
2
|
+
/** @typedef {import("./TaggedWorkerError.ts").TaggedWorkerError} TaggedWorkerError */
|
|
3
|
+
/** @typedef {import("./TaskFailure.ts").TaskFailure} TaskFailure */
|
|
4
|
+
/** @typedef {import("./TaskResult.ts").TaskResult} TaskResult */
|
|
5
|
+
/** @typedef {import("./UnknownWorkerError.ts").UnknownWorkerError} UnknownWorkerError */
|
|
6
|
+
/** @typedef {import("./WorkerDispatchKind.ts").WorkerDispatchKind} WorkerDispatchKind */
|
|
7
|
+
/** @typedef {import("./WorkerTask.ts").WorkerTask} WorkerTask */
|
|
8
|
+
/** @typedef {import("./WorkerTaskError.ts").WorkerTaskError} WorkerTaskError */
|
|
9
|
+
/** @typedef {import("./WorkerTaskKind.ts").WorkerTaskKind} WorkerTaskKind */
|
|
10
|
+
// @smithers-type-exports-end
|
|
11
|
+
|
|
12
|
+
import * as Entity from "@effect/cluster/Entity";
|
|
13
|
+
import * as Rpc from "@effect/rpc/Rpc";
|
|
14
|
+
import { Schema } from "effect";
|
|
15
|
+
/** @typedef {import("@smithers-orchestrator/graph/TaskDescriptor").TaskDescriptor} _TaskDescriptor */
|
|
16
|
+
|
|
17
|
+
export const WorkerTaskKind = Schema.Literal("agent", "compute", "static");
|
|
18
|
+
export const WorkerDispatchKind = Schema.Literal("compute", "static", "legacy");
|
|
19
|
+
export const WorkerTask = Schema.Struct({
|
|
20
|
+
executionId: Schema.String,
|
|
21
|
+
bridgeKey: Schema.String,
|
|
22
|
+
workflowName: Schema.String,
|
|
23
|
+
runId: Schema.String,
|
|
24
|
+
nodeId: Schema.String,
|
|
25
|
+
iteration: Schema.Number,
|
|
26
|
+
retries: Schema.Number,
|
|
27
|
+
taskKind: WorkerTaskKind,
|
|
28
|
+
dispatchKind: WorkerDispatchKind,
|
|
29
|
+
});
|
|
30
|
+
const WorkerErrorDetails = Schema.Record({
|
|
31
|
+
key: Schema.String,
|
|
32
|
+
value: Schema.Unknown,
|
|
33
|
+
});
|
|
34
|
+
const TaskAbortedError = Schema.Struct({
|
|
35
|
+
_tag: Schema.Literal("TaskAborted"),
|
|
36
|
+
message: Schema.String,
|
|
37
|
+
details: Schema.optional(WorkerErrorDetails),
|
|
38
|
+
name: Schema.optional(Schema.String),
|
|
39
|
+
});
|
|
40
|
+
const TaskTimeoutError = Schema.Struct({
|
|
41
|
+
_tag: Schema.Literal("TaskTimeout"),
|
|
42
|
+
message: Schema.String,
|
|
43
|
+
nodeId: Schema.String,
|
|
44
|
+
attempt: Schema.Number,
|
|
45
|
+
timeoutMs: Schema.Number,
|
|
46
|
+
});
|
|
47
|
+
const TaskHeartbeatTimeoutError = Schema.Struct({
|
|
48
|
+
_tag: Schema.Literal("TaskHeartbeatTimeout"),
|
|
49
|
+
message: Schema.String,
|
|
50
|
+
nodeId: Schema.String,
|
|
51
|
+
iteration: Schema.Number,
|
|
52
|
+
attempt: Schema.Number,
|
|
53
|
+
timeoutMs: Schema.Number,
|
|
54
|
+
staleForMs: Schema.Number,
|
|
55
|
+
lastHeartbeatAtMs: Schema.Number,
|
|
56
|
+
});
|
|
57
|
+
const RunNotFoundError = Schema.Struct({
|
|
58
|
+
_tag: Schema.Literal("RunNotFound"),
|
|
59
|
+
message: Schema.String,
|
|
60
|
+
runId: Schema.String,
|
|
61
|
+
});
|
|
62
|
+
const InvalidInputError = Schema.Struct({
|
|
63
|
+
_tag: Schema.Literal("InvalidInput"),
|
|
64
|
+
message: Schema.String,
|
|
65
|
+
details: Schema.optional(WorkerErrorDetails),
|
|
66
|
+
});
|
|
67
|
+
const DbWriteFailedError = Schema.Struct({
|
|
68
|
+
_tag: Schema.Literal("DbWriteFailed"),
|
|
69
|
+
message: Schema.String,
|
|
70
|
+
details: Schema.optional(WorkerErrorDetails),
|
|
71
|
+
});
|
|
72
|
+
const AgentCliError = Schema.Struct({
|
|
73
|
+
_tag: Schema.Literal("AgentCliError"),
|
|
74
|
+
message: Schema.String,
|
|
75
|
+
details: Schema.optional(WorkerErrorDetails),
|
|
76
|
+
});
|
|
77
|
+
const WorkflowFailedError = Schema.Struct({
|
|
78
|
+
_tag: Schema.Literal("WorkflowFailed"),
|
|
79
|
+
message: Schema.String,
|
|
80
|
+
details: Schema.optional(WorkerErrorDetails),
|
|
81
|
+
status: Schema.optional(Schema.Number),
|
|
82
|
+
});
|
|
83
|
+
export const TaggedWorkerError = Schema.Union(TaskAbortedError, TaskTimeoutError, TaskHeartbeatTimeoutError, RunNotFoundError, InvalidInputError, DbWriteFailedError, AgentCliError, WorkflowFailedError);
|
|
84
|
+
const UnknownWorkerError = Schema.Struct({
|
|
85
|
+
_tag: Schema.Literal("UnknownWorkerError"),
|
|
86
|
+
errorId: Schema.String,
|
|
87
|
+
message: Schema.String,
|
|
88
|
+
});
|
|
89
|
+
export const WorkerTaskError = Schema.Union(TaggedWorkerError, UnknownWorkerError);
|
|
90
|
+
const TaskSuccess = Schema.Struct({
|
|
91
|
+
_tag: Schema.Literal("Success"),
|
|
92
|
+
executionId: Schema.String,
|
|
93
|
+
terminal: Schema.Boolean,
|
|
94
|
+
});
|
|
95
|
+
const TaskFailure = Schema.Struct({
|
|
96
|
+
_tag: Schema.Literal("Failure"),
|
|
97
|
+
executionId: Schema.String,
|
|
98
|
+
error: WorkerTaskError,
|
|
99
|
+
});
|
|
100
|
+
export const TaskResult = Schema.Union(TaskSuccess, TaskFailure);
|
|
101
|
+
export const TaskWorkerEntity = Entity.make("TaskWorker", [
|
|
102
|
+
Rpc.make("execute", {
|
|
103
|
+
payload: WorkerTask,
|
|
104
|
+
success: TaskResult,
|
|
105
|
+
}),
|
|
106
|
+
]);
|
|
107
|
+
/**
|
|
108
|
+
* @param {_TaskDescriptor} desc
|
|
109
|
+
* @returns {WorkerTaskKind}
|
|
110
|
+
*/
|
|
111
|
+
function getWorkerTaskKind(desc) {
|
|
112
|
+
if (desc.agent) {
|
|
113
|
+
return "agent";
|
|
114
|
+
}
|
|
115
|
+
if (desc.computeFn) {
|
|
116
|
+
return "compute";
|
|
117
|
+
}
|
|
118
|
+
return "static";
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* @param {string} bridgeKey
|
|
122
|
+
* @param {string} workflowName
|
|
123
|
+
* @param {string} runId
|
|
124
|
+
* @param {_TaskDescriptor} desc
|
|
125
|
+
* @param {WorkerDispatchKind} dispatchKind
|
|
126
|
+
* @returns {WorkerTask}
|
|
127
|
+
*/
|
|
128
|
+
export function makeWorkerTask(bridgeKey, workflowName, runId, desc, dispatchKind) {
|
|
129
|
+
return {
|
|
130
|
+
executionId: bridgeKey,
|
|
131
|
+
bridgeKey,
|
|
132
|
+
workflowName,
|
|
133
|
+
runId,
|
|
134
|
+
nodeId: desc.nodeId,
|
|
135
|
+
iteration: desc.iteration,
|
|
136
|
+
retries: desc.retries,
|
|
137
|
+
taskKind: getWorkerTaskKind(desc),
|
|
138
|
+
dispatchKind,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* @param {TaskResult} result
|
|
143
|
+
* @returns {result is TaskFailure}
|
|
144
|
+
*/
|
|
145
|
+
export function isTaskResultFailure(result) {
|
|
146
|
+
return result._tag === "Failure";
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* @param {WorkerTaskError} error
|
|
150
|
+
* @returns {error is UnknownWorkerError}
|
|
151
|
+
*/
|
|
152
|
+
export function isUnknownWorkerError(error) {
|
|
153
|
+
return error._tag === "UnknownWorkerError";
|
|
154
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { HttpRunner } from "@effect/cluster";
|
|
2
|
+
import { mkdir, cp, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { Effect, Layer } from "effect";
|
|
5
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
6
|
+
import { spawnCaptureEffect } from "@smithers-orchestrator/driver/child-process";
|
|
7
|
+
import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
|
|
8
|
+
import { SandboxEntityExecutor } from "@smithers-orchestrator/sandbox/effect/sandbox-entity";
|
|
9
|
+
/**
|
|
10
|
+
* @param {SandboxTransportConfig} config
|
|
11
|
+
* @returns {SandboxHandle}
|
|
12
|
+
*/
|
|
13
|
+
function baseHandle(config) {
|
|
14
|
+
const sandboxRoot = join(config.rootDir, ".smithers", "sandboxes", config.runId, config.sandboxId);
|
|
15
|
+
return {
|
|
16
|
+
runtime: config.runtime,
|
|
17
|
+
runId: config.runId,
|
|
18
|
+
sandboxId: config.sandboxId,
|
|
19
|
+
sandboxRoot,
|
|
20
|
+
requestPath: join(sandboxRoot, "request"),
|
|
21
|
+
resultPath: join(sandboxRoot, "result"),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export const DockerSandboxExecutorLive = Layer.succeed(SandboxEntityExecutor, SandboxEntityExecutor.of({
|
|
25
|
+
create: (config) => Effect.gen(function* () {
|
|
26
|
+
const handle = baseHandle(config);
|
|
27
|
+
yield* spawnCaptureEffect("docker", ["info"], {
|
|
28
|
+
cwd: config.rootDir,
|
|
29
|
+
env: process.env,
|
|
30
|
+
timeoutMs: 10_000,
|
|
31
|
+
maxOutputBytes: 200_000,
|
|
32
|
+
}).pipe(Effect.catchAll(() => Effect.fail(new SmithersError("PROCESS_SPAWN_FAILED", "Docker daemon not reachable.", { runtime: "docker" }))));
|
|
33
|
+
yield* Effect.tryPromise({
|
|
34
|
+
try: async () => {
|
|
35
|
+
await mkdir(handle.requestPath, { recursive: true });
|
|
36
|
+
await mkdir(handle.resultPath, { recursive: true });
|
|
37
|
+
},
|
|
38
|
+
catch: (cause) => toSmithersError(cause, "create docker sandbox workspace"),
|
|
39
|
+
});
|
|
40
|
+
return handle;
|
|
41
|
+
}),
|
|
42
|
+
ship: (bundlePath, handle) => Effect.tryPromise({
|
|
43
|
+
try: async () => {
|
|
44
|
+
await rm(handle.requestPath, { recursive: true, force: true });
|
|
45
|
+
await mkdir(handle.requestPath, { recursive: true });
|
|
46
|
+
await cp(bundlePath, handle.requestPath, { recursive: true });
|
|
47
|
+
},
|
|
48
|
+
catch: (cause) => toSmithersError(cause, "ship docker bundle"),
|
|
49
|
+
}),
|
|
50
|
+
execute: (_command, _handle) => Effect.succeed({ exitCode: 0 }),
|
|
51
|
+
collect: (handle) => Effect.succeed({ bundlePath: handle.resultPath }),
|
|
52
|
+
cleanup: (_handle) => Effect.void,
|
|
53
|
+
}));
|
|
54
|
+
export const CodeplaneSandboxExecutorLive = Layer.succeed(SandboxEntityExecutor, SandboxEntityExecutor.of({
|
|
55
|
+
create: (config) => Effect.gen(function* () {
|
|
56
|
+
const apiUrl = process.env.CODEPLANE_API_URL;
|
|
57
|
+
const apiKey = process.env.CODEPLANE_API_KEY;
|
|
58
|
+
if (!apiUrl || !apiKey) {
|
|
59
|
+
yield* Effect.fail(new SmithersError("INVALID_INPUT", "Codeplane runtime requires CODEPLANE_API_URL and CODEPLANE_API_KEY."));
|
|
60
|
+
}
|
|
61
|
+
const handle = baseHandle(config);
|
|
62
|
+
yield* Effect.tryPromise({
|
|
63
|
+
try: async () => {
|
|
64
|
+
await mkdir(handle.requestPath, { recursive: true });
|
|
65
|
+
await mkdir(handle.resultPath, { recursive: true });
|
|
66
|
+
},
|
|
67
|
+
catch: (cause) => toSmithersError(cause, "create codeplane sandbox workspace"),
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
...handle,
|
|
71
|
+
workspaceId: `${config.runId}:${config.sandboxId}`,
|
|
72
|
+
};
|
|
73
|
+
}),
|
|
74
|
+
ship: (bundlePath, handle) => Effect.tryPromise({
|
|
75
|
+
try: async () => {
|
|
76
|
+
await rm(handle.requestPath, { recursive: true, force: true });
|
|
77
|
+
await mkdir(handle.requestPath, { recursive: true });
|
|
78
|
+
await cp(bundlePath, handle.requestPath, { recursive: true });
|
|
79
|
+
},
|
|
80
|
+
catch: (cause) => toSmithersError(cause, "ship codeplane bundle"),
|
|
81
|
+
}),
|
|
82
|
+
execute: (_command, _handle) => Effect.succeed({ exitCode: 0 }),
|
|
83
|
+
collect: (handle) => Effect.succeed({ bundlePath: handle.resultPath }),
|
|
84
|
+
cleanup: (_handle) => Effect.void,
|
|
85
|
+
}));
|
|
86
|
+
export const SandboxHttpRunner = HttpRunner;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @smithers-type-exports-begin
|
|
2
|
+
/** @typedef {import("./ApprovalPayload.ts").ApprovalPayload} ApprovalPayload */
|
|
3
|
+
/** @typedef {import("./ApprovalResult.ts").ApprovalResult} ApprovalResult */
|
|
4
|
+
/** @typedef {import("./CancelPayload.ts").CancelPayload} CancelPayload */
|
|
5
|
+
/** @typedef {import("./CancelResult.ts").CancelResult} CancelResult */
|
|
6
|
+
/** @typedef {import("./GetRunPayload.ts").GetRunPayload} GetRunPayload */
|
|
7
|
+
/** @typedef {import("./GetRunResult.ts").GetRunResult} GetRunResult */
|
|
8
|
+
/** @typedef {import("./ListRunsPayload.ts").ListRunsPayload} ListRunsPayload */
|
|
9
|
+
/** @typedef {import("./RunStatusSchema.ts").RunStatusSchema} RunStatusSchema */
|
|
10
|
+
/** @typedef {import("./RunSummary.ts").RunSummary} RunSummary */
|
|
11
|
+
/** @typedef {import("./SignalPayload.ts").SignalPayload} SignalPayload */
|
|
12
|
+
/** @typedef {import("./SignalResult.ts").SignalResult} SignalResult */
|
|
13
|
+
// @smithers-type-exports-end
|
|
14
|
+
|
|
15
|
+
import * as Rpc from "@effect/rpc/Rpc";
|
|
16
|
+
import * as RpcGroup from "@effect/rpc/RpcGroup";
|
|
17
|
+
import { Schema } from "effect";
|
|
18
|
+
export const RunStatusSchema = Schema.Literal("running", "waiting-approval", "waiting-event", "waiting-timer", "finished", "continued", "failed", "cancelled");
|
|
19
|
+
export const ApprovalPayloadSchema = Schema.Struct({
|
|
20
|
+
runId: Schema.String,
|
|
21
|
+
nodeId: Schema.String,
|
|
22
|
+
iteration: Schema.optional(Schema.Number),
|
|
23
|
+
note: Schema.optional(Schema.String),
|
|
24
|
+
decidedBy: Schema.optional(Schema.String),
|
|
25
|
+
});
|
|
26
|
+
export const ApprovalResultSchema = Schema.Struct({
|
|
27
|
+
runId: Schema.String,
|
|
28
|
+
nodeId: Schema.String,
|
|
29
|
+
iteration: Schema.Number,
|
|
30
|
+
approved: Schema.Boolean,
|
|
31
|
+
});
|
|
32
|
+
export const CancelPayloadSchema = Schema.Struct({
|
|
33
|
+
runId: Schema.String,
|
|
34
|
+
});
|
|
35
|
+
export const CancelResultSchema = Schema.Struct({
|
|
36
|
+
runId: Schema.String,
|
|
37
|
+
status: Schema.Literal("cancelling", "cancelled"),
|
|
38
|
+
});
|
|
39
|
+
export const SignalPayloadSchema = Schema.Struct({
|
|
40
|
+
runId: Schema.String,
|
|
41
|
+
signalName: Schema.String,
|
|
42
|
+
data: Schema.optional(Schema.Unknown),
|
|
43
|
+
correlationId: Schema.optional(Schema.String),
|
|
44
|
+
sentBy: Schema.optional(Schema.String),
|
|
45
|
+
});
|
|
46
|
+
export const SignalResultSchema = Schema.Struct({
|
|
47
|
+
runId: Schema.String,
|
|
48
|
+
signalName: Schema.String,
|
|
49
|
+
delivered: Schema.Boolean,
|
|
50
|
+
status: Schema.Literal("signalled", "ignored"),
|
|
51
|
+
});
|
|
52
|
+
export const ListRunsPayloadSchema = Schema.Struct({
|
|
53
|
+
limit: Schema.optional(Schema.Number),
|
|
54
|
+
status: Schema.optional(RunStatusSchema),
|
|
55
|
+
});
|
|
56
|
+
export const RunSummarySchema = Schema.Struct({
|
|
57
|
+
runId: Schema.String,
|
|
58
|
+
parentRunId: Schema.NullOr(Schema.String),
|
|
59
|
+
workflowName: Schema.String,
|
|
60
|
+
workflowPath: Schema.NullOr(Schema.String),
|
|
61
|
+
workflowHash: Schema.NullOr(Schema.String),
|
|
62
|
+
status: RunStatusSchema,
|
|
63
|
+
createdAtMs: Schema.Number,
|
|
64
|
+
startedAtMs: Schema.NullOr(Schema.Number),
|
|
65
|
+
finishedAtMs: Schema.NullOr(Schema.Number),
|
|
66
|
+
heartbeatAtMs: Schema.NullOr(Schema.Number),
|
|
67
|
+
runtimeOwnerId: Schema.NullOr(Schema.String),
|
|
68
|
+
cancelRequestedAtMs: Schema.NullOr(Schema.Number),
|
|
69
|
+
hijackRequestedAtMs: Schema.NullOr(Schema.Number),
|
|
70
|
+
hijackTarget: Schema.NullOr(Schema.String),
|
|
71
|
+
vcsType: Schema.NullOr(Schema.String),
|
|
72
|
+
vcsRoot: Schema.NullOr(Schema.String),
|
|
73
|
+
vcsRevision: Schema.NullOr(Schema.String),
|
|
74
|
+
errorJson: Schema.NullOr(Schema.String),
|
|
75
|
+
configJson: Schema.NullOr(Schema.String),
|
|
76
|
+
});
|
|
77
|
+
export const GetRunPayloadSchema = Schema.Struct({
|
|
78
|
+
runId: Schema.String,
|
|
79
|
+
});
|
|
80
|
+
export const GetRunResultSchema = Schema.NullOr(RunSummarySchema);
|
|
81
|
+
export const approve = Rpc.make("approve", {
|
|
82
|
+
payload: ApprovalPayloadSchema,
|
|
83
|
+
success: ApprovalResultSchema,
|
|
84
|
+
});
|
|
85
|
+
export const cancel = Rpc.make("cancel", {
|
|
86
|
+
payload: CancelPayloadSchema,
|
|
87
|
+
success: CancelResultSchema,
|
|
88
|
+
});
|
|
89
|
+
export const signal = Rpc.make("signal", {
|
|
90
|
+
payload: SignalPayloadSchema,
|
|
91
|
+
success: SignalResultSchema,
|
|
92
|
+
});
|
|
93
|
+
export const listRuns = Rpc.make("listRuns", {
|
|
94
|
+
payload: ListRunsPayloadSchema,
|
|
95
|
+
success: Schema.Array(RunSummarySchema),
|
|
96
|
+
});
|
|
97
|
+
export const getRun = Rpc.make("getRun", {
|
|
98
|
+
payload: GetRunPayloadSchema,
|
|
99
|
+
success: GetRunResultSchema,
|
|
100
|
+
});
|
|
101
|
+
export const SmithersRpcGroup = RpcGroup.make(approve, cancel, signal, listRuns, getRun);
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import * as SingleRunner from "@effect/cluster/SingleRunner";
|
|
2
|
+
import * as SqliteClient from "@effect/sql-sqlite-bun/SqliteClient";
|
|
3
|
+
import { Effect, Layer, Scope } from "effect";
|
|
4
|
+
import { fromTaggedErrorPayload } from "@smithers-orchestrator/errors/fromTaggedErrorPayload";
|
|
5
|
+
import { toTaggedErrorPayload } from "@smithers-orchestrator/errors/toTaggedErrorPayload";
|
|
6
|
+
import { isUnknownWorkerError, isTaskResultFailure, TaskWorkerEntity, } from "./entity-worker.js";
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {(task: WorkerTask) => void} TaskWorkerDispatchSubscriber
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {{ terminal: boolean; }} WorkerExecutionResult
|
|
12
|
+
*/
|
|
13
|
+
/** @typedef {import("./WorkerTask.ts").WorkerTask} WorkerTask */
|
|
14
|
+
/** @typedef {import("./TaskResult.ts").TaskResult} TaskResult */
|
|
15
|
+
/** @typedef {import("./TaskFailure.ts").TaskFailure} TaskFailure */
|
|
16
|
+
/** @typedef {import("./WorkerTaskError.ts").WorkerTaskError} WorkerTaskError */
|
|
17
|
+
|
|
18
|
+
const workerExecutions = new Map();
|
|
19
|
+
const workerErrors = new Map();
|
|
20
|
+
const dispatchSubscribers = new Set();
|
|
21
|
+
let singleRunnerRuntimePromise;
|
|
22
|
+
/**
|
|
23
|
+
* @param {WorkerTask} task
|
|
24
|
+
*/
|
|
25
|
+
function notifyDispatchSubscribers(task) {
|
|
26
|
+
for (const subscriber of dispatchSubscribers) {
|
|
27
|
+
try {
|
|
28
|
+
subscriber(task);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Dispatch observers are best-effort and should not affect execution.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* @param {WorkerTask} task
|
|
37
|
+
* @returns {Extract<TaskResult, { _tag: "Failure" }>}
|
|
38
|
+
*/
|
|
39
|
+
function buildMissingExecutionResult(task) {
|
|
40
|
+
return {
|
|
41
|
+
_tag: "Failure",
|
|
42
|
+
executionId: task.executionId,
|
|
43
|
+
error: {
|
|
44
|
+
_tag: "UnknownWorkerError",
|
|
45
|
+
errorId: `missing:${task.executionId}`,
|
|
46
|
+
message: `No worker execution registered for ${task.executionId}`,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} executionId
|
|
52
|
+
* @param {unknown} error
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
function storeWorkerError(executionId, error) {
|
|
56
|
+
const errorId = `${executionId}:error`;
|
|
57
|
+
workerErrors.set(errorId, error);
|
|
58
|
+
return errorId;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} executionId
|
|
62
|
+
* @param {unknown} error
|
|
63
|
+
* @returns {WorkerTaskError}
|
|
64
|
+
*/
|
|
65
|
+
function toWorkerTaskError(executionId, error) {
|
|
66
|
+
const taggedError = toTaggedErrorPayload(error);
|
|
67
|
+
if (taggedError) {
|
|
68
|
+
return taggedError;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
_tag: "UnknownWorkerError",
|
|
72
|
+
errorId: storeWorkerError(executionId, error),
|
|
73
|
+
message: error instanceof Error ? error.message : String(error),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* @param {TaskFailure} result
|
|
78
|
+
* @returns {unknown}
|
|
79
|
+
*/
|
|
80
|
+
function consumeWorkerError(result) {
|
|
81
|
+
if (!isUnknownWorkerError(result.error)) {
|
|
82
|
+
return fromTaggedErrorPayload(result.error);
|
|
83
|
+
}
|
|
84
|
+
const error = workerErrors.get(result.error.errorId);
|
|
85
|
+
workerErrors.delete(result.error.errorId);
|
|
86
|
+
if (error !== undefined) {
|
|
87
|
+
return error;
|
|
88
|
+
}
|
|
89
|
+
return new Error(result.error.message);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* @param {WorkerTask} task
|
|
93
|
+
* @returns {Promise<TaskResult>}
|
|
94
|
+
*/
|
|
95
|
+
async function runRegisteredExecution(task) {
|
|
96
|
+
const registered = workerExecutions.get(task.executionId);
|
|
97
|
+
if (!registered) {
|
|
98
|
+
return buildMissingExecutionResult(task);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
notifyDispatchSubscribers(registered.task);
|
|
102
|
+
const result = await registered.execute();
|
|
103
|
+
return {
|
|
104
|
+
_tag: "Success",
|
|
105
|
+
executionId: task.executionId,
|
|
106
|
+
terminal: result.terminal,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return {
|
|
111
|
+
_tag: "Failure",
|
|
112
|
+
executionId: task.executionId,
|
|
113
|
+
error: toWorkerTaskError(task.executionId, error),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
if (workerExecutions.get(task.executionId) === registered) {
|
|
118
|
+
workerExecutions.delete(task.executionId);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* @returns {Promise<SingleRunnerRuntime>}
|
|
124
|
+
*/
|
|
125
|
+
async function buildSingleRunnerRuntime() {
|
|
126
|
+
const runnerLayer = SingleRunner.layer({ runnerStorage: "memory" }).pipe(Layer.provide(Layer.orDie(SqliteClient.layer({
|
|
127
|
+
filename: ":memory:",
|
|
128
|
+
disableWAL: true,
|
|
129
|
+
}))));
|
|
130
|
+
const layer = TaskWorkerEntity.toLayer(TaskWorkerEntity.of({
|
|
131
|
+
execute: (request) => Effect.promise(() => runRegisteredExecution(request.payload)),
|
|
132
|
+
}), { concurrency: "unbounded" }).pipe(Layer.provideMerge(runnerLayer));
|
|
133
|
+
const scope = await Effect.runPromise(Scope.make());
|
|
134
|
+
const context = await Effect.runPromise(Layer.buildWithScope(layer, scope));
|
|
135
|
+
const client = await Effect.runPromise(TaskWorkerEntity.client.pipe(Effect.provide(context)));
|
|
136
|
+
return {
|
|
137
|
+
client: client,
|
|
138
|
+
context,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* @returns {Promise<SingleRunnerRuntime>}
|
|
143
|
+
*/
|
|
144
|
+
async function getSingleRunnerRuntime() {
|
|
145
|
+
if (!singleRunnerRuntimePromise) {
|
|
146
|
+
singleRunnerRuntimePromise = buildSingleRunnerRuntime().catch((error) => {
|
|
147
|
+
singleRunnerRuntimePromise = undefined;
|
|
148
|
+
throw error;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return singleRunnerRuntimePromise;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* @param {WorkerTask} task
|
|
155
|
+
* @param {() => Promise<WorkerExecutionResult>} execute
|
|
156
|
+
* @returns {Promise<WorkerExecutionResult>}
|
|
157
|
+
*/
|
|
158
|
+
export async function dispatchWorkerTask(task, execute) {
|
|
159
|
+
const runtime = await getSingleRunnerRuntime();
|
|
160
|
+
const registered = {
|
|
161
|
+
task,
|
|
162
|
+
execute,
|
|
163
|
+
};
|
|
164
|
+
workerExecutions.set(task.executionId, registered);
|
|
165
|
+
try {
|
|
166
|
+
const result = await Effect.runPromise(runtime.client(task.bridgeKey).execute(task).pipe(Effect.provide(runtime.context)));
|
|
167
|
+
if (isTaskResultFailure(result)) {
|
|
168
|
+
throw consumeWorkerError(result);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
terminal: result.terminal,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
if (workerExecutions.get(task.executionId) === registered) {
|
|
176
|
+
workerExecutions.delete(task.executionId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* @param {TaskWorkerDispatchSubscriber} subscriber
|
|
182
|
+
* @returns {() => void}
|
|
183
|
+
*/
|
|
184
|
+
export function subscribeTaskWorkerDispatches(subscriber) {
|
|
185
|
+
dispatchSubscribers.add(subscriber);
|
|
186
|
+
return () => {
|
|
187
|
+
dispatchSubscribers.delete(subscriber);
|
|
188
|
+
};
|
|
189
|
+
}
|