Skip to content
Merged
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
42 changes: 42 additions & 0 deletions apps/server/src/provider/Layers/CursorAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,48 @@ const cursorAdapterTestLayer = it.layer(
);

cursorAdapterTestLayer("CursorAdapterLive", (it) => {
it.effect("enables ACP protocol logging from Cursor settings", () =>
Effect.gen(function* () {
const settings = yield* ServerSettingsService;
const wrapperPath = yield* Effect.promise(() => makeMockAgentWrapper());
yield* settings.updateSettings({
providers: {
cursor: { binaryPath: wrapperPath, verboseProtocolLogging: true },
},
});
const protocolLogged = yield* Deferred.make<void>();
const resolveSettings = yield* makeResolveCursorSettings;
const adapter = yield* makeCursorAdapter(decodeCursorSettings({}), {
resolveSettings,
nativeEventLogger: {
filePath: "memory://cursor-native-events",
write: (record: unknown) =>
typeof record === "object" &&
record !== null &&
"event" in record &&
typeof record.event === "object" &&
record.event !== null &&
"kind" in record.event &&
record.event.kind === "protocol"
? Deferred.succeed(protocolLogged, undefined).pipe(Effect.asVoid)
: Effect.void,
close: () => Effect.void,
},
});
const threadId = ThreadId.make("cursor-verbose-protocol-log");

yield* adapter.startSession({
threadId,
provider: ProviderDriverKind.make("cursor"),
cwd: process.cwd(),
runtimeMode: "full-access",
modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "default" },
});
yield* Deferred.await(protocolLogged);
yield* adapter.stopSession(threadId);
}),
);

it.effect("starts a session and maps mock ACP prompt flow to runtime events", () =>
Effect.gen(function* () {
const adapter = yield* CursorAdapter;
Expand Down
12 changes: 6 additions & 6 deletions apps/server/src/provider/Layers/CursorAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,12 +514,6 @@ export function makeCursorAdapter(
let ctx!: CursorSessionContext;

const resumeSessionId = parseCursorResume(input.resumeCursor)?.sessionId;
const acpNativeLoggers = makeAcpNativeLoggers({
nativeEventLogger,
provider: PROVIDER,
threadId: input.threadId,
});

// Resolve the CursorSettings used to spawn the ACP child. Production
// leaves `options.resolveSettings` undefined so we use the value
// captured at adapter construction — per-instance isolation is
Expand All @@ -531,6 +525,12 @@ export function makeCursorAdapter(
const effectiveCursorSettings = options?.resolveSettings
? yield* options.resolveSettings
: cursorSettings;
const acpNativeLoggers = makeAcpNativeLoggers({
nativeEventLogger,
provider: PROVIDER,
threadId: input.threadId,
verboseProtocolLogging: effectiveCursorSettings.verboseProtocolLogging,
});

const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId);
const mcpServers = [
Expand Down
5 changes: 5 additions & 0 deletions apps/server/src/provider/Layers/CursorProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ const baseCursorSettings: CursorSettings = {
binaryPath: "cursor-agent",
apiEndpoint: "",
customModels: [],
verboseProtocolLogging: false,
};
const cursorAcpDiscoveryFailedMessage = [
"Cursor ACP model discovery failed.",
Expand Down Expand Up @@ -435,6 +436,7 @@ describe("checkCursorProviderStatus", () => {
binaryPath: missingCursorBinaryPath,
apiEndpoint: "",
customModels: [],
verboseProtocolLogging: false,
}),
);

Expand All @@ -456,6 +458,7 @@ describe("checkCursorProviderStatus", () => {
binaryPath: wrapperPath,
apiEndpoint: "",
customModels: [],
verboseProtocolLogging: false,
},
{
...process.env,
Expand Down Expand Up @@ -484,6 +487,7 @@ describe("discoverCursorModelsViaAcp", () => {
binaryPath: wrapperPath,
apiEndpoint: "",
customModels: [],
verboseProtocolLogging: false,
}).pipe(Effect.scoped),
);

Expand All @@ -506,6 +510,7 @@ describe("discoverCursorModelsViaAcp", () => {
binaryPath: wrapperPath,
apiEndpoint: "",
customModels: [],
verboseProtocolLogging: false,
}),
);

Expand Down
44 changes: 42 additions & 2 deletions apps/server/src/provider/Layers/EventNdjsonLogger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ describe("EventNdjsonLogger", () => {
}),
);

it.effect("drops transient canonical events before serialization", () =>
it.effect("drops transient provider events before serialization", () =>
Effect.gen(function* () {
const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-provider-log-"));
const basePath = NodePath.join(tempDir, "events.log");
Expand All @@ -334,6 +334,46 @@ describe("EventNdjsonLogger", () => {
yield* canonical.write(circularDelta, threadId);
yield* canonical.write({ type: "item.completed", id: "final" }, threadId);
yield* native.write({ type: "content.delta", id: "native-delta" }, threadId);
yield* native.write(
{ method: "item/agentMessage/delta", payload: circularDelta },
threadId,
);
yield* native.write(
{ method: "thread/realtime/outputAudio/delta", payload: circularDelta },
threadId,
);
yield* native.write(
{ method: "thread/realtime/transcript/delta", payload: circularDelta },
threadId,
);
yield* native.write(
{
event: {
method: "claude/stream_event/content_block_delta/text_delta",
payload: circularDelta,
},
},
threadId,
);
yield* native.write(
{
event: {
method: "session/update",
payload: { update: { sessionUpdate: "agent_message_chunk" } },
},
},
threadId,
);
yield* native.write(
{
event: {
type: "message.part.updated",
payload: { properties: { part: { type: "text" } } },
},
},
threadId,
);
yield* native.write({ type: "turn.completed", id: "native-final" }, threadId);
yield* store.close();

const lines = NodeFS.readFileSync(ownedLogPath(basePath, "thread-filtered"), "utf8")
Expand All @@ -345,7 +385,7 @@ describe("EventNdjsonLogger", () => {
lines.map(({ stream, payload }) => ({ stream, payload })),
[
{ stream: "CANON", payload: '{"type":"item.completed","id":"final"}' },
{ stream: "NTIVE", payload: '{"type":"content.delta","id":"native-delta"}' },
{ stream: "NTIVE", payload: '{"type":"turn.completed","id":"native-final"}' },
],
);
} finally {
Expand Down
61 changes: 54 additions & 7 deletions apps/server/src/provider/Layers/EventNdjsonLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ const transientCanonicalEventTypes = new Set([
"tool.progress",
"turn.proposed.delta",
]);
const transientNativeMethods = new Set([
"item/agentMessage/delta",
"item/commandExecution/outputDelta",
"item/fileChange/outputDelta",
"item/plan/delta",
"item/reasoning/summaryTextDelta",
"item/reasoning/textDelta",
"thread/realtime/outputAudio/delta",
"thread/realtime/transcript/delta",
]);
const transientAcpUpdates = new Set(["agent_message_chunk", "agent_thought_chunk"]);

export type EventNdjsonStream = "native" | "canonical" | "orchestration";

Expand Down Expand Up @@ -128,7 +139,7 @@ export interface PendingRecord {
}

interface StoreState {
readonly pending: ReadonlyArray<PendingRecord>;
readonly pending: Array<PendingRecord>;
readonly pendingBytes: number;
readonly sinks: ReadonlyMap<string, RotatingFileSink>;
readonly flushScheduled: boolean;
Expand Down Expand Up @@ -180,12 +191,50 @@ function providerLogPath(directory: string, prefix: string, threadSegment: strin
}

function shouldPersist(stream: EventNdjsonStream, event: unknown): boolean {
if (stream !== "canonical" || typeof event !== "object" || event === null) {
if (stream === "orchestration" || typeof event !== "object" || event === null) {
return true;
}
try {
const type = Reflect.get(event, "type");
return typeof type !== "string" || !transientCanonicalEventTypes.has(type);
if (typeof type === "string" && transientCanonicalEventTypes.has(type)) {
return false;
}
if (stream !== "native") return true;

const nested = Reflect.get(event, "event");
const nativeEvent = typeof nested === "object" && nested !== null ? nested : event;
const method = Reflect.get(nativeEvent, "method");
if (
typeof method === "string" &&
(transientNativeMethods.has(method) ||
method.startsWith("claude/stream_event/content_block_delta/"))
) {
return false;
}

const nativeType = Reflect.get(nativeEvent, "type");
if (nativeType === "message.part.delta") return false;

const payload = Reflect.get(nativeEvent, "payload");
if (typeof payload !== "object" || payload === null) return true;

if (method === "session/update") {
const update = Reflect.get(payload, "update");
if (typeof update !== "object" || update === null) return true;
const updateType = Reflect.get(update, "sessionUpdate");
return typeof updateType !== "string" || !transientAcpUpdates.has(updateType);
}

if (nativeType === "message.part.updated") {
const properties = Reflect.get(payload, "properties");
if (typeof properties !== "object" || properties === null) return true;
const part = Reflect.get(properties, "part");
if (typeof part !== "object" || part === null) return true;
const partType = Reflect.get(part, "type");
return partType !== "text" && partType !== "reasoning";
}

return true;
} catch {
return true;
}
Expand Down Expand Up @@ -598,10 +647,8 @@ export const makeEventNdjsonLogStore = Effect.fnUntraced(function* (
results.push(result);
current = drained;
}
const pending = [
...current.pending,
{ stream, threadSegment: resolveThreadSegment(threadId), line, bytes },
];
const pending = current.pending;
pending.push({ stream, threadSegment: resolveThreadSegment(threadId), line, bytes });
const pendingBytes = current.pendingBytes + bytes;
const flushNow =
resolved.batchWindowMs === 0 ||
Expand Down
36 changes: 36 additions & 0 deletions apps/server/src/provider/Layers/GrokAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,42 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {
}),
);

it.effect("enables ACP protocol logging from Grok settings", () =>
Effect.gen(function* () {
const wrapperPath = yield* Effect.promise(() => makeMockGrokWrapper());
const protocolLogged = yield* Deferred.make<void>();
const adapter = yield* makeGrokAdapter(
decodeGrokSettings({ binaryPath: wrapperPath, verboseProtocolLogging: true }),
{
nativeEventLogger: {
filePath: "memory://grok-native-events",
write: (record: unknown) =>
typeof record === "object" &&
record !== null &&
"event" in record &&
typeof record.event === "object" &&
record.event !== null &&
"kind" in record.event &&
record.event.kind === "protocol"
? Deferred.succeed(protocolLogged, undefined).pipe(Effect.asVoid)
: Effect.void,
close: () => Effect.void,
},
},
).pipe(Effect.orDie);
const threadId = ThreadId.make("grok-verbose-protocol-log");

yield* adapter.startSession({
threadId,
provider: ProviderDriverKind.make("grok"),
cwd: process.cwd(),
runtimeMode: "full-access",
});
yield* Deferred.await(protocolLogged);
yield* adapter.stopSession(threadId);
}),
);

// Production calls startSession from a request fiber that finishes as soon as
// the session exists. `Effect.forkChild` made the notification consumer a
// child of that fiber, and Effect interrupts a fiber's children when it
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/provider/Layers/GrokAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
nativeEventLogger,
provider: PROVIDER,
threadId: input.threadId,
verboseProtocolLogging: grokSettings.verboseProtocolLogging,
});

const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ const makeGrokConfig = (overrides: Partial<GrokSettings>): GrokSettings => ({
enabled: false,
binaryPath: "grok",
customModels: [],
verboseProtocolLogging: false,
...overrides,
});

Expand Down
Loading
Loading