1/**
2 * PiDriver — v1 `ProviderDriver` for the Pi coding agent, composing the
3 * orchestrator-v2 adapter (`PiAdapterV2`), the snapshot/probe layer
4 * (`PiProvider`), and Pi-backed text generation.
5 *
6 * Pi state (sessions, settings, extensions, auth) lives in the user's own
7 * `~/.pi/agent`, so continuation identity uses the default instance grouping.
8 */
9import { ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
10import { PiSettings } from "../settings.ts";
11import * as Effect from "effect/Effect";
12import * as FileSystem from "effect/FileSystem";
13import * as Path from "effect/Path";
14import * as Schema from "effect/Schema";
15import * as HttpClient from "effect/http/HttpClient";
16import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
17
18import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions";
19import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
20import { piUsageReader } from "./usage.ts";
21import { makePiTextGeneration } from "./textGeneration.ts";
22import { PiAdapterV2Driver, type PiAdapterV2DriverEnv } from "./adapter.ts";
23import { ProviderDriverError } from "@t3tools/provider-core/server/errors";
24import {
25 buildInitialPiProviderSnapshot,
26 checkPiProviderStatus,
27 discoverPiCommandsForCwd,
28 enrichPiSnapshot,
29} from "./status.ts";
30import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider";
31import {
32 defaultProviderContinuationIdentity,
33 type ProviderDriver,
34 type ProviderInstance,
35} from "@t3tools/provider-core/server/driver";
36import type { ServerProviderDraft } from "@t3tools/provider-core/server/snapshotProbe";
37import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
38import {
39 makeCachedProviderMaintenanceResolution,
40 makePackageManagedProviderMaintenanceResolver,
41 resolveProviderMaintenanceCapabilitiesEffect,
42} from "@t3tools/provider-core/server/maintenanceResolver";
43import {
44 haveProviderSnapshotSettingsChanged,
45 makeProviderSnapshotSettingsSource,
46 type ProviderSnapshotSettings,
47} from "@t3tools/provider-core/server/snapshotSettings";
48
49const decodePiSettings = Schema.decodeSync(PiSettings);
50
51const DRIVER_KIND = ProviderDriverKind.make("pi");
52const UPDATE = makePackageManagedProviderMaintenanceResolver({
53 provider: DRIVER_KIND,
54 npmPackageName: "@earendil-works/pi-coding-agent",
55 // Pi's updater covers its own installer and npm, pnpm, yarn, and bun globals.
56 nativeUpdate: { args: ["update", "--self"] },
57});
58
59export type PiDriverEnv =
60 | PiAdapterV2DriverEnv
61 | ProviderHost.ProviderHost
62 | ChildProcessSpawner.ChildProcessSpawner
63 | FileSystem.FileSystem
64 | HttpClient.HttpClient
65 | ProviderLatestVersions.ProviderLatestVersions
66 | Path.Path;
67
68const withInstanceIdentity =
69 (input: {
70 readonly instanceId: ProviderInstance["instanceId"];
71 readonly displayName: string | undefined;
72 readonly accentColor: string | undefined;
73 readonly continuationGroupKey: string;
74 }) =>
75 (snapshot: ServerProviderDraft): ServerProvider => ({
76 ...snapshot,
77 instanceId: input.instanceId,
78 driver: DRIVER_KIND,
79 ...(input.displayName ? { displayName: input.displayName } : {}),
80 ...(input.accentColor ? { accentColor: input.accentColor } : {}),
81 continuation: { groupKey: input.continuationGroupKey },
82 });
83
84export const PiDriver: ProviderDriver<PiSettings, PiDriverEnv, Path.Path> = {
85 driverKind: DRIVER_KIND,
86 metadata: {
87 displayName: "Pi",
88 supportsMultipleInstances: true,
89 },
90 usage: piUsageReader,
91 configSchema: PiSettings,
92 defaultConfig: (): PiSettings => decodePiSettings({}),
93 create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
94 Effect.gen(function* () {
95 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
96 const fileSystem = yield* FileSystem.FileSystem;
97 const pathService = yield* Path.Path;
98 const httpClient = yield* HttpClient.HttpClient;
99 const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions;
100 const host = yield* ProviderHost.ProviderHost;
101 const { cwd } = host.paths;
102 const processEnv = yield* mergeProviderInstanceEnvironment(environment);
103 const continuationIdentity = defaultProviderContinuationIdentity({
104 driverKind: DRIVER_KIND,
105 instanceId,
106 });
107 const stampIdentity = withInstanceIdentity({
108 instanceId,
109 displayName,
110 accentColor,
111 continuationGroupKey: continuationIdentity.continuationKey,
112 });
113 const effectiveConfig = { ...config, enabled } satisfies PiSettings;
114 const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution(
115 resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
116 binaryPath: effectiveConfig.binaryPath,
117 env: processEnv,
118 }).pipe(
119 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
120 Effect.provideService(FileSystem.FileSystem, fileSystem),
121 Effect.provideService(Path.Path, pathService),
122 ),
123 );
124
125 const orchestrationAdapter = yield* PiAdapterV2Driver.create({
126 instanceId,
127 displayName,
128 accentColor,
129 environment,
130 enabled,
131 config,
132 }).pipe(
133 Effect.mapError(
134 (cause) =>
135 new ProviderDriverError({
136 driver: DRIVER_KIND,
137 instanceId,
138 detail: "Failed to build Pi orchestration adapter.",
139 cause,
140 }),
141 ),
142 );
143 const textGeneration = yield* makePiTextGeneration(effectiveConfig, processEnv);
144
145 const checkProvider = checkPiProviderStatus(effectiveConfig, processEnv, cwd).pipe(
146 Effect.map(stampIdentity),
147 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
148 );
149
150 const snapshotSettings = yield* makeProviderSnapshotSettingsSource(effectiveConfig);
151 const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<PiSettings>>({
152 resolveMaintenance,
153 getSettings: snapshotSettings.getSettings,
154 streamSettings: snapshotSettings.streamSettings,
155 haveSettingsChanged: haveProviderSnapshotSettingsChanged,
156 initialSnapshot: (settings) =>
157 buildInitialPiProviderSnapshot(settings.provider).pipe(Effect.map(stampIdentity)),
158 checkProvider,
159 enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
160 resolveMaintenance().pipe(
161 Effect.flatMap((maintenanceCapabilities) =>
162 enrichPiSnapshot({
163 snapshot: currentSnapshot,
164 maintenanceCapabilities,
165 enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
166 publishSnapshot,
167 }),
168 ),
169 Effect.provideService(HttpClient.HttpClient, httpClient),
170 Effect.provideService(ProviderLatestVersions.ProviderLatestVersions, latestVersions),
171 ),
172 }).pipe(
173 Effect.mapError(
174 (cause) =>
175 new ProviderDriverError({
176 driver: DRIVER_KIND,
177 instanceId,
178 detail: "Failed to build Pi snapshot.",
179 cause,
180 }),
181 ),
182 );
183
184 return {
185 instanceId,
186 driverKind: DRIVER_KIND,
187 continuationIdentity,
188 displayName,
189 accentColor,
190 enabled,
191 snapshot,
192 snapshotForCwd: (workspaceCwd) =>
193 !effectiveConfig.enabled
194 ? snapshot.getSnapshot
195 : Effect.all([
196 snapshot.getSnapshot,
197 discoverPiCommandsForCwd(effectiveConfig, processEnv, workspaceCwd).pipe(
198 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
199 Effect.mapError(
200 (cause) =>
201 new ProviderDriverError({
202 driver: DRIVER_KIND,
203 instanceId,
204 detail: "Failed to discover Pi workspace commands.",
205 cause,
206 }),
207 ),
208 ),
209 ]).pipe(
210 Effect.map(([machineSnapshot, commands]) => ({ ...machineSnapshot, ...commands })),
211 ),
212 orchestrationAdapter,
213 textGeneration,
214 } satisfies ProviderInstance;
215 }),
216};