| 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 | */ |
| 9 | import { ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; |
| 10 | import { PiSettings } from "../settings.ts"; |
| 11 | import * as Effect from "effect/Effect"; |
| 12 | import * as FileSystem from "effect/FileSystem"; |
| 13 | import * as Path from "effect/Path"; |
| 14 | import * as Schema from "effect/Schema"; |
| 15 | import * as HttpClient from "effect/http/HttpClient"; |
| 16 | import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; |
| 17 | |
| 18 | import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions"; |
| 19 | import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; |
| 20 | import { piUsageReader } from "./usage.ts"; |
| 21 | import { makePiTextGeneration } from "./textGeneration.ts"; |
| 22 | import { PiAdapterV2Driver, type PiAdapterV2DriverEnv } from "./adapter.ts"; |
| 23 | import { ProviderDriverError } from "@t3tools/provider-core/server/errors"; |
| 24 | import { |
| 25 | buildInitialPiProviderSnapshot, |
| 26 | checkPiProviderStatus, |
| 27 | discoverPiCommandsForCwd, |
| 28 | enrichPiSnapshot, |
| 29 | } from "./status.ts"; |
| 30 | import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider"; |
| 31 | import { |
| 32 | defaultProviderContinuationIdentity, |
| 33 | type ProviderDriver, |
| 34 | type ProviderInstance, |
| 35 | } from "@t3tools/provider-core/server/driver"; |
| 36 | import type { ServerProviderDraft } from "@t3tools/provider-core/server/snapshotProbe"; |
| 37 | import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; |
| 38 | import { |
| 39 | makeCachedProviderMaintenanceResolution, |
| 40 | makePackageManagedProviderMaintenanceResolver, |
| 41 | resolveProviderMaintenanceCapabilitiesEffect, |
| 42 | } from "@t3tools/provider-core/server/maintenanceResolver"; |
| 43 | import { |
| 44 | haveProviderSnapshotSettingsChanged, |
| 45 | makeProviderSnapshotSettingsSource, |
| 46 | type ProviderSnapshotSettings, |
| 47 | } from "@t3tools/provider-core/server/snapshotSettings"; |
| 48 | |
| 49 | const decodePiSettings = Schema.decodeSync(PiSettings); |
| 50 | |
| 51 | const DRIVER_KIND = ProviderDriverKind.make("pi"); |
| 52 | const 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 | |
| 59 | export type PiDriverEnv = |
| 60 | | PiAdapterV2DriverEnv |
| 61 | | ProviderHost.ProviderHost |
| 62 | | ChildProcessSpawner.ChildProcessSpawner |
| 63 | | FileSystem.FileSystem |
| 64 | | HttpClient.HttpClient |
| 65 | | ProviderLatestVersions.ProviderLatestVersions |
| 66 | | Path.Path; |
| 67 | |
| 68 | const 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 | |
| 84 | export 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 | }; |