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
19 changes: 18 additions & 1 deletion apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import type * as AcpSchema from "effect-acp/schema";
const requestLogPath = process.env.T3_ACP_REQUEST_LOG_PATH;
const exitLogPath = process.env.T3_ACP_EXIT_LOG_PATH;
const emitToolCalls = process.env.T3_ACP_EMIT_TOOL_CALLS === "1";
const emitToolCallsOnPermissionPhrase =
process.env.T3_ACP_EMIT_TOOL_CALLS_ON_PERMISSION_PHRASE === "1";
const emitInterleavedAssistantToolCalls =
process.env.T3_ACP_EMIT_INTERLEAVED_ASSISTANT_TOOL_CALLS === "1";
const emitGenericToolPlaceholders = process.env.T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS === "1";
Expand All @@ -22,6 +24,7 @@ const emitXAiAskUserQuestion = process.env.T3_ACP_EMIT_XAI_ASK_USER_QUESTION ===
const emitCreatePlan = process.env.T3_ACP_EMIT_CREATE_PLAN === "1";
const emitUpdateTodos = process.env.T3_ACP_EMIT_UPDATE_TODOS === "1";
const failPrompt = process.env.T3_ACP_FAIL_PROMPT === "1";
const hangPromptWithXAiComplete = process.env.T3_ACP_HANG_PROMPT_WITH_XAI_COMPLETE === "1";
const failSetConfigOption = process.env.T3_ACP_FAIL_SET_CONFIG_OPTION === "1";
const exitOnSetConfigOption = process.env.T3_ACP_EXIT_ON_SET_CONFIG_OPTION === "1";
const promptResponseText = process.env.T3_ACP_PROMPT_RESPONSE_TEXT;
Expand Down Expand Up @@ -330,6 +333,9 @@ const program = Effect.gen(function* () {
yield* agent.handlePrompt((request) =>
Effect.gen(function* () {
const requestedSessionId = String(request.sessionId ?? sessionId);
const promptText = request.prompt
.map((block) => (block.type === "text" ? block.text : ""))
.join("\n");

if (failPrompt) {
return yield* AcpError.AcpRequestError.invalidParams("Mock prompt failure", {
Expand All @@ -338,6 +344,14 @@ const program = Effect.gen(function* () {
});
}

if (hangPromptWithXAiComplete) {
yield* agent.client.extNotification("_x.ai/session/prompt_complete", {
sessionId: requestedSessionId,
stopReason: "end_turn",
});
return yield* Effect.never;
}

if (emitInterleavedAssistantToolCalls) {
const toolCallId = "tool-call-1";

Expand Down Expand Up @@ -388,7 +402,10 @@ const program = Effect.gen(function* () {
return { stopReason: "end_turn" };
}

if (emitToolCalls) {
if (
emitToolCalls &&
(!emitToolCallsOnPermissionPhrase || promptText.includes("trigger permission"))
) {
const toolCallId = "tool-call-1";

yield* agent.client.sessionUpdate({
Expand Down
13 changes: 4 additions & 9 deletions apps/server/src/access/ServerExposure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,7 @@ describe("ServerExposure", () => {
);

it.effect("reports Tailscale Serve disabled when startup configure fails", () => {
const commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> =
[];
const commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> = [];

return Effect.scoped(
Effect.gen(function* () {
Expand Down Expand Up @@ -232,14 +231,10 @@ describe("ServerExposure", () => {
const exposure = yield* ServerExposure;
const endpoints = yield* exposure.getAdvertisedEndpoints;

expect(endpoints.map((endpoint) => endpoint.httpBaseUrl)).toContain(
"http://127.0.0.1:3773/",
);
expect(endpoints.map((endpoint) => endpoint.httpBaseUrl)).toContain("http://127.0.0.1:3773/");
expect(endpoints.some((endpoint) => endpoint.label === "This machine")).toBe(true);
expect(endpoints.some((endpoint) => endpoint.label === "Tailscale HTTPS")).toBe(true);
expect(
endpoints.find((endpoint) => endpoint.label === "Tailscale HTTPS"),
).toMatchObject({
expect(endpoints.find((endpoint) => endpoint.label === "Tailscale HTTPS")).toMatchObject({
httpBaseUrl: "https://desktop.tail.ts.net/",
status: "unavailable",
});
Expand Down Expand Up @@ -300,4 +295,4 @@ describe("ServerExposure", () => {
),
);
});
});
});
250 changes: 241 additions & 9 deletions apps/server/src/provider/Layers/GrokBuildAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,10 @@ const grokPermissionAdapterTestLayer = it.layer(
GrokBuildAdapter,
Effect.gen(function* () {
const wrapperPath = yield* Effect.promise(() =>
makeMockAgentWrapper({ T3_ACP_EMIT_TOOL_CALLS: "1" }),
makeMockAgentWrapper({
T3_ACP_EMIT_TOOL_CALLS: "1",
T3_ACP_EMIT_TOOL_CALLS_ON_PERMISSION_PHRASE: "1",
}),
);
const settings = decodeGrokBuildSettings({
enabled: true,
Expand Down Expand Up @@ -433,8 +436,14 @@ grokPermissionAdapterTestLayer("GrokBuildAdapter permissions", (it) => {
Stream.runCollect,
Effect.forkChild,
);
const turnCompletedFiber = yield* Stream.take(adapter.streamEvents, 20).pipe(
Stream.filter((event) => event.type === "turn.completed"),
Stream.take(1),
Stream.runCollect,
Effect.forkChild,
);

yield* adapter.sendTurn({
const turn = yield* adapter.sendTurn({
threadId,
input: "trigger permission",
attachments: [],
Expand All @@ -443,15 +452,238 @@ grokPermissionAdapterTestLayer("GrokBuildAdapter permissions", (it) => {
const openedEvents = Array.from(yield* Fiber.join(requestOpenedFiber));
const opened = openedEvents[0];
assert.isDefined(opened);
if (opened?.type === "request.opened" && opened.requestId) {
yield* adapter.respondToRequest(
threadId,
ApprovalRequestId.make(String(opened.requestId)),
"accept",
);

yield* adapter.interruptTurn(threadId, turn.turnId);

const completedEvents = Array.from(yield* Fiber.join(turnCompletedFiber));
const completed = completedEvents[0];
assert.equal(completed?.type, "turn.completed");
if (completed?.type === "turn.completed") {
assert.equal(completed.turnId, turn.turnId);
assert.equal(completed.payload.state, "cancelled");
assert.equal(completed.payload.stopReason, "cancelled");
}

yield* adapter.interruptTurn(threadId);
const session = (yield* adapter.listSessions()).find(
(candidate) => candidate.threadId === threadId,
);
assert.equal(session?.status, "ready");
assert.isUndefined(session?.activeTurnId);

yield* adapter.stopSession(threadId);
}),
);

it.effect("completes a later turn after interrupting a blocked permission turn", () =>
Effect.gen(function* () {
const adapter = yield* GrokBuildAdapter;
const threadId = ThreadId.make("grok-permission-interrupt-next-turn-thread");
yield* adapter.startSession({
threadId,
provider: ProviderDriverKind.make("grok-build"),
cwd: process.cwd(),
runtimeMode: "approval-required",
modelSelection: {
instanceId: ProviderInstanceId.make("grok-build-test"),
model: "default",
},
});

const requestOpenedFiber = yield* Stream.take(adapter.streamEvents, 20).pipe(
Stream.filter((event) => event.type === "request.opened"),
Stream.take(1),
Stream.runDrain,
Effect.forkChild,
);
const completedFiber = yield* Stream.take(adapter.streamEvents, 40).pipe(
Stream.filter((event) => event.type === "turn.completed"),
Stream.take(2),
Stream.runCollect,
Effect.forkChild,
);

const interruptedTurn = yield* adapter.sendTurn({
threadId,
input: "trigger permission",
attachments: [],
});

yield* Fiber.join(requestOpenedFiber);
yield* adapter.interruptTurn(threadId, interruptedTurn.turnId);

const nextTurn = yield* adapter.sendTurn({
threadId,
input: "plain follow-up after interrupt",
attachments: [],
});

const completedEvents = Array.from(yield* Fiber.join(completedFiber));
assert.equal(completedEvents.length, 2);
const [cancelled, completed] = completedEvents;
assert.equal(cancelled?.type, "turn.completed");
assert.equal(completed?.type, "turn.completed");
if (cancelled?.type === "turn.completed") {
assert.equal(cancelled.turnId, interruptedTurn.turnId);
assert.equal(cancelled.payload.state, "cancelled");
}
if (completed?.type === "turn.completed") {
assert.equal(completed.turnId, nextTurn.turnId);
assert.equal(completed.payload.state, "completed");
assert.equal(completed.payload.stopReason, "end_turn");
}

const session = (yield* adapter.listSessions()).find(
(candidate) => candidate.threadId === threadId,
);
assert.equal(session?.status, "ready");
assert.isUndefined(session?.activeTurnId);

yield* adapter.stopSession(threadId);
}),
);
});

const grokPromptCompleteAdapterTestLayer = it.layer(
Layer.effect(
GrokBuildAdapter,
Effect.gen(function* () {
const wrapperPath = yield* Effect.promise(() =>
makeMockAgentWrapper({ T3_ACP_HANG_PROMPT_WITH_XAI_COMPLETE: "1" }),
);
const settings = decodeGrokBuildSettings({
enabled: true,
command: wrapperPath,
args: [],
envJson: "{}",
customModels: [],
});
return yield* makeGrokBuildAdapter(settings, {
instanceId: ProviderInstanceId.make("grok-build-test"),
});
}),
).pipe(
Layer.provideMerge(
ServerConfig.layerTest(process.cwd(), {
prefix: "t3code-grok-prompt-complete-test-",
}),
),
Layer.provideMerge(NodeServices.layer),
),
);

grokPromptCompleteAdapterTestLayer("GrokBuildAdapter xAI prompt completion", (it) => {
it.effect("settles a turn from prompt_complete when session/prompt remains pending", () =>
Effect.gen(function* () {
const adapter = yield* GrokBuildAdapter;
const threadId = ThreadId.make("grok-prompt-complete-thread");
const turnCompletedFiber = yield* Stream.take(adapter.streamEvents, 20).pipe(
Stream.filter((event) => event.type === "turn.completed"),
Stream.take(1),
Stream.runCollect,
Effect.forkChild,
);

yield* adapter.startSession({
threadId,
provider: ProviderDriverKind.make("grok-build"),
cwd: process.cwd(),
runtimeMode: "full-access",
modelSelection: {
instanceId: ProviderInstanceId.make("grok-build-test"),
model: "default",
},
});

const turn = yield* adapter.sendTurn({
threadId,
input: "complete through xai notification",
attachments: [],
});

const completedEvents = Array.from(yield* Fiber.join(turnCompletedFiber));
const completed = completedEvents[0];
assert.equal(completed?.type, "turn.completed");
if (completed?.type === "turn.completed") {
assert.equal(completed.turnId, turn.turnId);
assert.equal(completed.payload.state, "completed");
assert.equal(completed.payload.stopReason, "end_turn");
}

const session = (yield* adapter.listSessions()).find(
(candidate) => candidate.threadId === threadId,
);
assert.equal(session?.status, "ready");
assert.isUndefined(session?.activeTurnId);

yield* adapter.stopSession(threadId);
}),
);

it.effect("completes a later turn after prompt_complete settlement", () =>
Effect.gen(function* () {
const adapter = yield* GrokBuildAdapter;
const threadId = ThreadId.make("grok-prompt-complete-next-turn-thread");
const firstCompletedFiber = yield* Stream.take(adapter.streamEvents, 40).pipe(
Stream.filter((event) => event.type === "turn.completed"),
Stream.take(1),
Stream.runCollect,
Effect.forkChild,
);

yield* adapter.startSession({
threadId,
provider: ProviderDriverKind.make("grok-build"),
cwd: process.cwd(),
runtimeMode: "full-access",
modelSelection: {
instanceId: ProviderInstanceId.make("grok-build-test"),
model: "default",
},
});

const firstTurn = yield* adapter.sendTurn({
threadId,
input: "complete through xai notification",
attachments: [],
});

const firstCompletedEvents = Array.from(yield* Fiber.join(firstCompletedFiber));
const firstCompleted = firstCompletedEvents[0];
assert.equal(firstCompleted?.type, "turn.completed");
if (firstCompleted?.type === "turn.completed") {
assert.equal(firstCompleted.turnId, firstTurn.turnId);
assert.equal(firstCompleted.payload.state, "completed");
assert.equal(firstCompleted.payload.stopReason, "end_turn");
}

const secondCompletedFiber = yield* Stream.take(adapter.streamEvents, 40).pipe(
Stream.filter((event) => event.type === "turn.completed"),
Stream.take(1),
Stream.runCollect,
Effect.forkChild,
);

const nextTurn = yield* adapter.sendTurn({
threadId,
input: "plain follow-up after prompt_complete",
attachments: [],
});

const secondCompletedEvents = Array.from(yield* Fiber.join(secondCompletedFiber));
const secondCompleted = secondCompletedEvents[0];
assert.equal(secondCompleted?.type, "turn.completed");
if (secondCompleted?.type === "turn.completed") {
assert.equal(secondCompleted.turnId, nextTurn.turnId);
assert.equal(secondCompleted.payload.state, "completed");
assert.equal(secondCompleted.payload.stopReason, "end_turn");
}

const session = (yield* adapter.listSessions()).find(
(candidate) => candidate.threadId === threadId,
);
assert.equal(session?.status, "ready");
assert.isUndefined(session?.activeTurnId);

yield* adapter.stopSession(threadId);
}),
);
Expand Down
Loading
Loading