Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
351 changes: 299 additions & 52 deletions apps/server/src/orchestration-v2/CheckpointCaptureService.test.ts

Large diffs are not rendered by default.

280 changes: 185 additions & 95 deletions apps/server/src/orchestration-v2/CheckpointCaptureService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import {
CheckpointScopeId,
CommandId,
type OrchestrationV2Checkpoint,
type OrchestrationV2DomainEvent,
type OrchestrationV2ExecutionNode,
type OrchestrationV2ProviderThread,
type OrchestrationV2Run,
type OrchestrationV2TurnItem,
RunId,
ThreadId,
} from "@t3tools/contracts";
import * as Cause from "effect/Cause";
import * as Context from "effect/Context";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
Expand All @@ -19,6 +21,7 @@ import * as CheckpointService from "./CheckpointService.ts";
import * as EventSink from "./EventSink.ts";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
import * as ProjectionStore from "./ProjectionStore.ts";
import * as ThreadCommandExecutor from "./ThreadCommandExecutor.ts";

export class CheckpointCaptureExecutionError extends Schema.TaggedError<CheckpointCaptureExecutionError>()(
"CheckpointCaptureExecutionError",
Expand All @@ -37,6 +40,7 @@ export interface CheckpointCaptureServiceV2Shape {
readonly threadId: ThreadId;
readonly runId: RunId;
readonly scopeId: CheckpointScopeId;
readonly willRetry?: boolean;
}) => Effect.Effect<void, CheckpointCaptureExecutionError>;
}

Expand All @@ -52,13 +56,15 @@ export const layer: Layer.Layer<
| EventSink.EventSinkV2
| IdAllocator.IdAllocatorV2
| ProjectionStore.ProjectionStoreV2
| ThreadCommandExecutor.ThreadCommandExecutor
> = Layer.effect(
CheckpointCaptureServiceV2,
Effect.gen(function* () {
const checkpoints = yield* CheckpointService.CheckpointServiceV2;
const eventSink = yield* EventSink.EventSinkV2;
const ids = yield* IdAllocator.IdAllocatorV2;
const projections = yield* ProjectionStore.ProjectionStoreV2;
const threadCommands = yield* ThreadCommandExecutor.ThreadCommandExecutor;

const execute = Effect.fn("orchestrationV2.checkpointCapture.execute")(function* (input: {
readonly threadId: ThreadId;
Expand Down Expand Up @@ -134,113 +140,197 @@ export const layer: Layer.Layer<
// (ProjectionStore preserves the field when absent from the payload).
const { delegatedCompletion: _delegatedCompletion, ...runWithoutDelegatedCompletion } = run;
const commandId = CommandId.make(`command:effect:checkpoint.capture:${run.id}`);
yield* eventSink.commitCommand({
commandId,
threadId: input.threadId,
commandType: "checkpoint.capture",
acceptedAt: capturedAt,
effects: [],
events: [
...(threadStartCheckpoint === null
? []
: [
{
id: yield* ids.allocate.event({ threadId: input.threadId, commandId }),
type: "checkpoint.captured" as const,
threadId: input.threadId,
nodeId: threadStartCheckpoint.nodeId,
driver: providerThread.driver,
providerInstanceId: run.providerInstanceId,
occurredAt: capturedAt,
payload: threadStartCheckpoint,
},
]),
...(baselineCheckpoint === null
? []
: [
{
id: yield* ids.allocate.event({ threadId: input.threadId, commandId }),
type: "checkpoint.captured" as const,
threadId: input.threadId,
nodeId: baselineCheckpoint.nodeId,
driver: providerThread.driver,
providerInstanceId: run.providerInstanceId,
occurredAt: capturedAt,
payload: baselineCheckpoint,
},
]),
{
id: yield* ids.allocate.event({ threadId: input.threadId, commandId }),
type: "checkpoint.captured",
threadId: input.threadId,
runId: run.id,
nodeId: rootNode.id,
driver: providerThread.driver,
providerInstanceId: run.providerInstanceId,
occurredAt: capturedAt,
payload: checkpoint,
},
{
id: yield* ids.allocate.event({ threadId: input.threadId, commandId }),
type: "turn-item.updated",
threadId: input.threadId,
runId: run.id,
nodeId: rootNode.id,
driver: providerThread.driver,
providerInstanceId: run.providerInstanceId,
occurredAt: capturedAt,
payload: makeCheckpointTurnItem({
idAllocator: ids,
run,
rootNode,
providerThread,
checkpoint,
completedAt: capturedAt,
}),
},
{
id: yield* ids.allocate.event({ threadId: input.threadId, commandId }),
type: "run.updated",
// Git and workspace admission stay outside the command lock so Stop can cancel them.
yield* eventSink
.writeIfRunCurrent({
commandId,
threadId: input.threadId,
runId: run.id,
activeAttemptId: run.activeAttemptId,
expectedStatus: run.status,
effects: [],
events: [
...(threadStartCheckpoint === null
? []
: [
{
id: yield* ids.allocate.event({ threadId: input.threadId, commandId }),
type: "checkpoint.captured" as const,
threadId: input.threadId,
nodeId: threadStartCheckpoint.nodeId,
driver: providerThread.driver,
providerInstanceId: run.providerInstanceId,
occurredAt: capturedAt,
payload: threadStartCheckpoint,
},
]),
...(baselineCheckpoint === null
? []
: [
{
id: yield* ids.allocate.event({ threadId: input.threadId, commandId }),
type: "checkpoint.captured" as const,
threadId: input.threadId,
nodeId: baselineCheckpoint.nodeId,
driver: providerThread.driver,
providerInstanceId: run.providerInstanceId,
occurredAt: capturedAt,
payload: baselineCheckpoint,
},
]),
{
id: yield* ids.allocate.event({ threadId: input.threadId, commandId }),
type: "checkpoint.captured",
threadId: input.threadId,
runId: run.id,
nodeId: rootNode.id,
driver: providerThread.driver,
providerInstanceId: run.providerInstanceId,
occurredAt: capturedAt,
payload: checkpoint,
},
{
id: yield* ids.allocate.event({ threadId: input.threadId, commandId }),
type: "turn-item.updated",
threadId: input.threadId,
runId: run.id,
nodeId: rootNode.id,
driver: providerThread.driver,
providerInstanceId: run.providerInstanceId,
occurredAt: capturedAt,
payload: makeCheckpointTurnItem({
idAllocator: ids,
run,
rootNode,
providerThread,
checkpoint,
completedAt: capturedAt,
}),
},
{
id: yield* ids.allocate.event({ threadId: input.threadId, commandId }),
type: "run.updated",
threadId: input.threadId,
runId: run.id,
nodeId: rootNode.id,
providerInstanceId: run.providerInstanceId,
occurredAt: capturedAt,
payload: stopped
? { ...runWithoutDelegatedCompletion, checkpointId: checkpoint.id }
: {
...runWithoutDelegatedCompletion,
status: "completed",
completedAt: capturedAt,
checkpointId: checkpoint.id,
},
},
...(stopped
? []
: [
{
id: yield* ids.allocate.event({ threadId: input.threadId, commandId }),
type: "node.updated" as const,
threadId: input.threadId,
runId: run.id,
nodeId: rootNode.id,
providerInstanceId: run.providerInstanceId,
occurredAt: capturedAt,
payload: {
...rootNode,
status: "completed" as const,
completedAt: capturedAt,
checkpointScopeId: scope.id,
},
},
]),
],
})
.pipe((settle) => threadCommands.withLock(input.threadId, settle));
});

const failCapture = Effect.fn("orchestrationV2.checkpointCapture.fail")(function* (
input: Parameters<CheckpointCaptureServiceV2Shape["execute"]>[0],
) {
yield* threadCommands.withLock(
input.threadId,
Effect.gen(function* () {
const { run, rootNode } = yield* projections.getCheckpointCaptureContext(
input.threadId,
input,
);
if (run?.status !== "waiting") return;
const now = yield* DateTime.now;
const commandId = CommandId.make(`command:effect:checkpoint.capture.failed:${run.id}`);
const { delegatedCompletion: _delegatedCompletion, ...currentRun } = run;
const eventContext = {
threadId: input.threadId,
runId: run.id,
nodeId: rootNode.id,
...(run.rootNodeId === null ? {} : { nodeId: run.rootNodeId }),
providerInstanceId: run.providerInstanceId,
occurredAt: capturedAt,
payload: stopped
? { ...runWithoutDelegatedCompletion, checkpointId: checkpoint.id }
: {
...runWithoutDelegatedCompletion,
status: "completed",
completedAt: capturedAt,
checkpointId: checkpoint.id,
},
},
...(stopped
? []
: [
{
id: yield* ids.allocate.event({ threadId: input.threadId, commandId }),
type: "node.updated" as const,
occurredAt: now,
};
const events: Array<OrchestrationV2DomainEvent> = [
{
...eventContext,
id: yield* ids.allocate.event({ threadId: input.threadId, commandId }),
type: "run.updated",
payload: { ...currentRun, status: "failed", completedAt: now },
},
];
if (rootNode !== undefined) {
events.push(
{
...eventContext,
id: yield* ids.allocate.event({ threadId: input.threadId, commandId }),
type: "node.updated",
payload: { ...rootNode, status: "failed", completedAt: now },
},
{
...eventContext,
id: yield* ids.allocate.event({ threadId: input.threadId, commandId }),
type: "turn-item.updated",
payload: {
id: ids.derive.runSignalTurnItem({ runId: run.id, signal: "checkpoint-failed" }),
threadId: input.threadId,
runId: run.id,
nodeId: rootNode.id,
providerInstanceId: run.providerInstanceId,
occurredAt: capturedAt,
payload: {
...rootNode,
status: "completed" as const,
completedAt: capturedAt,
checkpointScopeId: scope.id,
},
providerThreadId: run.providerThreadId,
providerTurnId: rootNode.providerTurnId,
nativeItemRef: null,
parentItemId: null,
ordinal: run.ordinal * 100 + 99,
type: "system_notice",
status: "completed",
title: "Checkpoint capture failed",
message:
"Checkpoint capture failed after retries. This turn has no checkpoint to restore.",
startedAt: now,
completedAt: now,
updatedAt: now,
},
]),
],
});
},
);
}
yield* eventSink.writeIfRunCurrent({
commandId,
threadId: input.threadId,
runId: run.id,
activeAttemptId: run.activeAttemptId,
expectedStatus: "waiting",
events,
});
}),
);
});

return CheckpointCaptureServiceV2.of({
execute: (input) =>
execute(input).pipe(
Effect.tapCause((cause) =>
input.willRetry === false && !Cause.hasInterruptsOnly(cause)
? failCapture(input)
: Effect.void,
),
Effect.mapError((cause) =>
isCheckpointCaptureExecutionError(cause)
? cause
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/orchestration-v2/EffectWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ export const layerExecutor: Layer.Layer<
threadId: effect.threadId,
runId: effect.request.runId,
scopeId: effect.request.scopeId,
willRetry,
})
.pipe(
Effect.mapError(
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/orchestration-v2/EventSink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export interface EventSinkV2Shape {
readonly commandId?: CommandId;
readonly threadId: ThreadId;
readonly runId: RunId;
readonly activeAttemptId: RunAttemptId;
readonly activeAttemptId: RunAttemptId | null;
readonly expectedStatus: OrchestrationV2Run["status"];
readonly events: ReadonlyArray<OrchestrationV2DomainEvent>;
readonly effects?: ReadonlyArray<EffectOutbox.PendingOrchestrationEffectV2>;
Expand Down
Loading
Loading