Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/fold-agent/examples/ApplyPatchAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'

import { defineAgent, openaiModel, startSession } from '@humanlayer/fold-core'
import * as NodeFileSystem from '@effect/platform-node/NodeFileSystem'
import { Console, Effect } from 'effect'

import { codingTools } from '../src/index'
Expand Down Expand Up @@ -49,7 +50,7 @@ const makeProgram = (apiKey: string) =>
yield* Console.log(`finished: ${finished.outcome}`)
yield* Console.log(`result: ${finished.resultText ?? '(no text)'}`)
yield* Console.log(`tools called: ${toolNames.join(', ')}`)
}).pipe(Effect.scoped)
}).pipe(Effect.scoped, Effect.provide(NodeFileSystem.layer))

if (apiKey === undefined || apiKey === '') {
console.error('Set OPENAI_API_KEY to run this example.')
Expand Down
3 changes: 2 additions & 1 deletion packages/fold-agent/examples/CodingAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'

import { anthropicModel, defineAgent, startSession } from '@humanlayer/fold-core'
import * as NodeFileSystem from '@effect/platform-node/NodeFileSystem'
import { Console, Effect } from 'effect'

import { codingTools, jsonlEventLog } from '../src/index'
Expand Down Expand Up @@ -47,7 +48,7 @@ const makeProgram = (apiKey: string) =>
yield* Console.log(`result: ${finished.resultText ?? '(no text)'}`)
yield* Console.log(`log rows: ${entries.length} (persisted to ${logPath})`)
yield* Console.log(`tools used: ${entries.filter((entry) => entry._tag === 'tool-result').length} tool results`)
}).pipe(Effect.scoped)
}).pipe(Effect.scoped, Effect.provide(NodeFileSystem.layer))

if (apiKey === undefined || apiKey === '') {
console.error('Set ANTHROPIC_API_KEY to run this example.')
Expand Down
3 changes: 2 additions & 1 deletion packages/fold-agent/examples/ConfigAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* Then: bun packages/fold-agent/examples/ConfigAgent.ts "your prompt"
*/
import { layerLiveIdFactory } from '@humanlayer/fold-core'
import * as NodeFileSystem from '@effect/platform-node/NodeFileSystem'
import { Console, Effect } from 'effect'

import { configInit, launchSession, loadFoldConfigOrNull } from '../src/index'
Expand All @@ -29,7 +30,7 @@ const program = Effect.gen(function* () {

const finished = yield* session.send(prompt)
yield* Console.log(`\n[${finished.outcome}] ${finished.resultText ?? '(no text)'}`)
}).pipe(Effect.provide(layerLiveIdFactory), Effect.scoped)
}).pipe(Effect.provide(layerLiveIdFactory), Effect.provide(NodeFileSystem.layer), Effect.scoped)

Effect.runPromise(program).catch((error) => {
console.error(error)
Expand Down
3 changes: 2 additions & 1 deletion packages/fold-agent/examples/SkillsFromDisk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'

import { anthropicModel, defineAgent, skillTool, startSession } from '@humanlayer/fold-core'
import * as NodeFileSystem from '@effect/platform-node/NodeFileSystem'
import { Console, Effect } from 'effect'

import { skillsFromDisk } from '../src/index'
Expand Down Expand Up @@ -73,7 +74,7 @@ const makeProgram = (apiKey: string) =>

yield* Console.log(`finished: ${finished.outcome}`)
yield* Console.log(`result:\n${finished.resultText ?? '(no text)'}`)
}).pipe(Effect.scoped)
}).pipe(Effect.scoped, Effect.provide(NodeFileSystem.layer))

if (apiKey === undefined || apiKey === '') {
console.error('Set ANTHROPIC_API_KEY to run this example.')
Expand Down
3 changes: 2 additions & 1 deletion packages/fold-agent/examples/SubagentsAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'

import { anthropicModel, defineAgent, defineSubagent, startSession, subagentTool } from '@humanlayer/fold-core'
import * as NodeFileSystem from '@effect/platform-node/NodeFileSystem'
import { Console, Effect } from 'effect'

import { bashTool, jsonlEventLog, readTool } from '../src/index'
Expand Down Expand Up @@ -89,7 +90,7 @@ const makeProgram = (apiKey: string) =>
yield* Console.log(`\nlog: ${entries.length} rows persisted to ${logPath}`)
yield* Console.log(`subagents started: ${subagentStarts.length} (researcher: ${researcherId ?? 'none'})`)
yield* Console.log(`researcher turns: ${researcherTurns} across ${researcherCalls} dispatch/resume calls`)
}).pipe(Effect.scoped)
}).pipe(Effect.scoped, Effect.provide(NodeFileSystem.layer))

if (apiKey === undefined || apiKey === '') {
console.error('Set ANTHROPIC_API_KEY to run this example.')
Expand Down
35 changes: 17 additions & 18 deletions packages/fold-agent/src/Bin/ManagedBinaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ import { accessSync, constants, statSync } from 'node:fs'
import { delimiter, join } from 'node:path'
import { promisify } from 'node:util'

import { Cause, Effect, Schema, type FileSystem } from 'effect'

import { fileSystemFor, type FsToolOptions } from '../Fs/DefaultFileSystem'
import { Cause, Effect, FileSystem, Schema } from 'effect'
import { managedBinaryRegistry, type ManagedBinaryAsset, type ManagedBinaryDefinition } from './Registry'

/** Environment variable that, when set (non-empty), disables managed-binary downloads entirely. */
Expand Down Expand Up @@ -88,8 +86,8 @@ export type ExecSeam = (
export type EnsureManagedBinariesOptions = {
/** The fold home directory; binaries install into `<foldHome>/bin`. */
readonly foldHome: string
/** FileSystem override for hermetic tests. Defaults to the Node platform filesystem. */
readonly fileSystem?: FsToolOptions['fileSystem']
/** @deprecated FileSystem is now provided through the Effect R channel. */
readonly fileSystem?: FileSystem.FileSystem
/** Environment lookup for {@link FOLD_DISABLE_BINARY_DOWNLOADS} and PATH. Defaults to `process.env`. */
readonly env?: (name: string) => string | undefined
/** PATH lookup seam. Defaults to scanning the env seam's PATH for an executable file. */
Expand Down Expand Up @@ -438,14 +436,15 @@ const resolveOneNeverFailing = (
}),
)

const ensureOnce = (options: EnsureManagedBinariesOptions): Effect.Effect<ReadonlyArray<ManagedBinaryStatus>> =>
const ensureOnce = (options: EnsureManagedBinariesOptions): Effect.Effect<ReadonlyArray<ManagedBinaryStatus>, never, FileSystem.FileSystem> =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const env = options.env ?? ((name: string) => process.env[name])
const platform = options.platform ?? process.platform
const disableFlag = env(FOLD_DISABLE_BINARY_DOWNLOADS)
const context: ResolveContext = {
foldHome: options.foldHome,
fs: fileSystemFor(options.fileSystem === undefined ? {} : { fileSystem: options.fileSystem }),
fs,
env,
which: options.which ?? defaultWhich(env, platform),
download: options.download ?? defaultDownload,
Expand All @@ -463,7 +462,7 @@ const ensureOnce = (options: EnsureManagedBinariesOptions): Effect.Effect<Readon
})
})

const memoizedRuns = new Map<string, Effect.Effect<ReadonlyArray<ManagedBinaryStatus>>>()
const memoizedResults = new Map<string, ReadonlyArray<ManagedBinaryStatus>>()

/**
* Ensure every managed binary is resolvable, returning one status per registry entry (in registry
Expand All @@ -472,18 +471,18 @@ const memoizedRuns = new Map<string, Effect.Effect<ReadonlyArray<ManagedBinarySt
*/
export const ensureManagedBinaries = (
options: EnsureManagedBinariesOptions,
): Effect.Effect<ReadonlyArray<ManagedBinaryStatus>> =>
): Effect.Effect<ReadonlyArray<ManagedBinaryStatus>, never, FileSystem.FileSystem> =>
Effect.suspend(() => {
if (options.memoize === false) return ensureOnce(options)

const key = `${options.foldHome}${options.disableDownloads === true}${options.requireManagedInstall === true}`
const existing = memoizedRuns.get(key)
if (existing !== undefined) return existing

// Effect.cached construction is synchronous; the Map get/set pair runs without a yield point in
// between, so concurrent callers cannot race past each other into two resolution passes.
const run = Effect.runSync(Effect.cached(ensureOnce(options)))
memoizedRuns.set(key, run)

return run
const existing = memoizedResults.get(key)
if (existing !== undefined) return Effect.succeed(existing)

// Run once and cache the result. The suspend boundary is synchronous so concurrent
// callers cannot race past the get into two resolution passes; the second caller's
// ensureOnce is idempotent in the unlikely event of an async interleave.
return ensureOnce(options).pipe(
Effect.tap((result) => Effect.sync(() => { memoizedResults.set(key, result) })),
)
})
10 changes: 3 additions & 7 deletions packages/fold-agent/src/Catalog/LoadCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@
import { dirname, join } from 'node:path'

import { ModelCatalogEntry } from '@humanlayer/fold-core'
import { Clock, Effect, Predicate, Schema, type FileSystem } from 'effect'

import { fileSystemFor, type FsToolOptions } from '../Fs/DefaultFileSystem'
import { Clock, Effect, FileSystem, Predicate, Schema } from 'effect'
import { bakedModelCatalog } from './BakedCatalog'
import { decodeModelsDevModels, ModelsDevDecodeError } from './ModelsDevSchema'
import { modelCatalogEntriesFromModelsDev } from './Normalize'
Expand Down Expand Up @@ -58,8 +56,6 @@ export type ModelCatalogCache = typeof ModelCatalogCache.Type
export type LoadModelCatalogOptions = {
/** The fold home directory; the cache lives at `<foldHome>/cache/models-dev.json`. */
readonly foldHome: string
/** FileSystem override for hermetic tests. Defaults to the Node platform filesystem. */
readonly fileSystem?: FsToolOptions['fileSystem']
/** Environment lookup for {@link FOLD_DISABLE_MODELS_FETCH}. Defaults to reading `process.env`. */
readonly env?: (name: string) => string | undefined
/** Fetch seam returning the parsed JSON payload. Defaults to global `fetch` with a 10s timeout. */
Expand Down Expand Up @@ -154,9 +150,9 @@ const fetchCatalogEntries = (
* Load the model catalog entries for a launch. Never fails: fresh cache, else fetch-and-cache, else
* stale cache, else the baked snapshot.
*/
export const loadModelCatalog = (options: LoadModelCatalogOptions): Effect.Effect<ReadonlyArray<ModelCatalogEntry>> =>
export const loadModelCatalog = (options: LoadModelCatalogOptions): Effect.Effect<ReadonlyArray<ModelCatalogEntry>, never, FileSystem.FileSystem> =>
Effect.gen(function* () {
const fs = fileSystemFor(options.fileSystem === undefined ? {} : { fileSystem: options.fileSystem })
const fs = yield* FileSystem.FileSystem
const env = options.env ?? ((name: string) => process.env[name])
const now = yield* options.now ?? Clock.currentTimeMillis
const ttlMs = options.ttlMs ?? defaultCatalogTtlMs
Expand Down
16 changes: 6 additions & 10 deletions packages/fold-agent/src/Config/ConfigSchemaJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
*/
import { join } from 'node:path'

import { JsonSchema, Effect, Schema } from 'effect'

import { fileSystemFor, type FsToolOptions } from '../Fs/DefaultFileSystem'
import { JsonSchema, Effect, FileSystem, Schema } from 'effect'
import { FoldConfig } from './ConfigSchema'
import { writeFoldInfo } from './FoldInfo'
import { defaultConfigPath, defaultFoldHome } from './Load'
Expand Down Expand Up @@ -120,14 +118,12 @@ export const starterConfigJsonc = (): string =>
export type ConfigInitOptions = {
/** The fold home directory. Defaults to `~/.fold`. */
readonly foldHome?: string
/** FileSystem override for hermetic tests. Defaults to the Node platform filesystem. */
readonly fileSystem?: FsToolOptions['fileSystem']
}

/** Write `config.schema.json` under the fold home, creating the directory if needed. Returns its path. */
export const writeFoldConfigSchema = (options?: ConfigInitOptions): Effect.Effect<string> =>
export const writeFoldConfigSchema = (options?: ConfigInitOptions): Effect.Effect<string, never, FileSystem.FileSystem> =>
Effect.gen(function* () {
const fs = fileSystemFor(options?.fileSystem === undefined ? {} : { fileSystem: options.fileSystem })
const fs = yield* FileSystem.FileSystem
const home = options?.foldHome ?? defaultFoldHome()
yield* fs.makeDirectory(home, { recursive: true }).pipe(Effect.orDie)

Expand Down Expand Up @@ -159,9 +155,9 @@ export type ConfigInitResult = {
* `foldcode auth codex login` runs or the `apiKeyEnv` variables are exported. Only the two generated
* files are ever overwritten.
*/
export const bootstrapFoldHome = (options?: ConfigInitOptions): Effect.Effect<ConfigInitResult> =>
export const bootstrapFoldHome = (options?: ConfigInitOptions): Effect.Effect<ConfigInitResult, never, FileSystem.FileSystem> =>
Effect.gen(function* () {
const fs = fileSystemFor(options?.fileSystem === undefined ? {} : { fileSystem: options.fileSystem })
const fs = yield* FileSystem.FileSystem
const schemaPath = yield* writeFoldConfigSchema(options)
const infoPath = yield* writeFoldInfo(options)

Expand Down Expand Up @@ -189,4 +185,4 @@ export const bootstrapFoldHome = (options?: ConfigInitOptions): Effect.Effect<Co
})

/** `foldcode config init`: the same bootstrap, kept under its command-facing name. */
export const configInit = (options?: ConfigInitOptions): Effect.Effect<ConfigInitResult> => bootstrapFoldHome(options)
export const configInit = (options?: ConfigInitOptions): Effect.Effect<ConfigInitResult, never, FileSystem.FileSystem> => bootstrapFoldHome(options)
8 changes: 3 additions & 5 deletions packages/fold-agent/src/Config/FoldInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
*/
import { join } from 'node:path'

import { Effect } from 'effect'

import { fileSystemFor } from '../Fs/DefaultFileSystem'
import { Effect, FileSystem } from 'effect'
import type { ConfigInitOptions } from './ConfigSchemaJson'
import { defaultFoldHome } from './Load'

Expand Down Expand Up @@ -215,9 +213,9 @@ and \`fd\` over find. Disable downloads with \`FOLD_DISABLE_BINARY_DOWNLOADS=1\`
`

/** Write (always overwrite) `<foldHome>/FOLD_INFO.md`, creating the directory if needed. Returns its path. */
export const writeFoldInfo = (options?: ConfigInitOptions): Effect.Effect<string> =>
export const writeFoldInfo = (options?: ConfigInitOptions): Effect.Effect<string, never, FileSystem.FileSystem> =>
Effect.gen(function* () {
const fs = fileSystemFor(options?.fileSystem === undefined ? {} : { fileSystem: options.fileSystem })
const fs = yield* FileSystem.FileSystem
const home = options?.foldHome ?? defaultFoldHome()
yield* fs.makeDirectory(home, { recursive: true }).pipe(Effect.orDie)
const path = foldInfoPath(home)
Expand Down
12 changes: 4 additions & 8 deletions packages/fold-agent/src/Config/Load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
import { homedir } from 'node:os'
import { join } from 'node:path'

import { Effect, Predicate, Schema } from 'effect'

import { fileSystemFor, type FsToolOptions } from '../Fs/DefaultFileSystem'
import { Effect, FileSystem, Predicate, Schema } from 'effect'
import { FoldConfig } from './ConfigSchema'

/** The config file could not be found at the resolved path. */
Expand All @@ -40,8 +38,6 @@ export type LoadConfigOptions = {
readonly path?: string
/** The fold home directory. Defaults to `~/.fold`. */
readonly foldHome?: string
/** FileSystem override for hermetic tests. Defaults to the Node platform filesystem. */
readonly fileSystem?: FsToolOptions['fileSystem']
}

/** The fold home directory: `~/.fold`. */
Expand Down Expand Up @@ -149,9 +145,9 @@ export const parseFoldConfig = (
*/
export const loadFoldConfig = (
options?: LoadConfigOptions,
): Effect.Effect<FoldConfig, ConfigFileNotFoundError | ConfigParseError | ConfigDecodeError> =>
): Effect.Effect<FoldConfig, ConfigFileNotFoundError | ConfigParseError | ConfigDecodeError, FileSystem.FileSystem> =>
Effect.gen(function* () {
const fs = fileSystemFor(options?.fileSystem === undefined ? {} : { fileSystem: options.fileSystem })
const fs = yield* FileSystem.FileSystem
const path = configPathFor(options)

const exists = yield* fs.exists(path).pipe(Effect.catch(() => Effect.succeed(false)))
Expand All @@ -167,5 +163,5 @@ export const loadFoldConfig = (
*/
export const loadFoldConfigOrNull = (
options?: LoadConfigOptions,
): Effect.Effect<FoldConfig | null, ConfigParseError | ConfigDecodeError> =>
): Effect.Effect<FoldConfig | null, ConfigParseError | ConfigDecodeError, FileSystem.FileSystem> =>
loadFoldConfig(options).pipe(Effect.catchTag('ConfigFileNotFoundError', () => Effect.succeed(null)))
10 changes: 4 additions & 6 deletions packages/fold-agent/src/Config/ProviderConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import { dirname } from 'node:path'
import { DEFAULT_CODEX_MODEL_ID } from '@humanlayer/fold-codex'
import { DEFAULT_OPENCODE_MODEL_ID } from '@humanlayer/fold-opencode'
import { DEFAULT_XAI_MODEL_ID } from '@humanlayer/fold-xai'
import { Clock, Effect, Match, Random, Schema } from 'effect'

import { fileSystemFor } from '../Fs/DefaultFileSystem'
import { Clock, Effect, FileSystem, Match, Random, Schema } from 'effect'
import type { FoldConfig, ProviderKind } from './ConfigSchema'
import {
configPathFor,
Expand Down Expand Up @@ -88,9 +86,9 @@ const validBaseUrl = (value: string): Effect.Effect<string, ProviderConfiguratio
const writeConfig = (
config: FoldConfig,
options: LoadConfigOptions | undefined,
): Effect.Effect<void, ProviderConfigurationWriteError> =>
): Effect.Effect<void, ProviderConfigurationWriteError, FileSystem.FileSystem> =>
Effect.gen(function* () {
const fs = fileSystemFor(options?.fileSystem === undefined ? {} : { fileSystem: options.fileSystem })
const fs = yield* FileSystem.FileSystem
const path = configPathFor(options)
// A unique temp path for the atomic write-rename. Clock/Random are the seams here (not Date.now/crypto),
// so a test can pin the temporary filename deterministically.
Expand Down Expand Up @@ -122,7 +120,7 @@ const writeConfig = (
export const configureProvider = (
input: ConfigureProviderInput,
options?: LoadConfigOptions,
): Effect.Effect<FoldConfig, ConfigureProviderError> =>
): Effect.Effect<FoldConfig, ConfigureProviderError, FileSystem.FileSystem> =>
Effect.gen(function* () {
const name = yield* required(input.name, 'name')
const baseUrl = yield* validBaseUrl(input.baseUrl)
Expand Down
9 changes: 3 additions & 6 deletions packages/fold-agent/src/EventLog/JsonlDescriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@
import { eventLogSource, EventLog, type FoldEventLog } from '@humanlayer/fold-core'
import { Context, Effect, FileSystem, Layer } from 'effect'

import { fileSystemFor, type FsToolOptions } from '../Fs/DefaultFileSystem'
import { layerJsonl } from './JsonlLayer'

/** Options for {@link jsonlEventLog}: the FileSystem seam (Node default, overridable for tests). */
export type JsonlEventLogOptions = Pick<FsToolOptions, 'fileSystem'>

/** Back a session's durable log with one JSONL file. Existing entries replay on start (resume). */
export const jsonlEventLog = (filePath: string, options?: JsonlEventLogOptions): FoldEventLog =>
export const jsonlEventLog = (filePath: string): FoldEventLog =>
eventLogSource(
Effect.gen(function* () {
const fsLayer = Layer.succeed(FileSystem.FileSystem, fileSystemFor(options))
const fs = yield* FileSystem.FileSystem
const fsLayer = Layer.succeed(FileSystem.FileSystem, fs)
const context = yield* Layer.build(layerJsonl(filePath).pipe(Layer.provide(fsLayer)))

return Context.get(context, EventLog)
Expand Down
Loading