From 7cfdc81b32ff54d3198af1cc64a23550bda6644f Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Sat, 15 Aug 2026 16:32:04 -0700 Subject: [PATCH 1/4] refactor: use Effect guards for runtime errors HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a002e8-7aeb-7845-a1f7-b46b966b896a --- .../scripts/update-managed-binaries.ts | 6 +++-- .../fold-agent/src/Catalog/LoadCatalog.ts | 6 ++--- packages/fold-agent/src/Config/Load.ts | 4 ++-- packages/fold-agent/src/Tools/WebFetchTool.ts | 8 +++---- .../fold-agent/src/Tools/WebSearchTool.ts | 10 ++++---- .../test/EventLog/EventLogJsonl.vi.test.ts | 6 ++--- packages/fold-codex/src/Hardening.ts | 4 ++-- packages/fold-codex/test/CodexAuth.vi.test.ts | 6 +++-- .../src/AgentRuntime/AgentRuntimeLayer.ts | 4 ++-- .../src/Compaction/CompactionLayer.ts | 4 ++-- packages/fold-core/src/HookRunner/Errors.ts | 23 +++++++++++-------- .../src/ToolRuntime/ModelVisibleErrors.ts | 6 ++--- .../src/ToolRuntime/ToolRuntimeLayer.ts | 4 ++-- .../EventLog/StoredLogEntryDecoder.vi.test.ts | 4 ++-- .../test/Tools/PatchEngine.vi.test.ts | 6 +++-- packages/fold-tui-theme/src/github/client.ts | 18 +++++++++------ 16 files changed, 67 insertions(+), 52 deletions(-) diff --git a/packages/fold-agent/scripts/update-managed-binaries.ts b/packages/fold-agent/scripts/update-managed-binaries.ts index d3dfa95..29310e6 100644 --- a/packages/fold-agent/scripts/update-managed-binaries.ts +++ b/packages/fold-agent/scripts/update-managed-binaries.ts @@ -12,7 +12,7 @@ import { writeFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' -import { Effect, Schema } from 'effect' +import { Effect, Predicate, Schema } from 'effect' import { MANAGED_BINARY_PLATFORMS, managedBinaryRegistry } from '../src/Bin/Registry' @@ -33,7 +33,9 @@ const fetchBytes = (url: string): Effect.Effect = return new Uint8Array(await response.arrayBuffer()) }, catch: (cause) => - new ChecksumBakeError({ message: `GET ${url}: ${cause instanceof Error ? cause.message : String(cause)}` }), + new ChecksumBakeError({ + message: `GET ${url}: ${Predicate.isError(cause) ? cause.message : String(cause)}`, + }), }).pipe( Effect.timeout(downloadTimeoutMillis), Effect.catchTag('TimeoutError', () => diff --git a/packages/fold-agent/src/Catalog/LoadCatalog.ts b/packages/fold-agent/src/Catalog/LoadCatalog.ts index 05f311f..5a53ac4 100644 --- a/packages/fold-agent/src/Catalog/LoadCatalog.ts +++ b/packages/fold-agent/src/Catalog/LoadCatalog.ts @@ -14,7 +14,7 @@ import { dirname, join } from 'node:path' import { ModelCatalogEntry } from '@humanlayer/fold-core' -import { Clock, Effect, Schema, type FileSystem } from 'effect' +import { Clock, Effect, Predicate, Schema, type FileSystem } from 'effect' import { fileSystemFor, type FsToolOptions } from '../Fs/DefaultFileSystem' import { bakedModelCatalog } from './BakedCatalog' @@ -75,7 +75,7 @@ export const modelCatalogCachePath = (foldHome: string): string => join(foldHome /** The ONE mapper from thrown fetch failures to the typed catalog fetch error. */ const catalogFetchErrorFrom = (cause: unknown): CatalogFetchError => - new CatalogFetchError({ message: cause instanceof Error ? cause.message : String(cause) }) + new CatalogFetchError({ message: Predicate.isError(cause) ? cause.message : String(cause) }) const defaultFetchJson = (url: string): Effect.Effect => Effect.tryPromise({ @@ -104,7 +104,7 @@ const readCache = (fs: FileSystem.FileSystem, path: string): Effect.Effect JSON.parse(text), catch: (cause) => - new CatalogCacheParseError({ message: cause instanceof Error ? cause.message : String(cause) }), + new CatalogCacheParseError({ message: Predicate.isError(cause) ? cause.message : String(cause) }), }).pipe( Effect.flatMap((parsed) => decodeCache(parsed)), Effect.catch((error) => diff --git a/packages/fold-agent/src/Config/Load.ts b/packages/fold-agent/src/Config/Load.ts index 9fcbafd..a15ad07 100644 --- a/packages/fold-agent/src/Config/Load.ts +++ b/packages/fold-agent/src/Config/Load.ts @@ -12,7 +12,7 @@ import { homedir } from 'node:os' import { join } from 'node:path' -import { Effect, Schema } from 'effect' +import { Effect, Predicate, Schema } from 'effect' import { fileSystemFor, type FsToolOptions } from '../Fs/DefaultFileSystem' import { FoldConfig } from './ConfigSchema' @@ -135,7 +135,7 @@ export const parseFoldConfig = ( const parsed = yield* Effect.try({ try: (): unknown => JSON.parse(stripJsonc(text)), catch: (cause) => - new ConfigParseError({ path, message: cause instanceof Error ? cause.message : String(cause) }), + new ConfigParseError({ path, message: Predicate.isError(cause) ? cause.message : String(cause) }), }) return yield* decodeConfig(parsed).pipe( diff --git a/packages/fold-agent/src/Tools/WebFetchTool.ts b/packages/fold-agent/src/Tools/WebFetchTool.ts index 3d38e93..ffbb87e 100644 --- a/packages/fold-agent/src/Tools/WebFetchTool.ts +++ b/packages/fold-agent/src/Tools/WebFetchTool.ts @@ -1,5 +1,5 @@ import { defineTool, webFetchToolContract, type FoldTool } from '@humanlayer/fold-core' -import { Effect } from 'effect' +import { Effect, Predicate } from 'effect' const maxResponseSize = 5 * 1024 * 1024 const defaultTimeoutMs = 30_000 @@ -65,7 +65,7 @@ const readBody = (response: Response): Effect.Effect ({ message: error instanceof Error ? error.message : String(error) }), + catch: (error) => ({ message: Predicate.isError(error) ? error.message : String(error) }), }) export const webFetchTool = (): FoldTool => @@ -90,9 +90,9 @@ export const webFetchTool = (): FoldTool => }), catch: (error) => ({ message: - error instanceof Error && error.name === 'AbortError' + Predicate.isError(error) && error.name === 'AbortError' ? `Request timed out after ${timeoutMs}ms` - : error instanceof Error + : Predicate.isError(error) ? error.message : String(error), }), diff --git a/packages/fold-agent/src/Tools/WebSearchTool.ts b/packages/fold-agent/src/Tools/WebSearchTool.ts index d6bcd35..b372b51 100644 --- a/packages/fold-agent/src/Tools/WebSearchTool.ts +++ b/packages/fold-agent/src/Tools/WebSearchTool.ts @@ -1,5 +1,5 @@ import { CurrentAgent, defineTool, webSearchToolContract, type FoldTool } from '@humanlayer/fold-core' -import { Effect } from 'effect' +import { Effect, Predicate } from 'effect' const defaultTimeoutMs = 25_000 const maxNumResults = 20 @@ -88,7 +88,7 @@ const parseMcpResponse = (body: string): Effect.Effect ({ - message: `Failed to parse web search response: ${error instanceof Error ? error.message : String(error)}`, + message: `Failed to parse web search response: ${Predicate.isError(error) ? error.message : String(error)}`, }), }) @@ -123,9 +123,9 @@ const callMcp = (input: { }), catch: (error) => ({ message: - error instanceof Error && error.name === 'AbortError' + Predicate.isError(error) && error.name === 'AbortError' ? `${input.tool} request timed out` - : error instanceof Error + : Predicate.isError(error) ? error.message : String(error), }), @@ -140,7 +140,7 @@ const callMcp = (input: { const body = yield* Effect.tryPromise({ try: () => response.text(), catch: (error) => ({ - message: `Failed to read web search response: ${error instanceof Error ? error.message : String(error)}`, + message: `Failed to read web search response: ${Predicate.isError(error) ? error.message : String(error)}`, }), }) return yield* parseMcpResponse(body) diff --git a/packages/fold-agent/test/EventLog/EventLogJsonl.vi.test.ts b/packages/fold-agent/test/EventLog/EventLogJsonl.vi.test.ts index 7dbdf01..b905978 100644 --- a/packages/fold-agent/test/EventLog/EventLogJsonl.vi.test.ts +++ b/packages/fold-agent/test/EventLog/EventLogJsonl.vi.test.ts @@ -13,7 +13,7 @@ import { StateId, type LogEntryInput, } from '@humanlayer/fold-core' -import { Effect, Fiber, FileSystem, Stream } from 'effect' +import { Effect, Fiber, FileSystem, Predicate, Stream } from 'effect' import { layerJsonl } from '../../src/index' @@ -105,7 +105,7 @@ it.effect('jsonl layer maps invalid persisted lines to EventLogCorruptEntryError return yield* Stream.runCollect(log.entries()) }).pipe(Effect.provide(layerJsonl(filePath)), Effect.flip) - if (!(error instanceof EventLogCorruptEntryError)) { + if (!Predicate.isTagged(error, 'EventLogCorruptEntryError')) { throw new Error(`expected EventLogCorruptEntryError, got ${error._tag}`) } expect(error.line).toBe(1) @@ -201,7 +201,7 @@ it.effect('jsonl layer rejects event formats newer than the installed Fold runti }).pipe(Effect.provide(layerJsonl(filePath)), Effect.flip) expect(error).toBeInstanceOf(EventLogUnsupportedVersionError) - if (error instanceof EventLogUnsupportedVersionError) { + if (Predicate.isTagged(error, 'EventLogUnsupportedVersionError')) { expect(error.version).toBe(2) expect(error.supportedVersions).toEqual([1]) } diff --git a/packages/fold-codex/src/Hardening.ts b/packages/fold-codex/src/Hardening.ts index 4ca1b0f..6a60759 100644 --- a/packages/fold-codex/src/Hardening.ts +++ b/packages/fold-codex/src/Hardening.ts @@ -65,11 +65,11 @@ export const codexAcquisitionStallError = (timeoutMs: number): AiError.AiError = /** True for the first-event stall errors this package mints (the only retryable stall class). */ export const isCodexFirstEventStall = (error: unknown): error is AiError.AiError => - error instanceof AiError.AiError && error.module === CODEX_ERROR_MODULE && error.method === FIRST_EVENT_METHOD + AiError.isAiError(error) && error.module === CODEX_ERROR_MODULE && error.method === FIRST_EVENT_METHOD /** True for the mid-stream idle stall errors this package mints. */ export const isCodexIdleStall = (error: unknown): error is AiError.AiError => - error instanceof AiError.AiError && error.module === CODEX_ERROR_MODULE && error.method === IDLE_METHOD + AiError.isAiError(error) && error.module === CODEX_ERROR_MODULE && error.method === IDLE_METHOD /** * A retryable provider failure is safe to repeat only before the model has emitted any stream event. diff --git a/packages/fold-codex/test/CodexAuth.vi.test.ts b/packages/fold-codex/test/CodexAuth.vi.test.ts index 1c937ec..2d1e177 100644 --- a/packages/fold-codex/test/CodexAuth.vi.test.ts +++ b/packages/fold-codex/test/CodexAuth.vi.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { describe, expect, it } from '@effect/vitest' -import { Effect, Layer, Option } from 'effect' +import { Effect, Layer, Option, Predicate } from 'effect' import { FetchHttpClient, type HttpClient } from 'effect/unstable/http' import { @@ -22,6 +22,8 @@ const jwtWith = (claims: Record): string => type RecordedRequest = { readonly url: string; readonly body: string } +const isWebRequest = (input: string | URL | Request): input is Request => Predicate.hasProperty(input, 'url') + /** A FetchHttpClient layer whose network is a scripted function, recording every request it serves. */ const scriptedFetchLayer = ( respond: (request: RecordedRequest) => Response, @@ -31,7 +33,7 @@ const scriptedFetchLayer = ( // Bun's `typeof fetch` carries a `preconnect` property; borrow the real one alongside the fake body. const fakeFetch: typeof fetch = Object.assign( async (input: string | URL | Request, init?: RequestInit) => { - const request = input instanceof Request ? input : new Request(String(input), init) + const request = isWebRequest(input) ? input : new Request(String(input), init) const recorded = { url: request.url, body: await request.clone().text() } requests.push(recorded) return respond(recorded) diff --git a/packages/fold-core/src/AgentRuntime/AgentRuntimeLayer.ts b/packages/fold-core/src/AgentRuntime/AgentRuntimeLayer.ts index b12d580..19ba10a 100644 --- a/packages/fold-core/src/AgentRuntime/AgentRuntimeLayer.ts +++ b/packages/fold-core/src/AgentRuntime/AgentRuntimeLayer.ts @@ -10,7 +10,7 @@ * those deltas are ephemeral and never persisted. Model provider failures become durable error + * agent-finished entries, never service failures. */ -import { Array as Arr, Cause, Effect, Exit, Layer, Ref, Result, Schema, Stream } from 'effect' +import { Array as Arr, Cause, Effect, Exit, Layer, Predicate, Ref, Result, Schema, Stream } from 'effect' import { LanguageModel, Prompt, type Response, type Tool, type Toolkit } from 'effect/unstable/ai' import { AgentEvents } from '../AgentEvents/AgentEventsService' @@ -79,7 +79,7 @@ type CompactionEnvelope = Pick { - if (error instanceof Error) return error.message + if (Predicate.isError(error)) return error.message try { return JSON.stringify(error) diff --git a/packages/fold-core/src/Compaction/CompactionLayer.ts b/packages/fold-core/src/Compaction/CompactionLayer.ts index c51c246..805ce21 100644 --- a/packages/fold-core/src/Compaction/CompactionLayer.ts +++ b/packages/fold-core/src/Compaction/CompactionLayer.ts @@ -9,7 +9,7 @@ */ import { AnthropicLanguageModel } from '@effect/ai-anthropic' import { OpenAiLanguageModel } from '@effect/ai-openai' -import { Effect, Stream } from 'effect' +import { Effect, Predicate, Stream } from 'effect' import { LanguageModel, Prompt } from 'effect/unstable/ai' import { ModelCatalog } from '../Model/ModelCatalog' @@ -44,7 +44,7 @@ import { export type EnabledAutoCompactConfig = Extract const describeSummarizerError = (error: unknown): string => { - if (error instanceof Error) return error.message + if (Predicate.isError(error)) return error.message try { return JSON.stringify(error) diff --git a/packages/fold-core/src/HookRunner/Errors.ts b/packages/fold-core/src/HookRunner/Errors.ts index ce7e989..6b05744 100644 --- a/packages/fold-core/src/HookRunner/Errors.ts +++ b/packages/fold-core/src/HookRunner/Errors.ts @@ -1,15 +1,20 @@ -import type { Cause } from 'effect' +import { Data, Predicate, type Cause } from 'effect' export type HookPhase = 'preRequest' | 'preToolUse' | 'postToolUse' | 'onComplete' -export class HookExecutionError extends Error { - readonly _tag = 'HookExecutionError' +export class HookExecutionError extends Data.TaggedError('HookExecutionError')<{ + readonly phase: HookPhase + readonly hookName: string + readonly cause: Cause.Cause +}> { + constructor(phase: HookPhase, hookName: string, cause: Cause.Cause) { + super({ phase, hookName, cause }) + } - constructor( - readonly phase: HookPhase, - readonly hookName: string, - override readonly cause: Cause.Cause, - ) { - super(`${phase} hook "${hookName}" failed`) + override get message(): string { + return `${this.phase} hook "${this.hookName}" failed` } } + +export const isHookExecutionError = (error: unknown): error is HookExecutionError => + Predicate.isTagged(error, 'HookExecutionError') diff --git a/packages/fold-core/src/ToolRuntime/ModelVisibleErrors.ts b/packages/fold-core/src/ToolRuntime/ModelVisibleErrors.ts index 66993f6..1960e3a 100644 --- a/packages/fold-core/src/ToolRuntime/ModelVisibleErrors.ts +++ b/packages/fold-core/src/ToolRuntime/ModelVisibleErrors.ts @@ -4,7 +4,7 @@ * result with an error message, D21) narrow raw Causes through these helpers, so the model always sees * the same escaped, truncated, single-line description regardless of which boundary caught the defect. */ -import { Cause } from 'effect' +import { Cause, Predicate } from 'effect' const maxModelVisibleErrorMessageLength = 300 @@ -25,7 +25,7 @@ export const truncateModelVisibleErrorMessage = (message: string): string => { } const stringifyUnknown = (value: unknown): string => { - if (value instanceof Error) return value.message + if (Predicate.isError(value)) return value.message try { return JSON.stringify(value) @@ -36,7 +36,7 @@ const stringifyUnknown = (value: unknown): string => { /** Render an unknown thrown/failed value as safe model-visible text. */ export const modelVisibleErrorDetailsFromUnknown = (value: unknown): string => { - const raw = value instanceof Error ? value.message : stringifyUnknown(value) + const raw = Predicate.isError(value) ? value.message : stringifyUnknown(value) return escapeSystemInformationContent(truncateModelVisibleErrorMessage(raw === '' ? 'unknown error' : raw)) } diff --git a/packages/fold-core/src/ToolRuntime/ToolRuntimeLayer.ts b/packages/fold-core/src/ToolRuntime/ToolRuntimeLayer.ts index a9cb265..9adcd43 100644 --- a/packages/fold-core/src/ToolRuntime/ToolRuntimeLayer.ts +++ b/packages/fold-core/src/ToolRuntime/ToolRuntimeLayer.ts @@ -9,7 +9,7 @@ import { Prompt } from 'effect/unstable/ai' import { EventLog } from '../EventLog/EventLogService' import type { LogEntry, ToolResultLogEntry } from '../EventLog/Schemas' -import { HookExecutionError } from '../HookRunner/Errors' +import { HookExecutionError, isHookExecutionError } from '../HookRunner/Errors' import { HookRunner } from '../HookRunner/HookRunnerService' import { Ids, ToolCallId, type AgentId } from '../Ids' import { Subagents } from '../Subagents/SubagentsService' @@ -159,7 +159,7 @@ const hookFailureResult = (error: HookExecutionError, toolName: string): string const hookExecutionErrorFromCause = (cause: Cause.Cause): HookExecutionError | undefined => { const reason = cause.reasons.find(Cause.isFailReason) - return reason?.error instanceof HookExecutionError ? reason.error : undefined + return isHookExecutionError(reason?.error) ? reason.error : undefined } const failureResultFromCause = (toolName: string, cause: Cause.Cause): string => { diff --git a/packages/fold-core/test/EventLog/StoredLogEntryDecoder.vi.test.ts b/packages/fold-core/test/EventLog/StoredLogEntryDecoder.vi.test.ts index d4b5916..1407aae 100644 --- a/packages/fold-core/test/EventLog/StoredLogEntryDecoder.vi.test.ts +++ b/packages/fold-core/test/EventLog/StoredLogEntryDecoder.vi.test.ts @@ -1,5 +1,5 @@ import { it, expect } from '@effect/vitest' -import { Effect } from 'effect' +import { Effect, Predicate } from 'effect' import { AgentId, @@ -60,7 +60,7 @@ it.effect('rejects an unsupported event format without guessing its schema', () const error = yield* decodeStoredLogEntry(sessionStartedEntry(2)).pipe(Effect.flip) expect(error).toBeInstanceOf(EventLogUnsupportedVersionError) - if (error instanceof EventLogUnsupportedVersionError) { + if (Predicate.isTagged(error, 'EventLogUnsupportedVersionError')) { expect(error.version).toBe(2) expect(error.seq).toBe(0) expect(error.supportedVersions).toEqual([1]) diff --git a/packages/fold-core/test/Tools/PatchEngine.vi.test.ts b/packages/fold-core/test/Tools/PatchEngine.vi.test.ts index 2e3472d..d5b6bd2 100644 --- a/packages/fold-core/test/Tools/PatchEngine.vi.test.ts +++ b/packages/fold-core/test/Tools/PatchEngine.vi.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from '@effect/vitest' -import { Effect, Result } from 'effect' +import { Effect, Predicate, Result } from 'effect' import { applyChunks, @@ -430,7 +430,9 @@ describe('computePatch', () => { ).pipe(Effect.result) if (!Result.isFailure(result)) throw new Error('expected failure') - if (!(result.failure instanceof PatchFileNotFoundError)) throw new Error('expected PatchFileNotFoundError') + if (!Predicate.isTagged(result.failure, 'PatchFileNotFoundError')) { + throw new Error('expected PatchFileNotFoundError') + } expect(result.failure.message).toBe('Failed to read file to update: gone.txt') }), ) diff --git a/packages/fold-tui-theme/src/github/client.ts b/packages/fold-tui-theme/src/github/client.ts index c7ba0c2..21f5238 100644 --- a/packages/fold-tui-theme/src/github/client.ts +++ b/packages/fold-tui-theme/src/github/client.ts @@ -136,13 +136,17 @@ function describeFailure(path: string, res: Response): string { /** A rejected request's reason as a string, with a friendly note for timeouts. */ function reasonText(reason: unknown): string { - if (reason instanceof Error) { - if (reason.name === 'TimeoutError' || reason.name === 'AbortError') { - return `request timed out after ${REQUEST_TIMEOUT_MS / 1000}s` - } - return reason.message + const name = stringProperty(reason, 'name') + if (name === 'TimeoutError' || name === 'AbortError') { + return `request timed out after ${REQUEST_TIMEOUT_MS / 1000}s` } - return String(reason) + return stringProperty(reason, 'message') ?? String(reason) +} + +const stringProperty = (value: unknown, property: string): string | undefined => { + if (typeof value !== 'object' || value === null || !(property in value)) return undefined + const propertyValue = Reflect.get(value, property) + return typeof propertyValue === 'string' ? propertyValue : undefined } /** Prefer an actionable rate-limit reason over a generic one when both lists fail. */ @@ -233,7 +237,7 @@ export async function loadFeed(options: LoadOptions): Promise { } } catch (error) { // Any unexpected throw (e.g. malformed JSON) still lands on fixtures. - const reason = error instanceof Error ? error.message : String(error) + const reason = stringProperty(error, 'message') ?? String(error) return { ...DEMO_FEED, repo, offlineReason: reason } } } From fc9887031bce46e76d541e940a25427a412f8b3b Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Sat, 15 Aug 2026 16:34:11 -0700 Subject: [PATCH 2/4] fix: finish Effect runtime guard migration HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a002e8-7aeb-7845-a1f7-b46b966b896a --- packages/fold-codex/src/Hardening.ts | 2 +- packages/fold-core/src/ToolRuntime/ToolRuntimeLayer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/fold-codex/src/Hardening.ts b/packages/fold-codex/src/Hardening.ts index 6a60759..f7f1b73 100644 --- a/packages/fold-codex/src/Hardening.ts +++ b/packages/fold-codex/src/Hardening.ts @@ -77,7 +77,7 @@ export const isCodexIdleStall = (error: unknown): error is AiError.AiError => * repeat a tool call that has already reached the agent runtime. */ export const isCodexRetryableBeforeFirstEvent = (error: unknown): error is AiError.AiError => - error instanceof AiError.AiError && error.isRetryable && !isCodexIdleStall(error) + AiError.isAiError(error) && error.isRetryable && !isCodexIdleStall(error) /** * Bound the stream's producer latency: the first event must arrive within `firstEventTimeoutMs` and diff --git a/packages/fold-core/src/ToolRuntime/ToolRuntimeLayer.ts b/packages/fold-core/src/ToolRuntime/ToolRuntimeLayer.ts index 9adcd43..2d92449 100644 --- a/packages/fold-core/src/ToolRuntime/ToolRuntimeLayer.ts +++ b/packages/fold-core/src/ToolRuntime/ToolRuntimeLayer.ts @@ -9,7 +9,7 @@ import { Prompt } from 'effect/unstable/ai' import { EventLog } from '../EventLog/EventLogService' import type { LogEntry, ToolResultLogEntry } from '../EventLog/Schemas' -import { HookExecutionError, isHookExecutionError } from '../HookRunner/Errors' +import { isHookExecutionError, type HookExecutionError } from '../HookRunner/Errors' import { HookRunner } from '../HookRunner/HookRunnerService' import { Ids, ToolCallId, type AgentId } from '../Ids' import { Subagents } from '../Subagents/SubagentsService' From da99d5a035336ac4957aa82c6c1bef80dd731d40 Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Sat, 15 Aug 2026 16:38:54 -0700 Subject: [PATCH 3/4] fix: use Effect predicates for unknown properties HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a002e8-7aeb-7845-a1f7-b46b966b896a --- bun.lock | 1 + packages/fold-tui-theme/package.json | 1 + packages/fold-tui-theme/src/github/client.ts | 7 ++++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 3e53e3f..82e87a0 100644 --- a/bun.lock +++ b/bun.lock @@ -169,6 +169,7 @@ "dependencies": { "@opentui/core": "catalog:", "@opentui/react": "0.4.3", + "effect": "catalog:", "react": "19.2.0", }, "devDependencies": { diff --git a/packages/fold-tui-theme/package.json b/packages/fold-tui-theme/package.json index 8aae0d0..ff6a4e3 100644 --- a/packages/fold-tui-theme/package.json +++ b/packages/fold-tui-theme/package.json @@ -28,6 +28,7 @@ "dependencies": { "@opentui/core": "catalog:", "@opentui/react": "0.4.3", + "effect": "catalog:", "react": "19.2.0" }, "devDependencies": { diff --git a/packages/fold-tui-theme/src/github/client.ts b/packages/fold-tui-theme/src/github/client.ts index 21f5238..247d367 100644 --- a/packages/fold-tui-theme/src/github/client.ts +++ b/packages/fold-tui-theme/src/github/client.ts @@ -1,3 +1,5 @@ +import { Predicate } from 'effect' + import { DEMO_FEED } from './fixtures' import type { Feed, GhItem, RateLimit } from './types' @@ -144,9 +146,8 @@ function reasonText(reason: unknown): string { } const stringProperty = (value: unknown, property: string): string | undefined => { - if (typeof value !== 'object' || value === null || !(property in value)) return undefined - const propertyValue = Reflect.get(value, property) - return typeof propertyValue === 'string' ? propertyValue : undefined + if (!Predicate.hasProperty(value, property)) return undefined + return Predicate.isString(value[property]) ? value[property] : undefined } /** Prefer an actionable rate-limit reason over a generic one when both lists fail. */ From f148f1c4fb2fffa2e1ec3f998cc21926d81db960 Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Sat, 15 Aug 2026 17:47:02 -0700 Subject: [PATCH 4/4] fix: publish Effect as a theme peer HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a002e8-7aeb-7845-a1f7-b46b966b896a --- bun.lock | 5 ++++- packages/fold-tui-theme/package.json | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index 82e87a0..e6d6cfe 100644 --- a/bun.lock +++ b/bun.lock @@ -169,13 +169,16 @@ "dependencies": { "@opentui/core": "catalog:", "@opentui/react": "0.4.3", - "effect": "catalog:", "react": "19.2.0", }, "devDependencies": { "@types/react": "19.2.2", + "effect": "catalog:", "typescript": "catalog:", }, + "peerDependencies": { + "effect": "catalog:", + }, }, "packages/fold-xai": { "name": "@humanlayer/fold-xai", diff --git a/packages/fold-tui-theme/package.json b/packages/fold-tui-theme/package.json index ff6a4e3..c859d50 100644 --- a/packages/fold-tui-theme/package.json +++ b/packages/fold-tui-theme/package.json @@ -28,11 +28,14 @@ "dependencies": { "@opentui/core": "catalog:", "@opentui/react": "0.4.3", - "effect": "catalog:", "react": "19.2.0" }, "devDependencies": { "@types/react": "19.2.2", + "effect": "catalog:", "typescript": "catalog:" + }, + "peerDependencies": { + "effect": "catalog:" } }