Skip to content
Draft
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
11 changes: 11 additions & 0 deletions apps/mobile/src/components/ProviderIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ export function ProviderIcon(props: ProviderIconProps) {
);
}

if (props.provider === "copilot") {
return (
<Svg width={size} height={size} viewBox="0 0 16 16" fill="none">
<Path
fill={mono}
d="M8 0a8 8 0 0 0-2.53 15.59c.4.07.55-.17.55-.38l-.01-1.49c-2.22.48-2.69-1.07-2.69-1.07-.36-.92-.89-1.17-.89-1.17-.72-.5.06-.49.06-.49.8.06 1.23.82 1.23.82.71 1.21 1.87.86 2.33.66.07-.52.28-.87.5-1.07-1.77-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.42 7.42 0 0 1 4 0c1.53-1.03 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.28.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48l-.01 2.19c0 .21.14.46.55.38A8 8 0 0 0 8 0Z"
/>
</Svg>
);
}

if (props.provider === "cursor") {
return (
<Svg width={size} height={size} viewBox="0 0 466.73 532.09" fill="none">
Expand Down
131 changes: 126 additions & 5 deletions apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ const emitStaleXAiPromptCompleteBeforeSecondHang =
const emitOverlappingXAiPromptCompleteOutOfOrder =
process.env.T3_ACP_EMIT_OVERLAPPING_XAI_PROMPT_COMPLETE_OUT_OF_ORDER === "1";
const failPrompt = process.env.T3_ACP_FAIL_PROMPT === "1";
const emitAvailableCommands = process.env.T3_ACP_EMIT_AVAILABLE_COMMANDS === "1";
const emitTaskToolCall = process.env.T3_ACP_EMIT_TASK_TOOL_CALL === "1";
const emitBackgroundTask = process.env.T3_ACP_EMIT_BACKGROUND_TASK === "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 @@ -310,11 +313,29 @@ const program = Effect.gen(function* () {
yield* agent.handleAuthenticate(() => Effect.succeed({}));

yield* agent.handleCreateSession(() =>
Effect.succeed({
sessionId,
modes: modeState(),
models: modelState(),
configOptions: configOptions(),
Effect.gen(function* () {
if (emitAvailableCommands) {
yield* agent.client.sessionUpdate({
sessionId,
update: {
sessionUpdate: "available_commands_update",
availableCommands: [
{
name: "research",
description: "Deep research on a topic",
input: { hint: "topic to research" },
},
{ name: "plan", description: "Create an implementation plan" },
],
},
});
}
return {
sessionId,
modes: modeState(),
models: modelState(),
configOptions: configOptions(),
};
}),
);

Expand Down Expand Up @@ -630,6 +651,106 @@ const program = Effect.gen(function* () {
return { stopReason: "end_turn" };
}

if (emitBackgroundTask) {
// Mirrors copilot-cli background agents: the launch tool_call is
// followed by an early `end_turn` response, then the agent keeps
// streaming progress and an idle report after the RPC has settled.
yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call",
toolCallId: "task-bg-1",
title: "Wait and write hello",
kind: "other",
status: "pending",
rawInput: {
agent_type: "task",
name: "delayed-hello",
mode: "background",
description: "Wait and write hello",
},
},
});

yield* Effect.forkDetach(
Effect.gen(function* () {
yield* Effect.sleep("150 millis");
yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "hello" },
},
});
yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call",
toolCallId: "wake-read-1",
title: "read_agent",
kind: "read",
status: "pending",
rawInput: { agent_id: "delayed-hello", since_turn: 0 },
},
});
yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: "wake-read-1",
status: "completed",
rawOutput: {
content:
"Agent is idle (waiting for messages). agent_id: delayed-hello, agent_type: task, status: idle, elapsed: 19s",
},
},
});
}),
);

return { stopReason: "end_turn" };
}

if (emitTaskToolCall) {
const taskToolCallId = "task-tool-1";

yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call",
toolCallId: taskToolCallId,
title: "task",
kind: "other",
status: "pending",
rawInput: {
agent: "researcher",
prompt: "explore the codebase",
},
},
});

yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: taskToolCallId,
status: "in_progress",
},
});

yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: taskToolCallId,
status: "completed",
rawOutput: { summary: "research done" },
},
});

return { stopReason: "end_turn" };
}

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

Expand Down
163 changes: 163 additions & 0 deletions apps/server/src/provider/Drivers/CopilotDriver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { CopilotSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
import * as Crypto from "effect/Crypto";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import { HttpClient } from "effect/unstable/http";
import { ChildProcessSpawner } from "effect/unstable/process";

import { ServerConfig } from "../../config.ts";
import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { makeCopilotTextGeneration } from "../../textGeneration/CopilotTextGeneration.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makeCopilotAdapter } from "../Layers/CopilotAdapter.ts";
import {
buildInitialCopilotProviderSnapshot,
checkCopilotProviderStatus,
enrichCopilotSnapshot,
} from "../Layers/CopilotProvider.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import {
defaultProviderContinuationIdentity,
type ProviderDriver,
type ProviderInstance,
} from "../ProviderDriver.ts";
import type { ServerProviderDraft } from "../providerSnapshot.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
makeManualOnlyProviderMaintenanceCapabilities,
makeStaticProviderMaintenanceResolver,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";
import {
haveProviderSnapshotSettingsChanged,
makeProviderSnapshotSettingsSource,
type ProviderSnapshotSettings,
} from "../providerUpdateSettings.ts";
const decodeCopilotSettings = Schema.decodeSync(CopilotSettings);

const DRIVER_KIND = ProviderDriverKind.make("copilot");
const UPDATE = makeStaticProviderMaintenanceResolver(
makeManualOnlyProviderMaintenanceCapabilities({
provider: DRIVER_KIND,
packageName: null,
}),
);

export type CopilotDriverEnv =
| BackgroundPolicy.BackgroundPolicy
| ChildProcessSpawner.ChildProcessSpawner
| Crypto.Crypto
| FileSystem.FileSystem
| HttpClient.HttpClient
| Path.Path
| ProviderEventLoggers
| ServerConfig
| ServerSettingsService;

const withInstanceIdentity =
(input: {
readonly instanceId: ProviderInstance["instanceId"];
readonly displayName: string | undefined;
readonly accentColor: string | undefined;
readonly continuationGroupKey: string;
}) =>
(snapshot: ServerProviderDraft): ServerProvider => ({
...snapshot,
instanceId: input.instanceId,
driver: DRIVER_KIND,
...(input.displayName ? { displayName: input.displayName } : {}),
...(input.accentColor ? { accentColor: input.accentColor } : {}),
continuation: { groupKey: input.continuationGroupKey },
});

export const CopilotDriver: ProviderDriver<CopilotSettings, CopilotDriverEnv> = {
driverKind: DRIVER_KIND,
metadata: {
displayName: "GitHub Copilot",
supportsMultipleInstances: true,
},
configSchema: CopilotSettings,
defaultConfig: (): CopilotSettings => decodeCopilotSettings({}),
create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
Effect.gen(function* () {
const crypto = yield* Crypto.Crypto;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const httpClient = yield* HttpClient.HttpClient;
const serverSettings = yield* ServerSettingsService;
const eventLoggers = yield* ProviderEventLoggers;
const processEnv = mergeProviderInstanceEnvironment(environment);
const continuationIdentity = defaultProviderContinuationIdentity({
driverKind: DRIVER_KIND,
instanceId,
});
const stampIdentity = withInstanceIdentity({
instanceId,
displayName,
accentColor,
continuationGroupKey: continuationIdentity.continuationKey,
});
const effectiveConfig = { ...config, enabled } satisfies CopilotSettings;
const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
binaryPath: effectiveConfig.binaryPath,
env: processEnv,
});

const adapter = yield* makeCopilotAdapter(effectiveConfig, {
environment: processEnv,
...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
instanceId,
});
const textGeneration = yield* makeCopilotTextGeneration(effectiveConfig, processEnv);

const checkProvider = checkCopilotProviderStatus(effectiveConfig, processEnv).pipe(
Effect.map(stampIdentity),
Effect.provideService(Crypto.Crypto, crypto),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
);

const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<CopilotSettings>>({
maintenanceCapabilities,
getSettings: snapshotSettings.getSettings,
streamSettings: snapshotSettings.streamSettings,
haveSettingsChanged: haveProviderSnapshotSettingsChanged,
initialSnapshot: (settings) =>
buildInitialCopilotProviderSnapshot(settings.provider).pipe(Effect.map(stampIdentity)),
checkProvider,
enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
enrichCopilotSnapshot({
snapshot: currentSnapshot,
maintenanceCapabilities,
enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
publishSnapshot,
httpClient,
}),
}).pipe(
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Failed to build GitHub Copilot snapshot: ${cause.message ?? String(cause)}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detail interpolates cause.message / String(cause), and ProviderDriverError.message is derived from detail — so the wrapper message comes from the cause (and can carry arbitrary defect text). Consider keeping a stable, structural detail and letting cause carry the underlying failure.

Suggested change
detail: `Failed to build GitHub Copilot snapshot: ${cause.message ?? String(cause)}`,
detail: "Failed to build the GitHub Copilot provider snapshot.",

Posted via Macroscope — Effect Service Conventions

cause,
}),
),
);

return {
instanceId,
driverKind: DRIVER_KIND,
continuationIdentity,
displayName,
accentColor,
enabled,
snapshot,
adapter,
textGeneration,
} satisfies ProviderInstance;
}),
};
Loading
Loading