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
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import {
type OrchestrationEngineShape,
} from "../src/orchestration/Services/OrchestrationEngine.ts";
import { ThreadDeletionReactor } from "../src/orchestration/Services/ThreadDeletionReactor.ts";
import { TaskFireReactor } from "../src/orchestration/Services/TaskFireReactor.ts";
import { OrchestrationReactor } from "../src/orchestration/Services/OrchestrationReactor.ts";
import { ProjectionSnapshotQuery } from "../src/orchestration/Services/ProjectionSnapshotQuery.ts";
import {
Expand Down Expand Up @@ -375,6 +376,12 @@ export const makeOrchestrationIntegrationHarness = (
drain: Effect.void,
}),
),
Layer.provideMerge(
Layer.succeed(TaskFireReactor, {
start: () => Effect.void,
drain: Effect.void,
}),
),
Layer.provideMerge(
Layer.succeed(AgentAwarenessRelay.AgentAwarenessRelay, {
publishThread: () => Effect.void,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { OrchestrationLayerLive } from "../src/orchestration/runtimeLayer.ts";
import * as OrchestrationEngine from "../src/orchestration/Services/OrchestrationEngine.ts";
import * as OrchestrationReactor from "../src/orchestration/Services/OrchestrationReactor.ts";
import * as ProjectionSnapshotQuery from "../src/orchestration/Services/ProjectionSnapshotQuery.ts";
import * as TaskScheduler from "../src/orchestration/Services/TaskScheduler.ts";
import { makeSqlitePersistenceLive } from "../src/persistence/Layers/Sqlite.ts";
import * as ProviderSessionRuntime from "../src/persistence/ProviderSessionRuntime.ts";
import * as ExternalLauncher from "../src/process/externalLauncher.ts";
Expand Down Expand Up @@ -75,6 +76,10 @@ const startupDependencies = Layer.mergeAll(
Layer.succeed(ProviderSessionReaper.ProviderSessionReaper, {
start: () => Effect.void,
}),
Layer.succeed(TaskScheduler.TaskScheduler, {
start: () => Effect.void,
tick: () => Effect.succeed(0),
}),
ServerLifecycleEvents.layer,
Layer.succeed(ServerEnvironment.ServerEnvironment, {
getEnvironmentId: Effect.succeed(EnvironmentId.make("environment-startup-orphan")),
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const RPC_REQUIRED_SCOPES = {
[ORCHESTRATION_WS_METHODS.getTurnDiff]: AuthOrchestrationReadScope,
[ORCHESTRATION_WS_METHODS.getFullThreadDiff]: AuthOrchestrationReadScope,
[ORCHESTRATION_WS_METHODS.searchThreads]: AuthOrchestrationReadScope,
[ORCHESTRATION_WS_METHODS.listTasks]: AuthOrchestrationReadScope,
[ORCHESTRATION_WS_METHODS.subscribeShell]: AuthOrchestrationReadScope,
[ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot]: AuthOrchestrationReadScope,
[ORCHESTRATION_WS_METHODS.subscribeThread]: AuthOrchestrationReadScope,
Expand Down
10 changes: 10 additions & 0 deletions apps/server/src/checkpointing/CheckpointDiffQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ describe("CheckpointDiffQuery.layer", () => {
getThreadDetailById: () => Effect.succeed(Option.none()),
getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
searchThreads: () => Effect.succeed({ matches: [] }),
listTasks: () => Effect.succeed([]),
listDueTasks: () => Effect.succeed([]),
}),
),
);
Expand Down Expand Up @@ -203,6 +205,8 @@ describe("CheckpointDiffQuery.layer", () => {
getThreadDetailById: () => Effect.succeed(Option.none()),
getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
searchThreads: () => Effect.succeed({ matches: [] }),
listTasks: () => Effect.succeed([]),
listDueTasks: () => Effect.succeed([]),
}),
),
);
Expand Down Expand Up @@ -287,6 +291,8 @@ describe("CheckpointDiffQuery.layer", () => {
getThreadDetailById: () => Effect.succeed(Option.none()),
getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
searchThreads: () => Effect.succeed({ matches: [] }),
listTasks: () => Effect.succeed([]),
listDueTasks: () => Effect.succeed([]),
}),
),
);
Expand Down Expand Up @@ -356,6 +362,8 @@ describe("CheckpointDiffQuery.layer", () => {
getThreadDetailById: () => Effect.succeed(Option.none()),
getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
searchThreads: () => Effect.succeed({ matches: [] }),
listTasks: () => Effect.succeed([]),
listDueTasks: () => Effect.succeed([]),
}),
),
);
Expand Down Expand Up @@ -410,6 +418,8 @@ describe("CheckpointDiffQuery.layer", () => {
getThreadDetailById: () => Effect.succeed(Option.none()),
getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
searchThreads: () => Effect.succeed({ matches: [] }),
listTasks: () => Effect.succeed([]),
listDueTasks: () => Effect.succeed([]),
}),
),
);
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export const make = Effect.gen(function* () {
threadSnooze: true,
threadPinning: true,
threadPinReorder: true,
taskScheduling: true,
threadTitleRegeneration: true,
...(serverSelfUpdate === null ? {} : { serverSelfUpdate }),
...(serverSelfUpdate === "boot-service" ? { serverSelfUpdateProgress: true } : {}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ describe("OrchestrationEngine", () => {
};

const projectionSnapshot = {
tasks: [],
snapshotSequence: 7,
updatedAt: "2026-03-03T00:00:04.000Z",
projects: [
Expand Down Expand Up @@ -208,6 +209,8 @@ describe("OrchestrationEngine", () => {
getThreadDetailById: () => Effect.succeed(Option.none()),
getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
searchThreads: () => Effect.succeed({ matches: [] }),
listTasks: () => Effect.succeed([]),
listDueTasks: () => Effect.succeed([]),
}),
),
Layer.provide(
Expand Down
12 changes: 10 additions & 2 deletions apps/server/src/orchestration/Layers/OrchestrationEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
OrchestrationEvent,
OrchestrationReadModel,
ProjectId,
TaskId,
ThreadId,
} from "@t3tools/contracts";
import { OrchestrationCommand } from "@t3tools/contracts";
Expand Down Expand Up @@ -61,8 +62,8 @@ interface CommandEnvelope {
}

function commandToAggregateRef(command: OrchestrationCommand): {
readonly aggregateKind: "project" | "thread";
readonly aggregateId: ProjectId | ThreadId;
readonly aggregateKind: "project" | "thread" | "task";
readonly aggregateId: ProjectId | ThreadId | TaskId;
} {
switch (command.type) {
case "project.create":
Expand All @@ -72,6 +73,13 @@ function commandToAggregateRef(command: OrchestrationCommand): {
aggregateKind: "project",
aggregateId: command.projectId,
};
case "task.schedule":
case "task.cancel":
case "task.fire":
return {
aggregateKind: "task",
aggregateId: command.taskId,
};
default:
return {
aggregateKind: "thread",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CheckpointReactor } from "../Services/CheckpointReactor.ts";
import { ProviderCommandReactor } from "../Services/ProviderCommandReactor.ts";
import { ProviderRuntimeIngestionService } from "../Services/ProviderRuntimeIngestion.ts";
import { ThreadDeletionReactor } from "../Services/ThreadDeletionReactor.ts";
import { TaskFireReactor } from "../Services/TaskFireReactor.ts";
import { OrchestrationReactor } from "../Services/OrchestrationReactor.ts";
import { makeOrchestrationReactor } from "./OrchestrationReactor.ts";
import * as AgentAwarenessRelay from "../../relay/AgentAwarenessRelay.ts";
Expand All @@ -23,7 +24,7 @@ describe("OrchestrationReactor", () => {
runtime = null;
});

it("starts provider ingestion, provider command, checkpoint, and thread deletion reactors", async () => {
it("starts provider ingestion, provider command, checkpoint, thread deletion, and task fire reactors", async () => {
const started: string[] = [];

runtime = ManagedRuntime.make(
Expand Down Expand Up @@ -64,6 +65,15 @@ describe("OrchestrationReactor", () => {
drain: Effect.void,
}),
),
Layer.provideMerge(
Layer.succeed(TaskFireReactor, {
start: () => {
started.push("task-fire-reactor");
return Effect.void;
},
drain: Effect.void,
}),
),
Layer.provideMerge(
Layer.succeed(AgentAwarenessRelay.AgentAwarenessRelay, {
publishThread: () => Effect.void,
Expand All @@ -85,6 +95,7 @@ describe("OrchestrationReactor", () => {
"provider-command-reactor",
"checkpoint-reactor",
"thread-deletion-reactor",
"task-fire-reactor",
"agent-awareness-relay",
]);

Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/orchestration/Layers/OrchestrationReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { CheckpointReactor } from "../Services/CheckpointReactor.ts";
import { ProviderCommandReactor } from "../Services/ProviderCommandReactor.ts";
import { ProviderRuntimeIngestionService } from "../Services/ProviderRuntimeIngestion.ts";
import { TaskFireReactor } from "../Services/TaskFireReactor.ts";
import { ThreadDeletionReactor } from "../Services/ThreadDeletionReactor.ts";
import * as AgentAwarenessRelay from "../../relay/AgentAwarenessRelay.ts";

Expand All @@ -16,13 +17,15 @@ export const makeOrchestrationReactor = Effect.gen(function* () {
const providerCommandReactor = yield* ProviderCommandReactor;
const checkpointReactor = yield* CheckpointReactor;
const threadDeletionReactor = yield* ThreadDeletionReactor;
const taskFireReactor = yield* TaskFireReactor;
const agentAwarenessRelay = yield* AgentAwarenessRelay.AgentAwarenessRelay;

const start: OrchestrationReactorShape["start"] = Effect.fn("start")(function* () {
yield* providerRuntimeIngestion.start();
yield* providerCommandReactor.start();
yield* checkpointReactor.start();
yield* threadDeletionReactor.start();
yield* taskFireReactor.start();
yield* agentAwarenessRelay.start();
});

Expand Down
76 changes: 76 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import {
type ChatAttachment,
type OrchestrationEvent,
type OrchestrationSessionStatus,
TaskScheduleSpec,
ThreadId,
} from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import * as SqlClient from "effect/unstable/sql/SqlClient";

Expand All @@ -34,6 +36,7 @@ import {
ProjectionTurnRepository,
} from "../../persistence/Services/ProjectionTurns.ts";
import { ProjectionThreadRepository } from "../../persistence/Services/ProjectionThreads.ts";
import { ProjectionTaskRepository } from "../../persistence/Services/ProjectionTasks.ts";
import { ProjectionPendingApprovalRepositoryLive } from "../../persistence/Layers/ProjectionPendingApprovals.ts";
import { ProjectionProjectRepositoryLive } from "../../persistence/Layers/ProjectionProjects.ts";
import { ProjectionStateRepositoryLive } from "../../persistence/Layers/ProjectionState.ts";
Expand All @@ -43,6 +46,7 @@ import { ProjectionThreadProposedPlanRepositoryLive } from "../../persistence/La
import { ProjectionThreadSessionRepositoryLive } from "../../persistence/Layers/ProjectionThreadSessions.ts";
import { ProjectionTurnRepositoryLive } from "../../persistence/Layers/ProjectionTurns.ts";
import { ProjectionThreadRepositoryLive } from "../../persistence/Layers/ProjectionThreads.ts";
import { ProjectionTaskRepositoryLive } from "../../persistence/Layers/ProjectionTasks.ts";
import { ServerConfig } from "../../config.ts";
import {
OrchestrationProjectionPipeline,
Expand All @@ -55,6 +59,8 @@ import {
toSafeThreadAttachmentSegment,
} from "../../attachmentStore.ts";

const encodeTaskScheduleJson = Schema.encodeEffect(Schema.fromJsonString(TaskScheduleSpec));

export const ORCHESTRATION_PROJECTOR_NAMES = {
projects: "projection.projects",
threads: "projection.threads",
Expand All @@ -65,6 +71,7 @@ export const ORCHESTRATION_PROJECTOR_NAMES = {
threadTurns: "projection.thread-turns",
checkpoints: "projection.checkpoints",
pendingApprovals: "projection.pending-approvals",
tasks: "projection.tasks",
} as const;

type ProjectorName =
Expand Down Expand Up @@ -480,6 +487,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
const projectionThreadSessionRepository = yield* ProjectionThreadSessionRepository;
const projectionTurnRepository = yield* ProjectionTurnRepository;
const projectionPendingApprovalRepository = yield* ProjectionPendingApprovalRepository;
const projectionTaskRepository = yield* ProjectionTaskRepository;

const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
Expand Down Expand Up @@ -1606,6 +1614,69 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
}
});

const applyTasksProjection: ProjectorDefinition["apply"] = Effect.fn("applyTasksProjection")(
function* (event, _attachmentSideEffects) {
switch (event.type) {
case "task.scheduled": {
// The payload was schema-decoded off the wire, so encoding cannot
// fail here; a failure is a defect, not a projection error.
const scheduleJson = yield* encodeTaskScheduleJson(event.payload.schedule).pipe(
Effect.orDie,
);
yield* projectionTaskRepository.upsert({
taskId: event.payload.taskId,
projectId: event.payload.projectId,
threadId: event.payload.threadId,
name: event.payload.name ?? null,
prompt: event.payload.prompt,
scheduleJson,
createdAt: event.payload.createdAt,
updatedAt: event.payload.updatedAt,
lastFiredAt: null,
nextFireAt: event.payload.nextFireAt,
cancelledAt: null,
});
return;
}

case "task.fired": {
const existingRow = yield* projectionTaskRepository.getById({
taskId: event.payload.taskId,
});
if (Option.isNone(existingRow)) {
return;
}
yield* projectionTaskRepository.upsert({
...existingRow.value,
lastFiredAt: event.payload.firedAt,
nextFireAt: event.payload.nextFireAt,
updatedAt: event.payload.updatedAt,
});
return;
}

case "task.cancelled": {
const existingRow = yield* projectionTaskRepository.getById({
taskId: event.payload.taskId,
});
if (Option.isNone(existingRow)) {
return;
}
yield* projectionTaskRepository.upsert({
...existingRow.value,
nextFireAt: null,
cancelledAt: event.payload.cancelledAt,
updatedAt: event.payload.updatedAt,
});
return;
}

default:
return;
}
},
);

const projectors: ReadonlyArray<ProjectorDefinition> = [
{
name: ORCHESTRATION_PROJECTOR_NAMES.projects,
Expand Down Expand Up @@ -1643,6 +1714,10 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
name: ORCHESTRATION_PROJECTOR_NAMES.threads,
apply: applyThreadsProjection,
},
{
name: ORCHESTRATION_PROJECTOR_NAMES.tasks,
apply: applyTasksProjection,
},
];

const runProjectorForEvent = Effect.fn("runProjectorForEvent")(function* (
Expand Down Expand Up @@ -1746,4 +1821,5 @@ export const OrchestrationProjectionPipelineLive = Layer.effect(
Layer.provideMerge(ProjectionTurnRepositoryLive),
Layer.provideMerge(ProjectionPendingApprovalRepositoryLive),
Layer.provideMerge(ProjectionStateRepositoryLive),
Layer.provideMerge(ProjectionTaskRepositoryLive),
);
Loading
Loading