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
104 changes: 104 additions & 0 deletions apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const emitXAiAskUserQuestionThenHang =
const emitContentThenHang = process.env.T3_ACP_EMIT_CONTENT_THEN_HANG === "1";
const emitPlanThenHang = process.env.T3_ACP_EMIT_PLAN_THEN_HANG === "1";
const emitActiveToolThenHang = process.env.T3_ACP_EMIT_ACTIVE_TOOL_THEN_HANG === "1";
const emitGrokMonitorPostTurnPoll = process.env.T3_ACP_EMIT_GROK_MONITOR_POST_TURN_POLL === "1";
const emitGrokBackgroundTaskStarted = process.env.T3_ACP_EMIT_GROK_BACKGROUND_TASK_STARTED === "1";
const emitForeignSessionUpdates = process.env.T3_ACP_EMIT_FOREIGN_SESSION_UPDATES === "1";
const emitPrivateThoughtChunk = process.env.T3_ACP_EMIT_PRIVATE_THOUGHT_CHUNK === "1";
const assistantMessageIdMode = process.env.T3_ACP_ASSISTANT_MESSAGE_IDS;
Expand Down Expand Up @@ -935,6 +937,108 @@ const program = Effect.gen(function* () {
return yield* Effect.never;
}

if (emitGrokMonitorPostTurnPoll) {
const monitorCallId = "call-monitor-1";
const pollCallId = "call-monitor-poll-1";
const taskId = "01a05f41-5107-7550-821e-79e8d1cd7687";
const description = "Watch count-sheet Typst unit until done";
writeJsonRpcNotification("session/update", {
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call",
toolCallId: monitorCallId,
title: "monitor",
kind: "other",
status: "pending",
rawInput: { description },
_meta: {
"x.ai/tool": { version: 1, name: "monitor", kind: "task", namespace: "grok_build" },
},
},
});
writeJsonRpcNotification("session/update", {
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: monitorCallId,
status: "completed",
rawInput: { description },
rawOutput: {
type: "Monitor",
taskId,
timeoutMs: 36_000_000,
},
},
});
writeJsonRpcNotification("_x.ai/session/prompt_complete", {
sessionId: requestedSessionId,
promptId: promptIdFromRequestMeta(request) ?? "mock-xai-prompt-1",
stopReason: "end_turn",
agentResult: null,
});
yield* Effect.sleep("120 millis");
writeJsonRpcNotification("session/update", {
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call",
toolCallId: pollCallId,
title: "get_command_or_subagent_output",
kind: "other",
status: "completed",
rawInput: { variant: "TaskOutput", task_ids: [taskId], timeout_ms: 0 },
rawOutput: {
type: "TaskOutput",
Result: {
task_id: taskId,
command: `[monitor] ${description}`,
status: "completed",
exit_code: 0,
output: "Monitor finished.",
},
},
},
});
return yield* Effect.never;
}

if (emitGrokBackgroundTaskStarted) {
const toolCallId = "call-fb9d0000-0000-0000-0000-000000000026";
const command = "sleep 40; echo done-a";
writeJsonRpcNotification("session/update", {
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call",
toolCallId,
title: "run_terminal_command",
kind: "execute",
status: "in_progress",
rawInput: { command },
},
});
writeJsonRpcNotification("session/update", {
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId,
status: "completed",
rawOutput: {
type: "BackgroundTaskStarted",
task_id: toolCallId,
task_type: "bash",
status: "running",
command,
},
},
});
writeJsonRpcNotification("_x.ai/session/prompt_complete", {
sessionId: requestedSessionId,
promptId: promptIdFromRequestMeta(request) ?? "mock-xai-prompt-1",
stopReason: "end_turn",
agentResult: null,
});
return yield* Effect.never;
}

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

Expand Down
64 changes: 62 additions & 2 deletions apps/server/src/provider/Layers/GrokAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,18 @@ import {
TurnId,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";

import { ServerConfig } from "../../config.ts";
import { execScriptSource, writeFakeCli } from "../../testUtils/fakeCli.ts";
import {
grokPromptSettlementBelongsToContext,
isGrokEnterPlanModeToolCall,
makeGrokAdapter,
nextGrokPlanModeActive,
selectGrokPermissionOptionId,
} from "./GrokAdapter.ts";
import { execScriptSource, writeFakeCli } from "../../testUtils/fakeCli.ts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";

const decodeGrokSettings = Schema.decodeSync(GrokSettings);

const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -251,6 +252,65 @@ it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => {
}),
);

for (const taskType of ["monitor", "shell"] as const) {
it.effect(`emits the ${taskType} background lifecycle`, () =>
Effect.gen(function* () {
const threadId = ThreadId.make(`grok-background-${taskType}`);
const wrapperPath = yield* Effect.promise(() =>
makeMockGrokWrapper({
[taskType === "monitor"
? "T3_ACP_EMIT_GROK_MONITOR_POST_TURN_POLL"
: "T3_ACP_EMIT_GROK_BACKGROUND_TASK_STARTED"]: "1",
}),
);
const adapter = yield* makeTestAdapter(wrapperPath);
const events: ProviderRuntimeEvent[] = [];
const finished = yield* Deferred.make<void>();
const eventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) =>
Effect.gen(function* () {
events.push(event);
if (event.type === (taskType === "monitor" ? "task.completed" : "turn.completed")) {
yield* Deferred.succeed(finished, undefined);
}
}),
).pipe(Effect.forkChild);
yield* adapter.startSession({ threadId, cwd: process.cwd(), runtimeMode: "full-access" });
yield* adapter.sendTurn({ threadId, input: "watch the unit" });
yield* Deferred.await(finished).pipe(Effect.timeout("3 seconds"));

const started = events.find((event) => event.type === "task.started");
const taskId =
taskType === "monitor"
? "01a05f41-5107-7550-821e-79e8d1cd7687"
: "call-fb9d0000-0000-0000-0000-000000000026";
assert.equal(started?.payload.taskType, taskType);
assert.equal(started?.payload.taskId, taskId);
if (taskType === "monitor") {
const completed = events.find((event) => event.type === "task.completed");
const turnEnd = events.findIndex((event) => event.type === "turn.completed");
assert.equal(completed?.payload.status, "completed");
assert.equal(completed?.payload.taskId, taskId);
assert.equal(completed?.turnId, undefined);
assert.isAtLeast(turnEnd, 0);
assert.isAbove(
events.findIndex((event) => event.type === "task.completed"),
turnEnd,
);
assert.deepEqual(
events
.slice(turnEnd + 1)
.filter((event) => event.type === "item.updated" || event.type === "item.completed"),
[],
);
} else {
assert.equal(started?.payload.description, "sleep 40; echo done-a");
}
yield* Fiber.interrupt(eventsFiber);
yield* adapter.stopSession(threadId);
}).pipe(TestClock.withLive),
);
}

it.effect("sends runtime context with the current model without changing saved prompts", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("grok-runtime-context");
Expand Down
35 changes: 35 additions & 0 deletions apps/server/src/provider/Layers/GrokAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ import {
normalizeGrokReasoningEffort,
resolveGrokAcpBaseModelId,
} from "../acp/GrokAcpSupport.ts";
import {
buildGrokBackgroundTaskEvents,
type GrokBackgroundTaskRecord,
} from "../acp/XAiBackgroundTasks.ts";
import {
extractGrokPlanMarkdownFromToolCallData,
extractXAiAskUserQuestions,
Expand Down Expand Up @@ -174,6 +178,10 @@ interface GrokSessionContext {
currentModelId: string | undefined;
currentReasoningEffort: string | undefined;
stopped: boolean;
/** Live monitor and shell identities, with their originating turns. */
readonly backgroundTasks: Map<string, GrokBackgroundTaskRecord>;
readonly completedBackgroundTaskIds: Map<string, string | undefined>;
readonly ambiguousBackgroundTaskIds: Set<string>;
}

function settlePendingApprovalsAsCancelled(
Expand Down Expand Up @@ -1337,6 +1345,9 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
? normalizeGrokReasoningEffort(requestedStartReasoningEffort)
: currentStartReasoningEffort,
stopped: false,
backgroundTasks: new Map(),
completedBackgroundTaskIds: new Map(),
ambiguousBackgroundTaskIds: new Set(),
};

const nf = yield* Stream.runDrain(
Expand All @@ -1359,6 +1370,30 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
}

const notificationTurnId = resolveNotificationTurnId(ctx);
if (
event._tag === "ToolCallUpdated" &&
!ctx.stopped &&
(notificationTurnId === undefined ||
!ctx.interruptedTurnIds.has(notificationTurnId))
) {
for (const taskEvent of buildGrokBackgroundTaskEvents({
tasks: ctx.backgroundTasks,
completedTaskIds: ctx.completedBackgroundTaskIds,
ambiguousTaskIds: ctx.ambiguousBackgroundTaskIds,
toolCallId: event.toolCall.toolCallId,
rawInput: event.toolCall.data.rawInput,
rawOutput: event.toolCall.data.rawOutput,
toolCallStatus: event.toolCall.status,
turnId: notificationTurnId,
})) {
yield* offerRuntimeEvent(ctx.sessionIncarnationId, {
...taskEvent,
...(yield* makeEventStamp()),
provider: PROVIDER,
threadId: ctx.threadId,
});
}
}
if (
notificationTurnId === undefined ||
ctx.interruptedTurnIds.has(notificationTurnId)
Expand Down
Loading
Loading