Skip to content

docs(pi): explain provider setup

MacroscopeApp / Macroscope - Effect Service Conventions failed Aug 8, 2026 in 2m 55s

Effect Service Conventions: 3 issues found

Three error-modeling/handling convention violations were found in the new Pi provider code. Inline comments were posted.

  • apps/server/src/textGeneration/PiTextGeneration.ts:100 — cause: String(cause) stringifies the underlying PiRpcError, erasing the error chain; pass the failure itself as cause.
  • apps/server/src/textGeneration/PiTextGeneration.ts:207-214 — uses Effect.catchTag("TimeoutError", ...); statically known tagged failures must be handled with Effect.catchTags({ ... }).
  • apps/server/src/provider/pi/PiRpcClient.ts:225 (with the class at lines 40-43) — PiRpcProcessExitedError has no cause field and its detail is built from String(cause), so the immediate underlying failure and stack are dropped and the message is opaque defect text.

Details

Note

Your check run agent prompt is: .macroscope/check-run-agents/effect-service-conventions.md
More information about how Check Run Agents work can be found in our Help Center.

Scope reviewed: new and changed TypeScript under apps/**, packages/** for this PR — apps/server/src/provider/Drivers/PiDriver.ts, apps/server/src/provider/Layers/PiAdapter.ts, apps/server/src/provider/Layers/PiProvider.ts, apps/server/src/provider/pi/{PiRpcClient,PiRpcSchema,PiModel,PiSessionFile}.ts, apps/server/src/textGeneration/PiTextGeneration.ts, apps/server/src/provider/builtInDrivers.ts, packages/contracts/src/{settings,providerRuntime}.ts, and the web/mobile registration call sites.

Checked and found acceptable:

  • Namespace imports from effect/* subpaths throughout the new modules; named imports retained only for @t3tools/contracts, pure helpers, schemas, and effect/unstable/process, matching the existing provider modules.
  • Dependency acquisition: makePiAdapter, makePiTextGeneration, and checkPiProviderStatus acquire Crypto, FileSystem, Path, and ChildProcessSpawner from the Effect environment; the optional makeRpcClient factory and onSessionPublished hook are legitimate test seams/callback strategies, and no ManagedRuntime.make, runPromise, or runPromiseExit was introduced in production code (Effect.runSync appears only in test fakes with a documented lint suppression).
  • No standalone FooShape types, compatibility re-export shims, or generic error-constructor aliases were introduced; PiDriver follows the existing ProviderDriver registration pattern and BUILT_IN_DRIVERS/BuiltInDriversEnv were updated mechanically.
  • Error classes use Schema.TaggedErrorClass with structured attributes (command/requestId, operation/sessionFile, provider/method), and predicates are exported/derived via Schema.is.

Not flagged (repository precedent, outside these conventions): the detail: cause.detail ?? String(cause) normalization in PiAdapter.request mirrors existing adapters (OpenClawAdapter, CodexAdapter), and the Pi model discovery failed: ${String(error)} provider snapshot message in PiProvider.ts is a probe status string rather than an Effect error attribute.


🔍 Agent activity — 1 tool call

💭 I'm reviewing the diff to check for Effect service convention violations across several key files. Looking at the imports first—they're using namespace imports from Effect subpaths, which seems correct, though I notice one import is using named imports from the unstable process module instead of a namespace import, which might be inconsistent with the convention. For the error types, I'm seeing some issues. PiRpcProtocolError has a detail field that's a static string like "failed to spawn Pi RPC", and PiRpcCommandError pulls its detail from the wire response with response.error ?? "Pi RPC command failed". The convention warns against copying raw wire payloads into detail, so that's borderline problematic. More concerning is PiRpcProcessExitedError—it's stringifying the cause directly into the detail field and dropping the cause entirely, which violates the rule about preserving the underlying error as cause and not duplicating it in the message. In PiProvider.ts, there's a message like Pi model discovery failed: ${String(error)} that looks user-facing, so I should check how other providers handle similar messages. For PiSessionFileError, it's structured with operation and sessionFile fields, which is good, but it's using cause: Schema.Defect() to store a raw value rather than an actual error—that's a borderline case of manufacturing an error just to populate cause. The Effect.fn usage looks fine, and dependency acquisition in makePiAdapter properly pulls Crypto, FileSystem, Path, and ChildProcessSpawner from the environment. The makeRpcClient option serves as a test seam, which is appropriate. In PiDriver.ts, the pattern of acquiring the spawner from the environment and then re-providing it via Effect.provideService is consistent with what other drivers do. The makePiTextGeneration function defaults process.env as a parameter, which reads a module global—acceptable since existing providers follow the same approach. However, I'm noticing a convention violation with Effect.catchTag in PiTextGeneration.ts. The codebase guidelines say to use Effect.catchTags with an object for all tagged error handling, even single tags, and to avoid catchTag entirely. The current code uses Effect.catchTag("TimeoutError", ...) when it should be Effect.catchTags({ TimeoutError: () => ... }). The Effect.catch in PiAdapter is fine since it's handling the whole channel. For the tests, Effect.runSync with Queue.unbounded is acceptable since it's in test code with a lint suppression comment explaining the rationale. I should verify whether the repo already uses catchTag elsewhere to understand the current state of compliance. I'm also noticing that PiRpcProcessExitedError and TextGenerationError both violate the error handling conventions — they're stringifying the cause instead of preserving it as the actual error object, which loses the original error information and breaks the pattern of deriving messages only from stable attributes. I'm noticing that TextGenerationError uses a shared contract with operation/detail/cause fields, where the detail field is the standard shape. The current I'm spotting a pattern of convention violations in the codebase. There's a use of catchTag in PiTextGeneration.ts at line 207 that directly contradicts the stated convention against it. Additionally, around line 120 in the error mapping for makeRpcClient, the code is stringifying the cause value when it should preserve the original underlying value to maintain the error chain—other similar code in the repo passes cause directly without transformation. Looking at the adapter conventions, I see that PiAdapter's request() helper converts the cause into a detail string when one isn't explicitly provided, which aligns with the existing pattern of using cause.message. But there's a tension here—the convention warns against adding a detail field that just copies cause.message and then using that to build the wrapper message, yet ProviderAdapterRequestError seems to do exactly that, which technically violates the guideline even though it's a widespread pattern in the codebase. The String(cause) fallback in detail is particularly problematic because it can pull in arbitrary error text, which directly contradicts the instruction not to copy arbitrary defect text into detail. I should flag the clearest violations: Effect.catchTag should be Effect.catchTags in PiTextGeneration around line 207, the cause: String(cause) pattern in PiTextGeneration erases the error chain, and PiRpcProcessExitedError lacks a cause field while constructing detail from String(cause), which violates both the "preserve the immediate underlying error" and "don't merely stringify an opaque cause" rules. Let me check the exact lines in PiRpcClient.ts and PiTextGeneration.ts to comment accurately and see if there are other violations like PiRpcProtocolError. For PiRpcCommandError, the detail comes from a wire payload (response.error), which is borderline — the convention warns against copying raw wire payloads into detail, but similar patterns exist elsewhere in the codebase, so I'll skip that. PiSessionFileError is trickier: it uses cause: Schema.Defect() with cause: input.instanceId, storing a raw value rather than an error, which is a mild misuse since pure validation errors without underlying failures don't need a cause, and the invalid value should probably be a structural field instead. The operation field uses a multi-value discriminator ("instanceId", "containment", "symlink", etc.), which the convention allows with a generic message, and since PiSessionFileError has no message override, the default is fine. Checking the rest: PiAdapter handles the channel with Effect.catch correctly, no ManagedRuntime or runPromise in production code, and the namespace imports aren't alphabetically ordered but that's not a convention here. These modules are factory functions matching existing patterns, so no Context.Service is needed, and PiDriver matches the ProviderDriver structure. For dependency acquisition, makePiTextGeneration takes settings, environment (defaulting to process.env), and a makeRpcClient factory parameter, which matches the existing si I've identified three specific issues in the error handling: the first is in PiTextGeneration where converting the cause to a string breaks the error chain—it should stay as the raw cause object. The second is that the same file needs to switch from Effect.catchTag to Effect.catchTags with a TimeoutError handler. The third problem is in PiRpcClient where the error gets stringified into a detail field and the underlying cause is lost entirely, but the error class definition doesn't even have a cause field to store it. For the fourth issue in PiAdapter, the request() helper is copying stringified cause details into the detail field, which violates the convention against duplicating cause information into wrapper messages. The helper itself is borderline—it does some normalization by picking cause.detail, but that normalization is exactly the discouraged pattern. I think it's worth flagging as a lower-priority concern since existing code already does similar things with cause.message. I'm noticing a tension between the provider probe message being user-facing text and the rule against splicing arbitrary error details into user-visible messages. The test expects the message to start with "Pi model discovery failed:", but using String(error) could leak raw wire error strings or other implementation details that shouldn't be exposed to callers, which violates the convention about keeping direct error attributes and log annotations bounded and safe. I'll focus on the three solid findings rather than flag this as a violation—it's a weaker concern compared to the concrete cases. Now I need to map out the exact line numbers for inline comments in the diff: the first issue is at line 100 in PiTextGeneration.ts where the cause is being stringified, the second spans lines 207-214 in the same file for the catchTag block, and the third is at line 225 in PiRpcClient.ts, with a note about the missing cause field around lines 40-43.

1. modify_pr — Reviewed the new Pi provider modules (PiDriver, PiAdapter, PiProvider, PiRpcClient, PiSessionFile, PiTextGeneration) against the Effect service conventions. Dependency acquisition, namespace imports, and layer/factory shapes look consistent with the existing provider modules. Three error-modeling/handling violations are noted inline.


Agent Credits: 194 credits