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