Skip to content
Closed
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
18 changes: 16 additions & 2 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import {
CommandId,
EventId,
type OrchestrationEvent,
ProviderKind,
ProviderSessionStartInput,
type ProviderModelOptions,
type ProviderKind,
type ProviderServiceTier,
type OrchestrationSession,
ThreadId,
Expand Down Expand Up @@ -203,6 +204,7 @@ const make = Effect.gen(function* () {
readonly model?: string;
readonly modelOptions?: ProviderModelOptions;
readonly serviceTier?: ProviderServiceTier | null;
readonly providerOptions?: ProviderSessionStartInput["providerOptions"];
},
) {
const readModel = yield* orchestrationEngine.getReadModel();
Expand All @@ -213,7 +215,11 @@ const make = Effect.gen(function* () {

const desiredRuntimeMode = thread.runtimeMode;
const currentProvider: ProviderKind | undefined =
thread.session?.providerName === "codex" ? thread.session.providerName : undefined;
thread.session?.providerName !== null &&
thread.session?.providerName !== undefined &&
Schema.is(ProviderKind)(thread.session.providerName)
? thread.session.providerName
: undefined;
const preferredProvider: ProviderKind | undefined = options?.provider ?? currentProvider;
const desiredModel = options?.model ?? thread.model;
const effectiveCwd = resolveThreadWorkspaceCwd({
Expand All @@ -239,6 +245,9 @@ const make = Effect.gen(function* () {
...(desiredModel ? { model: desiredModel } : {}),
...(options?.serviceTier !== undefined ? { serviceTier: options.serviceTier } : {}),
...(options?.modelOptions !== undefined ? { modelOptions: options.modelOptions } : {}),
...(options?.providerOptions !== undefined
? { providerOptions: options.providerOptions }
: {}),
...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}),
runtimeMode: desiredRuntimeMode,
});
Expand Down Expand Up @@ -325,6 +334,7 @@ const make = Effect.gen(function* () {
readonly model?: string;
readonly serviceTier?: ProviderServiceTier | null;
readonly modelOptions?: ProviderModelOptions;
readonly providerOptions?: ProviderSessionStartInput["providerOptions"];
readonly interactionMode?: "default" | "plan";
readonly createdAt: string;
}) {
Expand All @@ -337,6 +347,7 @@ const make = Effect.gen(function* () {
...(input.model !== undefined ? { model: input.model } : {}),
...(input.serviceTier !== undefined ? { serviceTier: input.serviceTier } : {}),
...(input.modelOptions !== undefined ? { modelOptions: input.modelOptions } : {}),
...(input.providerOptions !== undefined ? { providerOptions: input.providerOptions } : {}),
});
const normalizedInput = toNonEmptyProviderInput(input.messageText);
const normalizedAttachments = input.attachments ?? [];
Expand Down Expand Up @@ -472,6 +483,9 @@ const make = Effect.gen(function* () {
...(event.payload.model !== undefined ? { model: event.payload.model } : {}),
...(event.payload.serviceTier !== undefined ? { serviceTier: event.payload.serviceTier } : {}),
...(event.payload.modelOptions !== undefined ? { modelOptions: event.payload.modelOptions } : {}),
...(event.payload.providerOptions !== undefined
? { providerOptions: event.payload.providerOptions }
: {}),
interactionMode: event.payload.interactionMode,
createdAt: event.payload.createdAt,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
EventId,
MessageId,
ProjectId,
type ProviderKind,
ProviderItemId,
ThreadId,
TurnId,
Expand Down Expand Up @@ -45,7 +46,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value);
type LegacyProviderRuntimeEvent = {
readonly type: string;
readonly eventId: EventId;
readonly provider: "codex";
readonly provider: ProviderKind;
readonly createdAt: string;
readonly threadId: ThreadId;
readonly turnId?: string | undefined;
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/orchestration/decider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,9 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
...(command.model !== undefined ? { model: command.model } : {}),
...(command.serviceTier !== undefined ? { serviceTier: command.serviceTier } : {}),
...(command.modelOptions !== undefined ? { modelOptions: command.modelOptions } : {}),
...(command.providerOptions !== undefined
? { providerOptions: command.providerOptions }
: {}),
assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE,
runtimeMode:
readModel.threads.find((entry) => entry.id === command.threadId)?.runtimeMode ??
Expand Down
70 changes: 70 additions & 0 deletions apps/server/src/provider/Layers/CursorAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { EventEmitter } from "node:events";
import { PassThrough } from "node:stream";
import { type ChildProcess, spawn } from "node:child_process";

import { ThreadId } from "@t3tools/contracts";
import { it, assert } from "@effect/vitest";
import { Effect, Stream } from "effect";

import { CursorAdapter } from "../Services/CursorAdapter.ts";
import { makeCursorAdapterLive } from "./CursorAdapter.ts";

function makeFakeSpawn(): typeof spawn {
return ((command: string) => {
assert.equal(command.includes("cursor"), true);

const stdout = new PassThrough();
const stderr = new PassThrough();
const child = new EventEmitter() as ChildProcess;
child.stdout = stdout;
child.stderr = stderr;
child.stdin = null;
child.kill = () => true;

queueMicrotask(() => {
stdout.write('{"type":"assistant","message":{"content":[{"text":"Hello from Cursor"}]}}\n');
stdout.write('{"type":"result","result":"Hello from Cursor","session_id":"sess-cursor-1"}\n');
stdout.end();
child.emit("close", 0, null);
});

return child;
}) as typeof spawn;
}

const layer = it.layer(makeCursorAdapterLive({ spawnProcess: makeFakeSpawn() }));

layer("CursorAdapterLive", (it) => {
it.effect("starts a session and maps stream-json output into runtime events", () =>
Effect.gen(function* () {
const adapter = yield* CursorAdapter;
const threadId = ThreadId.makeUnsafe("thread-cursor");

const session = yield* adapter.startSession({
threadId,
provider: "cursor",
cwd: process.cwd(),
model: "gpt-5",
runtimeMode: "full-access",
});
assert.equal(session.provider, "cursor");
assert.equal(session.status, "ready");

yield* adapter.sendTurn({
threadId,
input: "Say hello",
interactionMode: "default",
});

const events = yield* Stream.take(adapter.streamEvents, 5).pipe(Stream.runCollect);
const eventTypes = Array.from(events).map((event) => event.type);
assert.deepEqual(eventTypes, [
"session.started",
"session.state.changed",
"turn.started",
"content.delta",
"turn.completed",
]);
}),
);
});
Loading