opencode-effect-enforcer 0.2.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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,51 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: vm-in-wrong-file
6
+ description: View Model definitions must be in .vm.ts files - detected VM pattern outside of proper location
7
+ glob: '**/!(*.vm).{ts,tsx}'
8
+ pattern: (interface\s+\w+VM\s*\{|Context\.(Service|GenericTag)<\w*VM>|Layer\.(effect|scoped)\(\s*\w+VM)
9
+ level: critical
10
+ suggestSkills:
11
+ - effect-react-vm
12
+ ---
13
+
14
+ # VM Code in Wrong File
15
+
16
+ ```haskell
17
+ -- File structure convention
18
+ data ComponentFiles = ComponentFiles
19
+ { component :: "Component.tsx" -- pure renderer
20
+ , viewModel :: "Component.vm.ts" -- VM definition
21
+ , index :: "index.ts" -- re-exports
22
+ }
23
+
24
+ -- VM file structure
25
+ data VMFile a = VMFile
26
+ { interface :: Interface a -- type contract
27
+ , tag :: GenericTag a -- DI tag
28
+ , layer :: Layer a -- implementation
29
+ }
30
+ ```
31
+
32
+ ```haskell
33
+ -- Anti-pattern: VM in component file
34
+ bad :: "Component.tsx"
35
+ bad = do
36
+ interface ComponentVM { ... } -- ✗ wrong file
37
+ ComponentVM = Context.Service -- ✗ wrong file
38
+ layer = Layer.effect(...) -- ✗ wrong file
39
+
40
+ -- Correct: VM in dedicated file
41
+ good :: "Component.vm.ts"
42
+ good = do
43
+ ComponentVM = Context.Service -- ✓ correct file (Effect v4 beta.46)
44
+ layer = Layer.effect(...) -- ✓ correct file
45
+ export default { service, layer } -- ✓ clean export
46
+
47
+ -- Import in component
48
+ import ComponentVM from "./Component.vm"
49
+ ```
50
+
51
+ VMs must be in `.vm.ts` files. Mixing rendering and state management breaks organization. Invoke `react-vm` skill for guidance.
@@ -0,0 +1,61 @@
1
+ ---
2
+ action: context
3
+ tool: (edit|write)
4
+ event: after
5
+ name: yield-in-for-loop
6
+ description: Use Effect.forEach or STM.forEach instead of yield* in for loops
7
+ glob: '**/*.{ts,tsx}'
8
+ detector: ast
9
+ rule:
10
+ pattern: yield* $A
11
+ inside:
12
+ any:
13
+ - kind: for_statement
14
+ - kind: for_in_statement
15
+ stopBy: end
16
+ level: warning
17
+ ---
18
+
19
+ # Use `Effect.forEach` Instead of For Loops
20
+
21
+ ```haskell
22
+ -- Transformation
23
+ for item in items { yield* process item } -- imperative, not composable
24
+ forEach items process -- declarative, parallelizable
25
+
26
+ -- Operations
27
+ forEach :: [a] → (a → Effect b) → Effect [b]
28
+ filter :: [a] → (a → Effect Bool) → Effect [a]
29
+ traverse :: (a → Effect b) → [a] → Effect [b]
30
+ ```
31
+
32
+ ```haskell
33
+ -- Pattern
34
+ bad :: [Item] → Effect ()
35
+ bad items = for item ← items do
36
+ yield* processItem item -- imperative loop
37
+
38
+ good :: [Item] → Effect ()
39
+ good items = forEach items processItem
40
+
41
+ parallel :: [Item] → Effect ()
42
+ parallel items = forEach items processItem { concurrency: "unbounded" }
43
+
44
+ -- With filtering
45
+ filtered :: [Id] → Effect ()
46
+ filtered ids = do
47
+ active ← filter ids (not <<< alreadyProcessed)
48
+ forEach active process
49
+
50
+ -- Effectful predicate
51
+ effectfulFilter :: [User] → Effect ()
52
+ effectfulFilter users = do
53
+ active ← Effect.filter users (checkUserStatus <<< _.id)
54
+ forEach active sendNotification
55
+ ```
56
+
57
+ For loops with `yield*` are imperative. `Effect.forEach` enables parallel execution, uniform error handling, and composition.
58
+
59
+ All three for-statement shapes are flagged: classic `for (i; cond; step)`, `for...in`, and `for...of`. The `for...of` form is the most common way this anti-pattern appears in Effect code (`for (const item of items) yield* process(item)`) — it loses concurrency control and uniform error handling just like the others.
60
+
61
+ Note: in tree-sitter TypeScript, `for ... of` parses as `for_in_statement` (the kinds are shared), so listing `for_in_statement` covers both `for...in` and `for...of`.
@@ -0,0 +1,472 @@
1
+ ---
2
+ name: effect-ai-chat
3
+ description: Build stateful AI chat sessions with the Effect Chat module. Use this skill when implementing multi-turn conversations, agentic tool-calling loops, chat persistence, streaming chat responses, or structured object generation within a conversation context.
4
+ ---
5
+
6
+ You are an Effect TypeScript expert specializing in the `Chat` module for stateful AI conversations.
7
+
8
+ ## Effect Source Reference
9
+
10
+ The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
11
+ Browse and read files there directly to look up APIs, types, and implementations.
12
+
13
+ Reference this for:
14
+
15
+ - Chat module source: `packages/effect/src/unstable/ai/Chat.ts`
16
+ - Chat usage examples: `ai-docs/src/71_ai/30_chat.ts`
17
+ - Tool integration examples: `ai-docs/src/71_ai/20_tools.ts`
18
+ - Prompt construction: `packages/effect/src/unstable/ai/Prompt.ts`
19
+
20
+ ## Core Imports
21
+
22
+ ```ts
23
+ import { Effect, Layer, Ref, Schema, Context, Stream } from 'effect';
24
+ import {
25
+ Chat,
26
+ Prompt,
27
+ LanguageModel,
28
+ Tool,
29
+ Toolkit,
30
+ AiError
31
+ } from 'effect/unstable/ai';
32
+ ```
33
+
34
+ ## What Chat Provides
35
+
36
+ The `Chat` module wraps `LanguageModel` with automatic conversation history management. Each `Chat` instance:
37
+
38
+ - Maintains a `Ref<Prompt.Prompt>` of accumulated messages
39
+ - Serializes calls via an internal semaphore (one generation at a time)
40
+ - Automatically appends user prompts and model responses to history
41
+ - Supports `generateText`, `streamText`, and `generateObject`
42
+ - Provides `export` / `exportJson` for serialization and `fromExport` / `fromJson` for restoration
43
+
44
+ ## Creating Sessions
45
+
46
+ ### Empty session
47
+
48
+ ```ts
49
+ const session = yield* Chat.empty;
50
+ ```
51
+
52
+ ### With a system prompt
53
+
54
+ ```ts
55
+ const session =
56
+ yield*
57
+ Chat.fromPrompt(
58
+ Prompt.empty.pipe(Prompt.setSystem('You are a helpful assistant.'))
59
+ );
60
+ ```
61
+
62
+ ### From raw message array
63
+
64
+ ```ts
65
+ const session =
66
+ yield*
67
+ Chat.fromPrompt([
68
+ { role: 'system', content: 'You are an assistant that can use tools.' },
69
+ { role: 'user', content: 'Hello!' }
70
+ ]);
71
+ ```
72
+
73
+ ### From serialized JSON (restoring a session)
74
+
75
+ ```ts
76
+ const session = yield* Chat.fromJson(savedJsonString);
77
+ // Or from structured data:
78
+ const session = yield* Chat.fromExport(savedData);
79
+ ```
80
+
81
+ ## Generating Text (Single Turn)
82
+
83
+ Call `session.generateText` with a prompt. The prompt is concatenated with accumulated history, sent to the model, and the response is appended to history automatically.
84
+
85
+ ```ts
86
+ const response =
87
+ yield*
88
+ session
89
+ .generateText({
90
+ prompt: 'What is the capital of France?'
91
+ })
92
+ .pipe(Effect.provide(modelLayer));
93
+
94
+ // response.text — the model's text reply
95
+ // response.content — array of response parts
96
+ // response.toolCalls — any tool calls the model made
97
+ // response.toolResults — resolved tool results
98
+ ```
99
+
100
+ ### Providing the model
101
+
102
+ The `generateText` method requires `LanguageModel.LanguageModel` in its context. Provide it per-call or at the layer level:
103
+
104
+ ```ts
105
+ // Per-call — allows switching models between turns
106
+ const modelLayer = OpenAiLanguageModel.model('gpt-5.2');
107
+ yield*
108
+ session.generateText({ prompt: '...' }).pipe(Effect.provide(modelLayer));
109
+ ```
110
+
111
+ ### Passing prompt as string vs Prompt
112
+
113
+ The `prompt` option accepts `Prompt.RawInput` — a string, an iterable of encoded messages, or a `Prompt.Prompt`. Internally, `Prompt.make(options.prompt)` is called; strings become user text messages and encoded message iterables are decoded.
114
+
115
+ ```ts
116
+ // String shorthand
117
+ yield* session.generateText({ prompt: 'Hello' });
118
+
119
+ // Empty prompt (continue from history alone, useful in agentic loops)
120
+ yield* session.generateText({ prompt: [] });
121
+ ```
122
+
123
+ ## Streaming Text
124
+
125
+ `streamText` returns a `Stream` of `Response.StreamPart` values. History is updated when the stream finalizes. Consume the stream to completion if the full assistant response should become history; if the stream is interrupted early, only the parts emitted before finalization are recorded.
126
+
127
+ ```ts
128
+ yield*
129
+ session
130
+ .streamText({
131
+ prompt: 'Write a story about space'
132
+ })
133
+ .pipe(
134
+ Stream.runForEach((part) =>
135
+ part.type === 'text-delta'
136
+ ? Effect.sync(() => process.stdout.write(part.delta))
137
+ : Effect.void
138
+ ),
139
+ Effect.provide(modelLayer)
140
+ );
141
+ ```
142
+
143
+ ## Generating Structured Objects
144
+
145
+ `generateObject` forces the model to return data conforming to a schema:
146
+
147
+ ```ts
148
+ const ContactSchema = Schema.Struct({
149
+ name: Schema.String,
150
+ email: Schema.String,
151
+ phone: Schema.optional(Schema.String)
152
+ });
153
+
154
+ const result =
155
+ yield*
156
+ session
157
+ .generateObject({
158
+ prompt: 'Extract: John Doe, john@example.com, 555-1234',
159
+ schema: ContactSchema
160
+ })
161
+ .pipe(Effect.provide(modelLayer));
162
+
163
+ // result.value — typed as { name: string; email: string; phone?: string }
164
+ ```
165
+
166
+ ## Conversation History
167
+
168
+ History is stored in `session.history`, a `Ref<Prompt.Prompt>`:
169
+
170
+ ```ts
171
+ // Read current history
172
+ const history = yield* Ref.get(session.history);
173
+ console.log(`${history.content.length} messages in conversation`);
174
+
175
+ // Manually inspect messages
176
+ for (const msg of history.content) {
177
+ console.log(msg.role, msg);
178
+ }
179
+ ```
180
+
181
+ Use `Prompt.fromResponseParts(response.content)` when manually folding model output back into history. It preserves text and reasoning, tool calls/results, approval requests, and skips preliminary tool results. Framework-executed final tool results become tool messages using `encodedResult`; provider-executed final tool results stay in the assistant message.
182
+
183
+ ## Persistence: Export and Restore
184
+
185
+ ### Export to JSON
186
+
187
+ ```ts
188
+ const json = yield* session.exportJson;
189
+ // json is a string — store in database, file system, localStorage, etc.
190
+ ```
191
+
192
+ ### Export to structured data
193
+
194
+ ```ts
195
+ const data = yield* session.export;
196
+ // data is `unknown` — the raw encoded form
197
+ ```
198
+
199
+ ### Restore from JSON
200
+
201
+ ```ts
202
+ const restored = yield* Chat.fromJson(json);
203
+ // Continue conversation from where it left off
204
+ yield* restored.generateText({ prompt: 'What were we discussing?' });
205
+ ```
206
+
207
+ ### Restore from structured data
208
+
209
+ ```ts
210
+ const restored = yield* Chat.fromExport(data);
211
+ ```
212
+
213
+ ## Chat Persistence Service
214
+
215
+ For automatic persistence (save after every generation), use `Chat.Persistence`:
216
+
217
+ ```ts
218
+ import { Persistence } from 'effect/unstable/persistence';
219
+
220
+ // Create a persistence layer and provide a BackingPersistence implementation
221
+ const PersistenceLayer = Chat.layerPersisted({ storeId: 'my-chats' }).pipe(
222
+ Layer.provide(Persistence.layerBackingMemory)
223
+ );
224
+
225
+ // Usage
226
+ const program = Effect.gen(function* () {
227
+ const persistence = yield* Chat.Persistence;
228
+
229
+ // Get existing chat or create new one
230
+ const chat = yield* persistence.getOrCreate('session-123', {
231
+ timeToLive: '1 hour' // optional TTL
232
+ });
233
+
234
+ // chat is a `Chat.Persisted` — same API as Chat.Service but auto-saves
235
+ const response = yield* chat
236
+ .generateText({
237
+ prompt: 'Hello!'
238
+ })
239
+ .pipe(Effect.provide(modelLayer));
240
+
241
+ // History is automatically saved to the backing store after generation
242
+
243
+ // Manual save if needed
244
+ yield* chat.save;
245
+ });
246
+ ```
247
+
248
+ The `Persisted` interface extends `Chat.Service` with:
249
+
250
+ - `id: string` — the chat identifier in the store
251
+ - `save: Effect<void, AiError | PersistenceError>` — manual save trigger
252
+
253
+ Provide a `Persistence.BackingPersistence` implementation (e.g., key-value store, database adapter) to the persistence layer.
254
+
255
+ ## Tool Integration (Agentic Loops)
256
+
257
+ Pass a toolkit to `generateText` to enable tool calling. The Chat module manages the full conversation context including tool call/result messages.
258
+
259
+ ### Define tools and toolkit
260
+
261
+ ```ts
262
+ const Tools = Toolkit.make(
263
+ Tool.make('getCurrentTime', {
264
+ description: 'Get the current time in ISO format',
265
+ parameters: Schema.Struct({ id: Schema.String }),
266
+ success: Schema.String
267
+ })
268
+ );
269
+
270
+ const ToolsLayer = Tools.toLayer(
271
+ Effect.gen(function* () {
272
+ return Tools.of({
273
+ getCurrentTime: Effect.fn('Tools.getCurrentTime')(function* (_) {
274
+ const now = yield* DateTime.now;
275
+ return DateTime.formatIso(now);
276
+ })
277
+ });
278
+ })
279
+ );
280
+ ```
281
+
282
+ ### Agentic loop pattern
283
+
284
+ The model calls tools, results are added to history, and you loop until the model returns a final text answer:
285
+
286
+ ```ts
287
+ const agent = Effect.fn('agent')(function* (question: string) {
288
+ const tools = yield* Tools;
289
+ const session = yield* Chat.fromPrompt([
290
+ { role: 'system', content: 'You are an assistant that can use tools.' },
291
+ { role: 'user', content: question }
292
+ ]);
293
+
294
+ let nextPrompt: Prompt.RawInput = [];
295
+
296
+ while (true) {
297
+ const response = yield* session
298
+ .generateText({
299
+ prompt: nextPrompt, // Empty after the first turn unless approving tools
300
+ toolkit: tools // Provide tools for this turn
301
+ })
302
+ .pipe(Effect.provide(modelLayer));
303
+
304
+ nextPrompt = [];
305
+
306
+ const approvalRequests = response.content.filter(
307
+ (part) => part.type === 'tool-approval-request'
308
+ );
309
+ if (approvalRequests.length > 0) {
310
+ // Append approval responses as a tool message, then call the model again.
311
+ nextPrompt = [
312
+ Prompt.toolMessage({
313
+ content: approvalRequests.map((request) =>
314
+ Prompt.toolApprovalResponsePart({
315
+ approvalId: request.approvalId,
316
+ approved: true // Or false with a reason from policy/user review
317
+ })
318
+ )
319
+ })
320
+ ];
321
+ continue;
322
+ }
323
+
324
+ if (response.toolCalls.length > 0) {
325
+ // Tools were called — results are already in history.
326
+ // Loop back so the model can see the results and decide next step.
327
+ continue;
328
+ }
329
+ // No tool calls or approval requests — model returned a final answer.
330
+ return response.text;
331
+ }
332
+ });
333
+ ```
334
+
335
+ Key points:
336
+
337
+ - Pass `prompt: []` (empty) after the first turn — the model already has the full conversation in history
338
+ - The `toolkit` option makes tools available to the model
339
+ - Tool calls and final tool results are automatically appended to history by `generateText`
340
+ - If a tool has `needsApproval`, the response may contain `tool-approval-request`; append `Prompt.toolApprovalResponsePart` in a tool message and call the model again
341
+ - The loop continues until the model stops calling tools and has no pending approvals
342
+
343
+ ### Registry-Backed Tool Loops
344
+
345
+ In production coding-agent harnesses, a `ToolRegistry` service often sits above the raw toolkit. Let the registry decide which tools are available, how they are described, and which runtime policy applies for the current agent/session.
346
+
347
+ ```typescript
348
+ const registry = yield* ToolRegistry.Service;
349
+ const toolkit = yield* registry.toolkitFor(agentName);
350
+ const promptBlock = yield* registry.descriptionBlock(agentName);
351
+
352
+ const session =
353
+ yield* Chat.fromPrompt(Prompt.empty.pipe(Prompt.setSystem(promptBlock)));
354
+ ```
355
+
356
+ Prefer overriding registry handles or test layers in tests rather than spying on tool modules directly. That keeps tests aligned with production wiring.
357
+
358
+ ## Wrapping Chat in a Domain Service
359
+
360
+ Use `Context.Service` to expose a clean domain API:
361
+
362
+ ```ts
363
+ class AiAssistantError extends Schema.TaggedError<AiAssistantError>()(
364
+ 'AiAssistantError',
365
+ {
366
+ reason: AiError.AiErrorReason
367
+ }
368
+ ) {
369
+ static fromAiError(error: AiError.AiError) {
370
+ return new AiAssistantError({ reason: error.reason });
371
+ }
372
+ }
373
+
374
+ class AiAssistant extends Context.Service<
375
+ AiAssistant,
376
+ {
377
+ chat(message: string): Effect.Effect<string, AiAssistantError>;
378
+ agent(question: string): Effect.Effect<string, AiAssistantError>;
379
+ }
380
+ >()('acme/AiAssistant') {
381
+ static readonly layer = Layer.effect(
382
+ AiAssistant,
383
+ Effect.gen(function* () {
384
+ const model = OpenAiLanguageModel.model('gpt-5.2');
385
+ const modelLayer = yield* model.captureRequirements;
386
+
387
+ // Session lives for the lifetime of the service
388
+ const session = yield* Chat.fromPrompt(
389
+ Prompt.empty.pipe(
390
+ Prompt.setSystem('You are a helpful assistant.')
391
+ )
392
+ );
393
+
394
+ const chat = Effect.fn('AiAssistant.chat')(
395
+ function* (message: string) {
396
+ const response = yield* session
397
+ .generateText({
398
+ prompt: message
399
+ })
400
+ .pipe(Effect.provide(modelLayer));
401
+ return response.text;
402
+ },
403
+ Effect.mapError((error) => AiAssistantError.fromAiError(error))
404
+ );
405
+
406
+ const tools = yield* Tools;
407
+ const agent = Effect.fn('AiAssistant.agent')(
408
+ function* (question: string) {
409
+ const agentSession = yield* Chat.fromPrompt([
410
+ {
411
+ role: 'system',
412
+ content: 'You are an assistant that can use tools.'
413
+ },
414
+ { role: 'user', content: question }
415
+ ]);
416
+ while (true) {
417
+ const response = yield* agentSession
418
+ .generateText({
419
+ prompt: [],
420
+ toolkit: tools
421
+ })
422
+ .pipe(Effect.provide(modelLayer));
423
+ if (response.toolCalls.length > 0) continue;
424
+ return response.text;
425
+ }
426
+ },
427
+ Effect.catchTag(
428
+ 'AiError',
429
+ (error) => Effect.fail(AiAssistantError.fromAiError(error)),
430
+ (e) => Effect.die(e)
431
+ )
432
+ );
433
+
434
+ return AiAssistant.of({ chat, agent });
435
+ })
436
+ ).pipe(Layer.provide([OpenAiClientLayer, ToolsLayer]));
437
+ }
438
+ ```
439
+
440
+ ## Error Handling
441
+
442
+ Chat operations produce `AiError.AiError` errors. Use `catchTag` with the three-argument form (v4 pattern):
443
+
444
+ ```ts
445
+ Effect.catchTag(
446
+ 'AiError',
447
+ (aiError) => Effect.fail(new MyDomainError({ reason: aiError.reason })),
448
+ (unexpectedError) => Effect.die(unexpectedError)
449
+ );
450
+ ```
451
+
452
+ `Chat.fromJson` and `Chat.fromExport` produce `Schema.SchemaError` if the data is malformed.
453
+
454
+ ## Concurrency
455
+
456
+ Each `Chat` instance uses an internal semaphore with 1 permit, ensuring that only one generation runs at a time per session. This prevents race conditions on the shared history ref. Create separate `Chat` instances for parallel conversations.
457
+
458
+ ## Critical Rules
459
+
460
+ 1. **Always provide `LanguageModel.LanguageModel`** — `generateText`, `streamText`, and `generateObject` all require it in context. Provide via `Effect.provide(modelLayer)`.
461
+ 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/unstable/ai`** — Chat, Prompt, Tool, Toolkit, LanguageModel, and AiError all come from this path.
463
+ 4. **One session = one conversation** — Create separate `Chat` instances for independent conversations. Don't share a session across unrelated threads.
464
+ 5. **Export before shutdown** — Use `exportJson` to persist state. Restore with `Chat.fromJson`.
465
+ 6. **Provide toolkit handlers** — When using tools, the toolkit's handler layer must be provided (e.g., `Layer.provide(ToolsLayer)`).
466
+
467
+ ## Anti-Patterns
468
+
469
+ - **Don't manually manage history** — Let Chat handle prompt concatenation. Don't manually `Ref.set` the history unless you have a specific advanced use case.
470
+ - **Don't use `LanguageModel.generateText` directly for multi-turn** — Use `Chat` instead; it handles history accumulation automatically.
471
+ - **Don't forget to provide the model layer** — Every `generateText`/`streamText`/`generateObject` call requires `LanguageModel.LanguageModel` in context.
472
+ - **Don't create a Chat per request if you want continuity** — Keep the session alive across turns for multi-turn conversation.