opencode-effect-enforcer 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/docs/effect-4.0.0-changelog.md +3213 -0
- package/docs/effect-4.0.0.md +110 -0
- package/guidance/effect-first-development.md +15 -305
- package/guidance/progressive-disclosure-guidance.md +18 -24
- package/package.json +2 -2
- package/patterns/avoid-any.md +2 -2
- package/patterns/avoid-direct-json.md +6 -6
- package/patterns/avoid-native-fetch.md +8 -6
- package/patterns/avoid-node-imports.md +2 -2
- package/patterns/avoid-non-null-assertion.md +2 -2
- package/patterns/avoid-object-type.md +2 -2
- package/patterns/avoid-platform-coupling.md +1 -1
- package/patterns/avoid-process-env.md +3 -4
- package/patterns/avoid-ts-ignore.md +1 -1
- package/patterns/context-tag-extends.md +11 -8
- package/patterns/effect-promise-vs-trypromise.md +6 -7
- package/patterns/prefer-arr-sort.md +2 -2
- package/patterns/prefer-effect-fn.md +21 -65
- package/patterns/prefer-schema-class.md +4 -4
- package/patterns/throw-in-effect-gen.md +1 -1
- package/patterns/use-clock-service.md +4 -0
- package/patterns/use-command-executor-service.md +2 -2
- package/patterns/use-http-client-service.md +8 -6
- package/patterns/use-random-service.md +6 -7
- package/skills/effect-ai-chat/SKILL.md +13 -7
- package/skills/effect-ai-language-model/SKILL.md +50 -21
- package/skills/effect-ai-prompt/SKILL.md +25 -14
- package/skills/effect-ai-provider/SKILL.md +50 -22
- package/skills/effect-ai-streaming/SKILL.md +27 -12
- package/skills/effect-ai-tool/SKILL.md +37 -28
- package/skills/effect-atom-rpc/SKILL.md +57 -36
- package/skills/effect-atom-state/SKILL.md +57 -19
- package/skills/effect-batching/SKILL.md +5 -3
- package/skills/effect-cache/SKILL.md +19 -7
- package/skills/effect-cli/SKILL.md +17 -8
- package/skills/effect-command-executor/SKILL.md +115 -64
- package/skills/effect-concurrency-testing/SKILL.md +26 -6
- package/skills/effect-config/SKILL.md +53 -2
- package/skills/effect-context-witness/SKILL.md +6 -6
- package/skills/effect-domain-modeling/SKILL.md +8 -1
- package/skills/effect-error-handling/SKILL.md +15 -2
- package/skills/effect-fiber/SKILL.md +20 -25
- package/skills/effect-filesystem/SKILL.md +69 -57
- package/skills/effect-http-api/SKILL.md +72 -22
- package/skills/effect-http-client/SKILL.md +25 -21
- package/skills/effect-http-server/SKILL.md +51 -21
- package/skills/effect-incremental-migration/SKILL.md +17 -8
- package/skills/effect-layer-design/SKILL.md +8 -0
- package/skills/effect-managed-runtime/SKILL.md +6 -0
- package/skills/effect-mcp-server/SKILL.md +64 -24
- package/skills/effect-observability/SKILL.md +61 -15
- package/skills/effect-parallelization/SKILL.md +24 -7
- package/skills/effect-path/SKILL.md +8 -2
- package/skills/effect-platform-abstraction/SKILL.md +88 -66
- package/skills/effect-platform-layers/SKILL.md +68 -67
- package/skills/effect-pubsub-event-bus/SKILL.md +56 -60
- package/skills/effect-react-composition/SKILL.md +19 -6
- package/skills/effect-rpc-api/SKILL.md +24 -24
- package/skills/effect-rpc-client/SKILL.md +33 -28
- package/skills/effect-rpc-cluster/SKILL.md +122 -78
- package/skills/effect-rpc-server/SKILL.md +56 -20
- package/skills/effect-scheduling/SKILL.md +29 -1
- package/skills/effect-schema-composition/SKILL.md +31 -13
- package/skills/effect-schema-v4/SKILL.md +94 -10
- package/skills/effect-scope/SKILL.md +13 -5
- package/skills/effect-service-implementation/SKILL.md +1 -1
- package/skills/effect-socket/SKILL.md +52 -8
- package/skills/effect-sql/SKILL.md +67 -33
- package/skills/effect-stream/SKILL.md +50 -5
- package/skills/effect-testing/SKILL.md +91 -2
- package/skills/effect-workflow/SKILL.md +76 -39
- package/src/guidance.ts +0 -1
|
@@ -33,29 +33,31 @@ good url = pipe(
|
|
|
33
33
|
|
|
34
34
|
```haskell
|
|
35
35
|
-- Composable request building
|
|
36
|
-
request ::
|
|
36
|
+
request :: HttpClientRequest
|
|
37
37
|
request = pipe(
|
|
38
|
-
HttpClientRequest.
|
|
39
|
-
HttpClientRequest.
|
|
38
|
+
HttpClientRequest.get("https://api.example.com/users"),
|
|
39
|
+
HttpClientRequest.setHeader("accept", "application/json")
|
|
40
40
|
)
|
|
41
41
|
|
|
42
42
|
-- With retry, timeout, tracing
|
|
43
43
|
resilient :: Effect Response HttpError (HttpClient | Scope)
|
|
44
44
|
resilient = pipe(
|
|
45
|
-
request,
|
|
46
|
-
Effect.flatMap(HttpClient.execute),
|
|
45
|
+
HttpClient.execute(request),
|
|
47
46
|
Effect.retry(Schedule.recurs(3)),
|
|
48
47
|
Effect.timeout(Duration.seconds(10))
|
|
49
48
|
)
|
|
50
49
|
|
|
51
50
|
-- Platform layer at entry point
|
|
52
51
|
main = program.pipe(
|
|
53
|
-
Effect.provide(BunHttpClient.layer) -- or NodeHttpClient.
|
|
52
|
+
Effect.provide(BunHttpClient.layer) -- or NodeHttpClient.layerUndici
|
|
54
53
|
)
|
|
55
54
|
```
|
|
56
55
|
|
|
57
56
|
Native `fetch` produces untyped Promise rejections. Use `HttpClientRequest`, `HttpClientResponse`, and `HttpClient` from Effect for typed errors, composable request building, and testability via layer substitution.
|
|
58
57
|
|
|
58
|
+
Import the HTTP modules from `effect/http`. Retry only proven-idempotent requests;
|
|
59
|
+
do not copy a retry policy onto ordinary non-idempotent POST/PATCH operations.
|
|
60
|
+
|
|
59
61
|
Exception: an explicit low-level platform implementation that cannot use Effect HTTP. Keep it isolated, lift it with `Effect.tryPromise`, propagate the supplied `AbortSignal`, classify status before decoding, and decode unknown bodies with `Schema`.
|
|
60
62
|
|
|
61
63
|
References: EF-9b, Checklist #42 in effect-first-development.md
|
|
@@ -73,12 +73,12 @@ good = do
|
|
|
73
73
|
| `node:fs/promises` | `FileSystem.FileSystem` |
|
|
74
74
|
| `node:path` | `Path.Path` |
|
|
75
75
|
| `node:os` | `FileSystem.makeTempFileScoped` / `Path.Path` / platform layer |
|
|
76
|
-
| `node:child_process` | `ChildProcessSpawner` + `ChildProcess` from `effect/
|
|
76
|
+
| `node:child_process` | `ChildProcessSpawner` + `ChildProcess` from `effect/process` |
|
|
77
77
|
| `node:http` | `HttpClient.HttpClient` |
|
|
78
78
|
| `node:https` | `HttpClient.HttpClient` |
|
|
79
79
|
| `node:stream` | `Stream` from effect |
|
|
80
80
|
| `node:readline` | `Terminal.Terminal` |
|
|
81
|
-
| `node:crypto` | `Crypto` from `effect/
|
|
81
|
+
| `node:crypto` | `Crypto.Crypto` from `effect/Crypto` or a wrapped Effect service |
|
|
82
82
|
|
|
83
83
|
**Exceptions:**
|
|
84
84
|
|
|
@@ -37,8 +37,8 @@ safe :: User → Maybe Email
|
|
|
37
37
|
safe user = user?.contact?.email ?? Nothing
|
|
38
38
|
|
|
39
39
|
-- With Schema for external data
|
|
40
|
-
validated :: Unknown →
|
|
41
|
-
validated = Schema.
|
|
40
|
+
validated :: Unknown → Effect User SchemaError
|
|
41
|
+
validated = Schema.decodeUnknownEffect User
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
The `!` operator is "trust me, this isn't null"—if wrong, runtime crash. Use `?.`, `??`, `Option`, or type guards for safe null handling.
|
|
@@ -39,8 +39,8 @@ good :: User → Effect ()
|
|
|
39
39
|
good user = doSomething user -- clear structure, IDE support
|
|
40
40
|
|
|
41
41
|
-- For unknown shapes
|
|
42
|
-
decode :: Unknown →
|
|
43
|
-
decode = Schema.
|
|
42
|
+
decode :: Unknown → Effect User SchemaError
|
|
43
|
+
decode = Schema.decodeUnknownEffect User
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
`Object` and `{}` provide no type safety—they accept any non-null value. Use explicit types, `Record<K,V>`, `unknown`, or Schema for validation.
|
|
@@ -38,6 +38,6 @@ good = Layer.provide(platformLayer) -- no platform coupling
|
|
|
38
38
|
good = -- runtime provides ChildProcessSpawner, FileSystem, etc.
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Binding packages wrap external systems (CLIs, APIs, databases) and must be platform-agnostic. They should depend on abstract services from `effect` and its
|
|
41
|
+
Binding packages wrap external systems (CLIs, APIs, databases) and must be platform-agnostic. They should depend on abstract services from `effect` and its area entrypoints, such as `FileSystem`, `Path`, `HttpClient` from `effect/http`, and `ChildProcessSpawner` from `effect/process`, rather than hardwiring `@effect/platform-bun` or `@effect/platform-node` concrete implementations.
|
|
42
42
|
|
|
43
43
|
Platform-specific layers (`BunServices.layer`, `NodeServices.layer`) belong in the runtime or CLI entry point, not in bindings. The runtime provides concrete implementations of abstract Effect services there.
|
|
@@ -29,13 +29,12 @@ Config.Redacted :: String -> Config Redacted -- for sensitive values
|
|
|
29
29
|
bad :: Effect String
|
|
30
30
|
bad = Effect.sync \_ -> process.env.API_KEY -- raw side effect
|
|
31
31
|
|
|
32
|
-
good :: Effect String ConfigError
|
|
32
|
+
good :: Effect (Redacted String) ConfigError
|
|
33
33
|
good = Config.Redacted "API_KEY" -- typed, redacted
|
|
34
34
|
|
|
35
|
-
better :: Effect
|
|
35
|
+
better :: Effect Int ConfigError
|
|
36
36
|
better = Config.Int("PORT")
|
|
37
|
-
& Config.withDefault
|
|
38
|
-
& Config.map Number.parse -- with transformation
|
|
37
|
+
& Config.withDefault 3000 -- defaults use the decoded type
|
|
39
38
|
```
|
|
40
39
|
|
|
41
40
|
`process.env` is a raw side effect with no type safety. Use `Config.*` for validated, composable, testable configuration.
|
|
@@ -32,7 +32,7 @@ bad = do
|
|
|
32
32
|
|
|
33
33
|
good :: Effect a
|
|
34
34
|
good = do
|
|
35
|
-
x ← Schema.
|
|
35
|
+
x ← Schema.decodeUnknownEffect schema raw -- prove correctness
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Suppressing errors masks bugs that surface at runtime. Fix the underlying type issue instead.
|
|
@@ -31,7 +31,7 @@ Use `Context.Service` for service definitions. Replace these legacy spellings:
|
|
|
31
31
|
| Legacy API | Era | Replacement |
|
|
32
32
|
| --------------------------- | --------- | -------------------------- |
|
|
33
33
|
| `Context.Tag` / `GenericTag` | pre-v4 | `Context.Service` |
|
|
34
|
-
| `Effect.Service` |
|
|
34
|
+
| `Effect.Service` | v3 | `Context.Service` |
|
|
35
35
|
| `ServiceMap.Service` / `.*` | prerelease | `Context.Service` / `Context.*` |
|
|
36
36
|
|
|
37
37
|
```haskell
|
|
@@ -47,21 +47,24 @@ class ParallelClient extends Effect.Service<ParallelClient>()(...)
|
|
|
47
47
|
class MyService extends ServiceMap.Service<MyService>()("@app/MyService", { ... })
|
|
48
48
|
|
|
49
49
|
-- Fix: Context.Service
|
|
50
|
-
class ParallelClient extends Context.Service<ParallelClient>()(
|
|
50
|
+
class ParallelClient extends Context.Service<ParallelClient, Interface>()(
|
|
51
51
|
"@parallel/ParallelClient"
|
|
52
52
|
)
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
<!-- typecheck -->
|
|
55
56
|
```typescript
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
import { Context, Effect, Layer } from 'effect';
|
|
58
|
+
|
|
59
|
+
class ParallelClient extends Context.Service<ParallelClient, {
|
|
60
|
+
readonly ping: Effect.Effect<string>;
|
|
61
|
+
}>()(
|
|
58
62
|
'@parallel/ParallelClient'
|
|
59
63
|
) {}
|
|
60
64
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
) {}
|
|
65
|
+
const layer = Layer.succeed(ParallelClient, {
|
|
66
|
+
ping: Effect.succeed('pong')
|
|
67
|
+
});
|
|
65
68
|
```
|
|
66
69
|
|
|
67
70
|
When migrating from `ServiceMap`, also update the corresponding module accessors:
|
|
@@ -16,7 +16,7 @@ suggestSkills:
|
|
|
16
16
|
|
|
17
17
|
```haskell
|
|
18
18
|
-- Transformation
|
|
19
|
-
promise :: IO (Promise a) → Effect a ∅ -- rejection = defect
|
|
19
|
+
promise :: IO (Promise a) → Effect a ∅ -- rejection = defect, outside typed E
|
|
20
20
|
tryPromise :: IO (Promise a) → Effect a E -- rejection = typed error (catchable)
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -24,7 +24,7 @@ tryPromise :: IO (Promise a) → Effect a E -- rejection = typed error (c
|
|
|
24
24
|
-- Pattern
|
|
25
25
|
bad :: Effect User ∅
|
|
26
26
|
bad = Effect.promise \_ → fetchUser id
|
|
27
|
-
-- rejection becomes
|
|
27
|
+
-- rejection becomes a defect: catchTag does not handle it
|
|
28
28
|
|
|
29
29
|
good :: Effect User FetchError
|
|
30
30
|
good = Effect.tryPromise
|
|
@@ -37,11 +37,10 @@ good = Effect.tryPromise
|
|
|
37
37
|
handle :: Effect User FetchError → Effect User ∅
|
|
38
38
|
handle = catchTag "FetchError" \e → defaultUser
|
|
39
39
|
|
|
40
|
-
-- Defects
|
|
41
|
-
|
|
42
|
-
defect = id -- can't recover from defects
|
|
40
|
+
-- Defects require explicit cause/defect handling, not ordinary typed recovery
|
|
41
|
+
inspectDefect = Effect.catchDefect
|
|
43
42
|
```
|
|
44
43
|
|
|
45
|
-
`Effect.promise` converts rejections to
|
|
44
|
+
`Effect.promise` converts rejections to defects outside the typed error channel. `Effect.catchDefect` and cause-level handlers can observe them, but expected rejection belongs in `Effect.tryPromise` with a typed error.
|
|
46
45
|
|
|
47
|
-
Any reference to `Effect.promise` is flagged — not just `yield* Effect.promise(...)`.
|
|
46
|
+
Any reference to `Effect.promise` is flagged — not just `yield* Effect.promise(...)`. Review callbacks, returned helpers, and direct calls alike. An explicit boundary whose rejection genuinely represents a defect may intentionally use `Effect.promise`; explain that contract rather than relabeling expected failures as defects.
|
|
@@ -44,9 +44,9 @@ sorted = Arr.sort byNameThenAge
|
|
|
44
44
|
|
|
45
45
|
-- Reverse
|
|
46
46
|
descending :: [User] -> [User]
|
|
47
|
-
descending = Arr.sort (Order.
|
|
47
|
+
descending = Arr.sort (Order.flip byName)
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
Native `.sort()` mutates the array in place and uses an untyped comparator. `Arr.sort` from `effect/Array` returns a new sorted array using a composable, typed `Order`.
|
|
51
51
|
|
|
52
|
-
References: EF-
|
|
52
|
+
References: EF-5 in effect-first-development.md
|
|
@@ -84,74 +84,30 @@ Key details:
|
|
|
84
84
|
|
|
85
85
|
## Complete Before/After
|
|
86
86
|
|
|
87
|
+
<!-- typecheck -->
|
|
87
88
|
```typescript
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
'@services/UserRepository',
|
|
91
|
-
{
|
|
92
|
-
make: Effect.gen(function* () {
|
|
93
|
-
const db = yield* DatabaseClient;
|
|
89
|
+
import { Context, Effect, Layer } from 'effect';
|
|
90
|
+
import * as Schema from 'effect/Schema';
|
|
94
91
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const row = yield* db.query(
|
|
99
|
-
'SELECT * FROM users WHERE id = ?',
|
|
100
|
-
id
|
|
101
|
-
);
|
|
102
|
-
if (!row)
|
|
103
|
-
return yield* new UserNotFound({
|
|
104
|
-
id,
|
|
105
|
-
message: `Not found: ${id}`
|
|
106
|
-
});
|
|
107
|
-
return row as User;
|
|
108
|
-
}),
|
|
109
|
-
|
|
110
|
-
create: (
|
|
111
|
-
data: CreateUserData
|
|
112
|
-
): Effect.Effect<User, DuplicateUser> =>
|
|
113
|
-
Effect.gen(function* () {
|
|
114
|
-
return yield* db.insert('users', data);
|
|
115
|
-
})
|
|
116
|
-
};
|
|
117
|
-
})
|
|
118
|
-
}
|
|
119
|
-
) {}
|
|
120
|
-
|
|
121
|
-
// AFTER — Effect.fn, every method gets a traced span
|
|
122
|
-
export class UserRepository extends Context.Service<UserRepository>()(
|
|
123
|
-
'@services/UserRepository',
|
|
124
|
-
{
|
|
125
|
-
make: Effect.gen(function* () {
|
|
126
|
-
const db = yield* DatabaseClient;
|
|
127
|
-
|
|
128
|
-
const findById = Effect.fn('UserRepository.findById')(
|
|
129
|
-
(id: string): Effect.Effect<User, UserNotFound> =>
|
|
130
|
-
Effect.gen(function* () {
|
|
131
|
-
const row = yield* db.query(
|
|
132
|
-
'SELECT * FROM users WHERE id = ?',
|
|
133
|
-
id
|
|
134
|
-
);
|
|
135
|
-
if (!row)
|
|
136
|
-
return yield* new UserNotFound({
|
|
137
|
-
id,
|
|
138
|
-
message: `Not found: ${id}`
|
|
139
|
-
});
|
|
140
|
-
return row as User;
|
|
141
|
-
})
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
const create = Effect.fn('UserRepository.create')(
|
|
145
|
-
(data: CreateUserData): Effect.Effect<User, DuplicateUser> =>
|
|
146
|
-
Effect.gen(function* () {
|
|
147
|
-
return yield* db.insert('users', data);
|
|
148
|
-
})
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
return { findById, create };
|
|
152
|
-
})
|
|
153
|
-
}
|
|
92
|
+
class User extends Schema.Class<User>('User')({ id: Schema.String }) {}
|
|
93
|
+
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
|
|
94
|
+
'UserNotFound', { id: Schema.String }
|
|
154
95
|
) {}
|
|
96
|
+
class Database extends Context.Service<Database, {
|
|
97
|
+
readonly findUser: (id: string) => Effect.Effect<User, UserNotFound>;
|
|
98
|
+
}>()('app/Database') {}
|
|
99
|
+
class UserRepository extends Context.Service<UserRepository, {
|
|
100
|
+
readonly findById: (id: string) => Effect.Effect<User, UserNotFound>;
|
|
101
|
+
}>()('app/UserRepository') {}
|
|
102
|
+
|
|
103
|
+
const layer = Layer.effect(UserRepository, Effect.gen(function* () {
|
|
104
|
+
const db = yield* Database;
|
|
105
|
+
// Before: (id: string) => Effect.gen(function* () { ... })
|
|
106
|
+
const findById = Effect.fn('UserRepository.findById')(function* (id: string) {
|
|
107
|
+
return yield* db.findUser(id);
|
|
108
|
+
});
|
|
109
|
+
return UserRepository.of({ findById });
|
|
110
|
+
}));
|
|
155
111
|
```
|
|
156
112
|
|
|
157
113
|
## When NOT to use Effect.fn
|
|
@@ -16,7 +16,7 @@ suggestSkills:
|
|
|
16
16
|
|
|
17
17
|
```haskell
|
|
18
18
|
-- Transformation
|
|
19
|
-
Schema.Struct :: { fields } -> Schema { fields } --
|
|
19
|
+
Schema.Struct :: { fields } -> Schema { fields } -- structural value with make helpers
|
|
20
20
|
Schema.Class :: String -> { fields } -> Class -- named, constructable, extensible
|
|
21
21
|
|
|
22
22
|
-- Pattern
|
|
@@ -25,7 +25,7 @@ bad = Schema.Struct({
|
|
|
25
25
|
id: Schema.String,
|
|
26
26
|
name: Schema.String
|
|
27
27
|
})
|
|
28
|
-
--
|
|
28
|
+
-- structural type, make helpers, no class identity / instanceof
|
|
29
29
|
|
|
30
30
|
good :: Schema
|
|
31
31
|
good = class User extends Schema.Class<User>("User")({
|
|
@@ -49,6 +49,6 @@ extend = class Admin extends User.extend<Admin>("Admin")({
|
|
|
49
49
|
}) {}
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
`Schema.Struct
|
|
52
|
+
Every schema, including `Schema.Struct`, has `make`, `makeOption`, and `makeEffect` construction helpers. `Schema.Class` additionally provides a named class, `new` / `instanceof` identity, and class extension. Prefer it for decoded domain/API shapes, union members, and values that need identity. `Schema.Struct` remains appropriate for local structural composition, configuration internals, and schemas where class identity adds no value, so review this informational finding in context.
|
|
53
53
|
|
|
54
|
-
References: EF-3
|
|
54
|
+
References: EF-3 in effect-first-development.md
|
|
@@ -30,7 +30,7 @@ suggestSkills:
|
|
|
30
30
|
|
|
31
31
|
```haskell
|
|
32
32
|
-- Transformation
|
|
33
|
-
throw :: Error -> ⊥ --
|
|
33
|
+
throw :: Error -> ⊥ -- defect, outside the typed error channel
|
|
34
34
|
yield* Effect.fail :: TaggedError -> E ⊥ E -- typed, catchable via catchTag
|
|
35
35
|
```
|
|
36
36
|
|
|
@@ -43,3 +43,7 @@ test = do
|
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Direct `Date` usage is non-deterministic. Use `DateTime.now` or `Clock.currentTimeMillis` for testable time operations via `TestClock`.
|
|
46
|
+
|
|
47
|
+
Use wall-clock time for timestamps and `Clock.monotonicTimeNanos` for elapsed
|
|
48
|
+
durations; wall-clock adjustments must not
|
|
49
|
+
change a measured interval.
|
|
@@ -35,7 +35,7 @@ ChildProcessSpawner :: Effect a ChildProcessSpawner -- typed I/O, scoped lifeti
|
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
```haskell
|
|
38
|
-
-- Pattern (Effect v4 — effect/
|
|
38
|
+
-- Pattern (Effect v4 — effect/process)
|
|
39
39
|
bad :: () → IO String
|
|
40
40
|
bad = exec "git" ["status"] (cb) -- callback, untyped, no cancellation
|
|
41
41
|
|
|
@@ -46,7 +46,7 @@ good = do
|
|
|
46
46
|
-- typed output, error channel, scoped lifetime
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Direct `child_process` imports give you callback APIs, manual lifecycle, and no error channel. Use `ChildProcessSpawner` and `ChildProcess` from `effect/
|
|
49
|
+
Direct `child_process` imports give you callback APIs, manual lifecycle, and no error channel. Use `ChildProcessSpawner` and `ChildProcess` from `effect/process` for typed errors, scoped resource lifetime, and platform-agnostic process spawning.
|
|
50
50
|
|
|
51
51
|
**Exceptions:**
|
|
52
52
|
|
|
@@ -49,26 +49,28 @@ good url = pipe
|
|
|
49
49
|
|
|
50
50
|
```haskell
|
|
51
51
|
-- Composable request building
|
|
52
|
-
request ::
|
|
52
|
+
request :: HttpClientRequest
|
|
53
53
|
request = pipe
|
|
54
|
-
(HttpClientRequest.
|
|
55
|
-
(HttpClientRequest.
|
|
54
|
+
(HttpClientRequest.get "https://api.example.com/users")
|
|
55
|
+
(HttpClientRequest.setHeader "accept" "application/json")
|
|
56
56
|
|
|
57
57
|
-- With retry, timeout, tracing
|
|
58
58
|
resilient :: Effect Response (HttpClient | HttpBodyError)
|
|
59
59
|
resilient = pipe
|
|
60
|
-
request
|
|
61
|
-
(Effect.flatMap HttpClient.execute)
|
|
60
|
+
(HttpClient.execute request)
|
|
62
61
|
(Effect.retry (Schedule.recurs 3))
|
|
63
62
|
(Effect.timeout (Duration.seconds 10))
|
|
64
63
|
|
|
65
64
|
-- Provide platform layer at entry point
|
|
66
65
|
main = program
|
|
67
|
-
& provide BunHttpClient.layer -- or NodeHttpClient.
|
|
66
|
+
& provide BunHttpClient.layer -- or NodeHttpClient.layerUndici
|
|
68
67
|
```
|
|
69
68
|
|
|
70
69
|
Direct `http` / `https` imports give you callback APIs, manual TLS plumbing, and no error channel. Use Effect's `HttpClient`, `HttpClientRequest`, and `HttpClientResponse` for typed errors, composable request building, declarative retry/timeout, and testability via layer substitution.
|
|
71
70
|
|
|
71
|
+
Import these modules from `effect/http`. The retry example assumes an idempotent
|
|
72
|
+
GET; only retry mutations when their idempotency is established by the contract.
|
|
73
|
+
|
|
72
74
|
**Exceptions:**
|
|
73
75
|
|
|
74
76
|
- Platform-specific layers that implement `HttpClient.HttpClient`
|
|
@@ -34,12 +34,11 @@ bad = floor (Math.random * 100) -- R = ∅, untestable
|
|
|
34
34
|
good :: Effect Int Random
|
|
35
35
|
good = Random.nextIntBetween 0 100 -- R ⊃ Random, deterministic in tests
|
|
36
36
|
|
|
37
|
-
--
|
|
38
|
-
|
|
39
|
-
test = do
|
|
40
|
-
TestRandom.feedInts [42, 7, 13] -- deterministic sequence
|
|
41
|
-
result ← good
|
|
42
|
-
assert (result == 42)
|
|
37
|
+
-- Repeatable sequence in tests
|
|
38
|
+
seeded = good & Random.withSeed "example-seed"
|
|
43
39
|
```
|
|
44
40
|
|
|
45
|
-
`Math.random()`
|
|
41
|
+
`Math.random()` bypasses Effect's random service. Use `Random.withSeed` for
|
|
42
|
+
repeatable sequences in tests, or provide a controlled `Random.Random` service
|
|
43
|
+
when the exact generated values matter. `Random.nextIntBetween(min, max)` includes
|
|
44
|
+
both endpoints by default; pass `{ halfOpen: true }` to exclude the upper bound.
|
|
@@ -8,14 +8,17 @@ You are an Effect TypeScript expert specializing in the `Chat` module for statef
|
|
|
8
8
|
## Effect Source Reference
|
|
9
9
|
|
|
10
10
|
The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
|
|
11
|
-
|
|
11
|
+
Use `git show effect@4.0.0:<path>` in that checkout for this skill's baseline;
|
|
12
|
+
main may be ahead. Keep `effect` and `@effect/*` packages on the same version.
|
|
13
|
+
The `effect/ai` APIs are tagged `@stability unstable`: minor releases may break
|
|
14
|
+
them even though the import path no longer contains `unstable`.
|
|
12
15
|
|
|
13
16
|
Reference this for:
|
|
14
17
|
|
|
15
|
-
- Chat module source: `packages/effect/src/
|
|
18
|
+
- Chat module source: `packages/effect/src/ai/Chat.ts`
|
|
16
19
|
- Chat usage examples: `ai-docs/src/71_ai/30_chat.ts`
|
|
17
20
|
- Tool integration examples: `ai-docs/src/71_ai/20_tools.ts`
|
|
18
|
-
- Prompt construction: `packages/effect/src/
|
|
21
|
+
- Prompt construction: `packages/effect/src/ai/Prompt.ts`
|
|
19
22
|
|
|
20
23
|
## Core Imports
|
|
21
24
|
|
|
@@ -28,7 +31,7 @@ import {
|
|
|
28
31
|
Tool,
|
|
29
32
|
Toolkit,
|
|
30
33
|
AiError
|
|
31
|
-
} from 'effect/
|
|
34
|
+
} from 'effect/ai';
|
|
32
35
|
```
|
|
33
36
|
|
|
34
37
|
## What Chat Provides
|
|
@@ -122,7 +125,10 @@ yield* session.generateText({ prompt: [] });
|
|
|
122
125
|
|
|
123
126
|
## Streaming Text
|
|
124
127
|
|
|
125
|
-
`streamText` returns a `Stream` of `Response.StreamPart` values. History is updated
|
|
128
|
+
`streamText` returns a `Stream` of `Response.StreamPart` values. History is updated
|
|
129
|
+
when the stream finalizes by folding the parts received so far. Consume to
|
|
130
|
+
completion to preserve the full response: interrupted text/reasoning sequences
|
|
131
|
+
without an end marker are not folded into history.
|
|
126
132
|
|
|
127
133
|
```ts
|
|
128
134
|
yield*
|
|
@@ -215,7 +221,7 @@ const restored = yield* Chat.fromExport(data);
|
|
|
215
221
|
For automatic persistence (save after every generation), use `Chat.Persistence`:
|
|
216
222
|
|
|
217
223
|
```ts
|
|
218
|
-
import { Persistence } from 'effect/
|
|
224
|
+
import { Persistence } from 'effect/persistence';
|
|
219
225
|
|
|
220
226
|
// Create a persistence layer and provide a BackingPersistence implementation
|
|
221
227
|
const PersistenceLayer = Chat.layerPersisted({ storeId: 'my-chats' }).pipe(
|
|
@@ -459,7 +465,7 @@ Each `Chat` instance uses an internal semaphore with 1 permit, ensuring that onl
|
|
|
459
465
|
|
|
460
466
|
1. **Always provide `LanguageModel.LanguageModel`** — `generateText`, `streamText`, and `generateObject` all require it in context. Provide via `Effect.provide(modelLayer)`.
|
|
461
467
|
2. **Use `prompt: []` in agentic loops** — After the initial prompt, pass an empty prompt to let the model respond based on accumulated history including tool results.
|
|
462
|
-
3. **Import from `effect/
|
|
468
|
+
3. **Import from `effect/ai`** — Chat, Prompt, Tool, Toolkit, LanguageModel, and AiError all come from this path.
|
|
463
469
|
4. **One session = one conversation** — Create separate `Chat` instances for independent conversations. Don't share a session across unrelated threads.
|
|
464
470
|
5. **Export before shutdown** — Use `exportJson` to persist state. Restore with `Chat.fromJson`.
|
|
465
471
|
6. **Provide toolkit handlers** — When using tools, the toolkit's handler layer must be provided (e.g., `Layer.provide(ToolsLayer)`).
|