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,652 @@
1
+ ---
2
+ name: effect-ai-language-model
3
+ description: Master the Effect AI LanguageModel service for text generation, structured output, streaming, and tool calling. Use when working with LLM interactions, schema-validated responses, or building conversational AI systems.
4
+ ---
5
+
6
+ # Effect AI Language Model
7
+
8
+ Pattern guide for working with the LanguageModel service from Effect AI for type-safe LLM interactions with Effect's functional patterns.
9
+
10
+ ## Import Patterns
11
+
12
+ **CRITICAL**: Always use namespace imports:
13
+
14
+ ```typescript
15
+ import * as LanguageModel from 'effect/unstable/ai/LanguageModel';
16
+ import * as Prompt from 'effect/unstable/ai/Prompt';
17
+ import * as Response from 'effect/unstable/ai/Response';
18
+ import * as Toolkit from 'effect/unstable/ai/Toolkit';
19
+ import * as Tool from 'effect/unstable/ai/Tool';
20
+ import * as Effect from 'effect/Effect';
21
+ import * as Stream from 'effect/Stream';
22
+ import * as Schema from 'effect/Schema';
23
+ ```
24
+
25
+ ## When to Use This Skill
26
+
27
+ - Generating text completions from language models
28
+ - Extracting structured data with schema validation
29
+ - Real-time streaming responses for chat interfaces
30
+ - Tool calling and function execution
31
+ - Multi-turn conversations with history
32
+ - Switching between different AI providers
33
+
34
+ ## Service Interface
35
+
36
+ ```haskell
37
+ LanguageModel :: Service
38
+
39
+ -- Core operations
40
+ generateText :: Options → Effect GenerateTextResponse E R
41
+ generateObject :: Options → Schema A → Effect (GenerateObjectResponse A) E R
42
+ streamText :: Options → Stream StreamPart E R
43
+
44
+ -- Service as dependency (LanguageModel is both a namespace and a service tag)
45
+ LanguageModel ∈ R → Effect.gen(function*() {
46
+ -- Option A: use static accessors (adds LanguageModel to R automatically)
47
+ const response = yield* LanguageModel.generateText(options)
48
+ -- Option B: yield the tag explicitly
49
+ const model = yield* LanguageModel.LanguageModel
50
+ const response = yield* model.generateText(options)
51
+ })
52
+ ```
53
+
54
+ ## generateText Pattern
55
+
56
+ Basic text generation with optional tool calling:
57
+
58
+ ```typescript
59
+ import * as LanguageModel from 'effect/unstable/ai/LanguageModel';
60
+ import * as Effect from 'effect/Effect';
61
+
62
+ // Simple text generation
63
+ const simple = LanguageModel.generateText({
64
+ prompt: 'Explain quantum computing'
65
+ });
66
+
67
+ // With system prompt and conversation history
68
+ const withHistory = LanguageModel.generateText({
69
+ prompt: [
70
+ { role: 'system', content: 'You are a helpful assistant' },
71
+ { role: 'user', content: [{ type: 'text', text: 'Hello!' }] }
72
+ ]
73
+ });
74
+
75
+ // With toolkit for tool calling
76
+ const withTools = LanguageModel.generateText({
77
+ prompt: "What's the weather in SF?",
78
+ toolkit: weatherToolkit,
79
+ toolChoice: 'auto' // "none" | "required" | { tool: "name" } | { oneOf: [...] }
80
+ });
81
+
82
+ // Parallel tool call execution
83
+ const withConcurrency = LanguageModel.generateText({
84
+ prompt: 'Search multiple sources',
85
+ toolkit: searchToolkit,
86
+ concurrency: 'unbounded' // or number for limited parallelism
87
+ });
88
+
89
+ // Disable automatic tool call resolution
90
+ const manualTools = LanguageModel.generateText({
91
+ prompt: 'Search for X',
92
+ toolkit: searchToolkit,
93
+ disableToolCallResolution: true // Get encoded tool calls without executing
94
+ });
95
+ ```
96
+
97
+ When `disableToolCallResolution: true`, tool-call `params` are preserved in the schema's **encoded** representation instead of being decoded and then returned. The response type reflects this as `GenerateTextResponse<Tools, true>` and `Response.ToolCallParts<Tools, true>`; streaming uses `Response.StreamPart<Tools, true>`. This matters for transformations such as `Schema.NumberFromString`: manual calls contain the wire value `{ count: '3' }`, not `{ count: 3 }`.
98
+
99
+ Pass those encoded params directly to `toolkit.handle(name, params, toolCallId)` when resolving manually. `Toolkit.handle` now accepts `Tool.ParametersEncoded<T>` and performs the decode before the handler receives `Tool.Parameters<T>`.
100
+
101
+ ### Response Accessors
102
+
103
+ ```typescript
104
+ const response = yield* LanguageModel.generateText({ prompt: '...' });
105
+
106
+ response.text; // string - concatenated text content
107
+ response.toolCalls; // decoded params normally; encoded params when resolution is disabled
108
+ response.toolResults; // Array<ToolResultParts> - tool outputs
109
+ response.finishReason; // "stop" | "length" | "content-filter" | "tool-calls" | "error" | "pause" | "unknown" | "other"
110
+ response.usage; // Usage object with nested structure, e.g. response.usage.outputTokens.total
111
+ response.reasoning; // Array<ReasoningPart> - reasoning steps (when model provides extended thinking)
112
+ response.reasoningText; // string | undefined - concatenated reasoning content
113
+ ```
114
+
115
+ `response.text` concatenates text parts only. Inspect `response.content` when you need reasoning, files/sources, metadata, finish/usage, provider errors, tool calls/results, or `tool-approval-request` parts.
116
+
117
+ With toolkit auto-resolution enabled, normal framework tool calls run and return `tool-result` parts. Tools with `needsApproval` return `tool-approval-request` until the next prompt supplies a matching `Prompt.toolApprovalResponsePart`; approved calls execute, and denied calls become `execution-denied` tool results.
118
+
119
+ When converting `response.content` back into history, `Prompt.fromResponseParts` keeps provider-executed final tool results in the assistant message and places framework-executed final results in a tool message. It skips preliminary results and uses each result's `encodedResult`, preserving the provider's expected conversation shape.
120
+
121
+ ## generateObject Pattern (Structured Output)
122
+
123
+ Force schema-validated output from the model:
124
+
125
+ ```typescript
126
+ import * as LanguageModel from 'effect/unstable/ai/LanguageModel';
127
+ import * as Schema from 'effect/Schema';
128
+ import * as Effect from 'effect/Effect';
129
+
130
+ // Define output schema
131
+ const ContactSchema = Schema.Struct({
132
+ name: Schema.String,
133
+ email: Schema.String,
134
+ phone: Schema.optional(Schema.String)
135
+ });
136
+
137
+ // Generate structured output
138
+ const extractContact = LanguageModel.generateObject({
139
+ prompt: 'Extract: John Doe, john@example.com, 555-1234',
140
+ schema: ContactSchema,
141
+ objectName: 'contact' // Optional, aids model understanding
142
+ });
143
+
144
+ // Usage
145
+ const program = Effect.gen(function* () {
146
+ const response = yield* extractContact;
147
+
148
+ response.value; // { name: "John Doe", email: "john@example.com", phone: "555-1234" }
149
+ response.text; // Raw generated text (JSON)
150
+ response.usage; // Token usage stats
151
+
152
+ return response.value;
153
+ });
154
+ ```
155
+
156
+ ### Schema-driven ADT extraction
157
+
158
+ ```typescript
159
+ const EventType = Schema.TaggedStruct('EventType', {
160
+ _tag: Schema.Literals(['meeting', 'deadline', 'reminder']),
161
+ title: Schema.String,
162
+ date: Schema.String
163
+ });
164
+
165
+ const extractEvent = LanguageModel.generateObject({
166
+ prompt: 'Parse: Team meeting on March 15th',
167
+ schema: EventType
168
+ });
169
+ ```
170
+
171
+ ## streamText Pattern
172
+
173
+ Real-time streaming text generation:
174
+
175
+ ```typescript
176
+ import * as LanguageModel from 'effect/unstable/ai/LanguageModel';
177
+ import * as Stream from 'effect/Stream';
178
+ import * as Effect from 'effect/Effect';
179
+ import * as Console from 'effect/Console';
180
+
181
+ // Basic streaming
182
+ const streamStory = LanguageModel.streamText({
183
+ prompt: 'Write a story about space exploration'
184
+ });
185
+
186
+ // Process stream parts
187
+ const program = streamStory.pipe(
188
+ Stream.runForEach((part) => {
189
+ if (part.type === 'text-delta') {
190
+ return Console.log(part.delta);
191
+ }
192
+ if (part.type === 'tool-params-delta') {
193
+ return Console.log('Tool params:', part.paramsDelta);
194
+ }
195
+ return Effect.void;
196
+ })
197
+ );
198
+ ```
199
+
200
+ ### Common StreamPart Types
201
+
202
+ ```haskell
203
+ Common StreamPart shapes include (non-exhaustive):
204
+ | { type: "text-start", id }
205
+ | { type: "text-delta", id, delta }
206
+ | { type: "text-end", id }
207
+ | { type: "reasoning-start", id }
208
+ | { type: "reasoning-delta", id, delta }
209
+ | { type: "reasoning-end", id }
210
+ | { type: "tool-params-start", id, name }
211
+ | { type: "tool-params-delta", id, paramsDelta }
212
+ | { type: "tool-params-end", id }
213
+ | { type: "tool-call", id, name, params }
214
+ | { type: "tool-result", id, name, result, isFailure, preliminary? }
215
+ | { type: "tool-approval-request", approvalId, toolCallId }
216
+ | { type: "finish", reason: FinishReason, usage: Usage }
217
+ | { type: "error", error: AiError }
218
+ ```
219
+
220
+ Streaming text, reasoning, and tool parameters use matching `id` values across start/delta/end. Providers must not emit standalone `text-delta` parts without a preceding `text-start` and following `text-end`. The full upstream union also includes `file`, document and URL `source`, and `response-metadata` parts.
221
+
222
+ ### Stream Processing Patterns
223
+
224
+ ```typescript
225
+ // Collect all text deltas
226
+ const collectText = streamText.pipe(
227
+ Stream.filter((part) => part.type === 'text-delta'),
228
+ Stream.map((part) => part.delta),
229
+ Stream.runFold('', (acc, delta) => acc + delta)
230
+ );
231
+
232
+ // Process chunks efficiently
233
+ const processChunks = streamText.pipe(
234
+ Stream.mapChunksEffect((chunk) =>
235
+ Effect.gen(function* () {
236
+ const parts = Array.from(chunk);
237
+ // Process batch of parts
238
+ yield* handleBatch(parts);
239
+ return chunk;
240
+ })
241
+ )
242
+ );
243
+
244
+ // Aggregate response with side effects
245
+ let combined: Array<StreamPart> = [];
246
+ const aggregated = streamText.pipe(
247
+ Stream.mapChunks((chunk) => {
248
+ combined = [...combined, ...chunk];
249
+ return chunk;
250
+ }),
251
+ Stream.ensuring(
252
+ Effect.sync(() => {
253
+ // Finalization logic with full response
254
+ console.log('Total parts:', combined.length);
255
+ })
256
+ )
257
+ );
258
+ ```
259
+
260
+ ## toolChoice Options
261
+
262
+ Control when and which tools the model can use:
263
+
264
+ ```typescript
265
+ // Auto-decide (default)
266
+ toolChoice: "auto" // Model decides whether to call tools
267
+
268
+ // Never use tools
269
+ toolChoice: "none" // Force text-only response
270
+
271
+ // Must use a tool
272
+ toolChoice: "required" // Model must call at least one tool
273
+
274
+ // Specific tool required
275
+ toolChoice: { tool: "search" } // Must call "search" tool
276
+
277
+ // Restricted subset - auto mode
278
+ toolChoice: {
279
+ oneOf: ["search", "calculate"] // Can use these tools or respond with text
280
+ }
281
+
282
+ // Restricted subset - required mode
283
+ toolChoice: {
284
+ mode: "required",
285
+ oneOf: ["search", "calculate"] // Must call one of these tools
286
+ }
287
+ ```
288
+
289
+ ## Error Handling
290
+
291
+ ```typescript
292
+ import * as AiError from 'effect/unstable/ai/AiError';
293
+
294
+ const robust = LanguageModel.generateText({
295
+ prompt: 'Analyze this...'
296
+ }).pipe(
297
+ Effect.catchTag('AiError', (error) => {
298
+ // Handle all AI errors — match on error.reason._tag for specific cases:
299
+ // "RateLimitError", "InvalidOutputError", "StructuredOutputError",
300
+ // "AuthenticationError", "ContentPolicyError", etc.
301
+ return Effect.succeed(fallbackResponse);
302
+ })
303
+ );
304
+ ```
305
+
306
+ ## Type Extraction Utilities
307
+
308
+ ```typescript
309
+ import type * as LanguageModel from 'effect/unstable/ai/LanguageModel';
310
+
311
+ // Extract error types from options
312
+ type MyError = LanguageModel.ExtractError<typeof options>;
313
+
314
+ // Extract service requirements from options
315
+ type MyRequirements = LanguageModel.ExtractServices<typeof options>;
316
+
317
+ // true only when options has literal disableToolCallResolution: true
318
+ type EncodedParams = LanguageModel.ExtractEncodedToolParameters<typeof options>;
319
+
320
+ // Inferred based on:
321
+ // - toolkit: Toolkit.WithHandler<Tools> → Tool.HandlerError<Tools> ∈ E
322
+ // - toolkit: Effect<Toolkit, E, R> → E | Tool.HandlerError<Tools> ∈ E, R ∈ R
323
+ // - disableToolCallResolution: true → no Tool.HandlerError in E
324
+ // and no handler/result-decoding services in R; tool-call params remain encoded
325
+ ```
326
+
327
+ ## Provider Implementation Pattern
328
+
329
+ Create custom LanguageModel providers using `LanguageModel.make`:
330
+
331
+ ```haskell
332
+ make :: ConstructorParams → Effect Service
333
+ ```
334
+
335
+ When implementing a custom LanguageModel provider, return encoded parts: `Array<Response.PartEncoded>` for `generateText` and `Stream<Response.StreamPartEncoded>` for `streamText`. If you emit `response-metadata`, encode timestamps as ISO strings. Providers that support provider-side conversations should honor `ProviderOptions.previousResponseId` and `ProviderOptions.incrementalPrompt`; providers that cannot should intentionally ignore them.
336
+
337
+ ```typescript
338
+ import * as LanguageModel from 'effect/unstable/ai/LanguageModel';
339
+ import * as Response from 'effect/unstable/ai/Response';
340
+
341
+ const makeCustomProvider = Effect.gen(function* () {
342
+ const service = yield* LanguageModel.make({
343
+ generateText: (options: LanguageModel.ProviderOptions) =>
344
+ Effect.gen(function* () {
345
+ // options.prompt: Prompt.Prompt
346
+ // options.tools: ReadonlyArray<Tool.Any>
347
+ // options.toolChoice: ToolChoice<any>
348
+ // options.responseFormat: { type: "text" } | { type: "json", schema, objectName }
349
+ // options.span: Span (for telemetry)
350
+ // options.previousResponseId: string | undefined
351
+ // options.incrementalPrompt: Prompt.Prompt | undefined
352
+
353
+ const result = yield* callProviderAPI(options);
354
+
355
+ // Return Response.PartEncoded[]
356
+ return [
357
+ Response.makePart('text', { text: result.content }),
358
+ Response.makePart('finish', {
359
+ reason: 'stop',
360
+ usage: new Response.Usage({
361
+ inputTokens: {
362
+ total: result.usage.input,
363
+ uncached: undefined,
364
+ cacheRead: undefined,
365
+ cacheWrite: undefined
366
+ },
367
+ outputTokens: {
368
+ total: result.usage.output,
369
+ text: undefined,
370
+ reasoning: undefined
371
+ }
372
+ }),
373
+ response: undefined
374
+ })
375
+ ];
376
+ }),
377
+
378
+ streamText: (_options: LanguageModel.ProviderOptions) => {
379
+ const textId = 'custom-text-1';
380
+ return Stream.fromIterable<Response.StreamPartEncoded>([
381
+ { type: 'text-start', id: textId },
382
+ { type: 'text-delta', id: textId, delta: 'Hello' },
383
+ { type: 'text-delta', id: textId, delta: ' world' },
384
+ { type: 'text-end', id: textId },
385
+ Response.makePart('finish', {
386
+ reason: 'stop',
387
+ usage: new Response.Usage({
388
+ inputTokens: {
389
+ total: undefined,
390
+ uncached: undefined,
391
+ cacheRead: undefined,
392
+ cacheWrite: undefined
393
+ },
394
+ outputTokens: {
395
+ total: undefined,
396
+ text: undefined,
397
+ reasoning: undefined
398
+ }
399
+ }),
400
+ response: undefined
401
+ })
402
+ ]);
403
+ }
404
+ });
405
+
406
+ return service;
407
+ });
408
+ ```
409
+
410
+ ## Common Patterns
411
+
412
+ ### Multi-turn with context
413
+
414
+ ```typescript
415
+ const conversation = Effect.gen(function* () {
416
+ let history: Prompt.Prompt = Prompt.empty;
417
+
418
+ const ask = (message: string) =>
419
+ Effect.gen(function* () {
420
+ const prompt = Prompt.concat(history, Prompt.make(message));
421
+ const response = yield* LanguageModel.generateText({ prompt });
422
+ history = Prompt.concat(
423
+ prompt,
424
+ Prompt.fromResponseParts(response.content)
425
+ );
426
+ return response.text;
427
+ });
428
+
429
+ const answer1 = yield* ask('What is TypeScript?');
430
+ const answer2 = yield* ask('How does it differ from JavaScript?');
431
+
432
+ return { answer1, answer2 };
433
+ });
434
+ ```
435
+
436
+ ### Parallel requests
437
+
438
+ ```typescript
439
+ const parallel = Effect.all(
440
+ [
441
+ LanguageModel.generateText({ prompt: 'Summarize A' }),
442
+ LanguageModel.generateText({ prompt: 'Summarize B' }),
443
+ LanguageModel.generateText({ prompt: 'Summarize C' })
444
+ ],
445
+ { concurrency: 'unbounded' }
446
+ );
447
+ ```
448
+
449
+ ### Retry with backoff
450
+
451
+ ```typescript
452
+ const resilient = LanguageModel.generateText({ prompt: '...' }).pipe(
453
+ Effect.retry({
454
+ times: 3,
455
+ schedule: Schedule.exponential('100 millis')
456
+ })
457
+ );
458
+ ```
459
+
460
+ ## Common Pitfall: LanguageModel Inside Services
461
+
462
+ `LanguageModel` is both a **namespace** (module with static functions) and a **service tag**. The static functions like `LanguageModel.generateText(...)` are accessors that add `LanguageModel` to the `R` context of the returned effect.
463
+
464
+ When calling `LanguageModel.generateText(...)` inside a service's layer construction, `LanguageModel` will leak into the service method's return type as a requirement, causing circular type issues.
465
+
466
+ ```typescript
467
+ // ❌ WRONG — LanguageModel leaks into service method signatures
468
+ export class MyService extends Context.Service<
469
+ MyService,
470
+ {
471
+ readonly doSomething: (text: string) => Effect.Effect<string, AiError>;
472
+ }
473
+ >()('MyService') {
474
+ // Using LanguageModel.generateText accessor adds LanguageModel to R
475
+ static readonly layer = Layer.effect(
476
+ this,
477
+ Effect.gen(function* () {
478
+ const doSomething = Effect.fn('MyService.doSomething')(
479
+ (text: string): Effect.Effect<string, AiError> =>
480
+ // This adds LanguageModel to R, making the method signature wrong
481
+ LanguageModel.generateText({ prompt: text }).pipe(
482
+ Effect.map((r) => r.text)
483
+ )
484
+ );
485
+ return { doSomething };
486
+ })
487
+ );
488
+ }
489
+
490
+ // ✅ CORRECT — Capture LanguageModel in the closure, expose clean signatures
491
+ export class MyService extends Context.Service<
492
+ MyService,
493
+ {
494
+ readonly doSomething: (text: string) => Effect.Effect<string, AiError>;
495
+ }
496
+ >()('MyService') {
497
+ static readonly layer = Layer.effect(
498
+ this,
499
+ Effect.gen(function* () {
500
+ // Yield the service tag at construction time — captured in closure
501
+ const lm = yield* LanguageModel.LanguageModel;
502
+
503
+ const doSomething = Effect.fn('MyService.doSomething')(
504
+ (text: string): Effect.Effect<string, AiError> =>
505
+ lm
506
+ .generateText({ prompt: text })
507
+ .pipe(Effect.map((r) => r.text))
508
+ );
509
+ return { doSomething };
510
+ })
511
+ );
512
+ }
513
+ ```
514
+
515
+ **Alternative**: If the service method SHOULD require `LanguageModel` in its context (caller provides it), that's fine — just be explicit about it in the return type:
516
+
517
+ ```typescript
518
+ const doSomething = Effect.fn('MyService.doSomething')(
519
+ (
520
+ text: string
521
+ ): Effect.Effect<string, AiError, LanguageModel.LanguageModel> =>
522
+ LanguageModel.generateText({ prompt: text }).pipe(
523
+ Effect.map((r) => r.text)
524
+ )
525
+ );
526
+ ```
527
+
528
+ ## Anti-patterns
529
+
530
+ ```typescript
531
+ // ❌ yield* LanguageModel (namespace, not the tag)
532
+ const model = yield* LanguageModel // ERROR: LanguageModel is a namespace
533
+
534
+ // ✅ yield* LanguageModel.LanguageModel (the actual service tag)
535
+ const model = yield* LanguageModel.LanguageModel
536
+
537
+ // ❌ Nested callbacks
538
+ LanguageModel.generateText({ prompt: "A" }).pipe(
539
+ Effect.flatMap((r1) =>
540
+ LanguageModel.generateText({ prompt: "B" }).pipe(
541
+ Effect.flatMap((r2) => ...)
542
+ )
543
+ )
544
+ )
545
+
546
+ // ✅ Effect.gen
547
+ Effect.gen(function* () {
548
+ const r1 = yield* LanguageModel.generateText({ prompt: "A" })
549
+ const r2 = yield* LanguageModel.generateText({ prompt: "B" })
550
+ return combine(r1, r2)
551
+ })
552
+
553
+ // ❌ Manual error construction
554
+ Effect.fail(new Error("Failed"))
555
+
556
+ // ✅ Tagged errors
557
+ Effect.fail(AiError.make({
558
+ module: "MyService",
559
+ method: "generate",
560
+ reason: new AiError.UnknownError({ description: "Failed" })
561
+ }))
562
+
563
+ // ❌ Promise-based streaming
564
+ streamText.pipe(Stream.runCollect, Effect.map(toPromise))
565
+
566
+ // ✅ Effect-based consumption
567
+ streamText.pipe(Stream.runForEach(processPart))
568
+
569
+ // ❌ Ignoring finishReason
570
+ const text = response.text // May be truncated
571
+
572
+ // ✅ Check finish reason
573
+ if (response.finishReason === "length") {
574
+ // Handle truncation
575
+ }
576
+ ```
577
+
578
+ ## Quality Checklist
579
+
580
+ - [ ] Use `generateText` for single-turn completions
581
+ - [ ] Use `generateObject` with Schema for structured output
582
+ - [ ] Use `streamText` for real-time streaming responses
583
+ - [ ] Check `finishReason` to detect truncation
584
+ - [ ] Handle errors with `catchTag("AiError", ...)`
585
+ - [ ] Use `Effect.gen` over flatMap chains
586
+ - [ ] Access service via `yield* LanguageModel.LanguageModel` (tag) or use static accessors like `LanguageModel.generateText`
587
+ - [ ] Provide toolkit for tool calling
588
+ - [ ] Set appropriate `toolChoice` mode
589
+ - [ ] Use `concurrency` for parallel tool execution
590
+
591
+ ## v4 Features
592
+
593
+ ### ExecutionPlan (Multi-Provider Fallback)
594
+
595
+ Use `ExecutionPlan` from `effect/ExecutionPlan` to define multi-provider fallback strategies:
596
+
597
+ ```typescript
598
+ import * as ExecutionPlan from 'effect/ExecutionPlan';
599
+
600
+ const plan = ExecutionPlan.make(
601
+ { provide: AnthropicLayer, attempts: 3 },
602
+ { provide: OpenAILayer, attempts: 2 }
603
+ );
604
+ ```
605
+
606
+ This allows automatic failover between providers with configurable retry attempts per provider.
607
+
608
+ Observe fallback behavior with the lifecycle hook instead of instrumenting each provider separately:
609
+
610
+ ```typescript
611
+ const generated = LanguageModel.generateText({ prompt: '...' }).pipe(
612
+ Effect.withExecutionPlan(plan, {
613
+ onEvent: (event) =>
614
+ Effect.logDebug('language model attempt').pipe(
615
+ Effect.annotateLogs({
616
+ event: event._tag,
617
+ attempt: event.attempt,
618
+ stepAttempt: event.stepAttempt,
619
+ stepIndex: event.stepIndex
620
+ })
621
+ })
622
+ );
623
+ ```
624
+
625
+ `AttemptFailure` contains the full `Cause`; every `AttemptStart` is paired with one success/failure terminal event, including interruption. Event handlers are awaited in order and their defects are ignored so observation cannot alter fallback outcomes.
626
+
627
+ ### Model.ProviderName
628
+
629
+ Inside an effect that runs with a language model provider, you can retrieve the current provider name:
630
+
631
+ ```typescript
632
+ import * as Model from 'effect/unstable/ai/Model';
633
+
634
+ const program = Effect.gen(function* () {
635
+ const providerName = yield* Model.ProviderName;
636
+ yield* Effect.log(`Using provider: ${providerName}`);
637
+ });
638
+ ```
639
+
640
+ ## Related Skills
641
+
642
+ - effect-ai-prompt - Constructing and composing prompts
643
+ - effect-ai-tool - Creating tools and toolkits
644
+ - effect-ai-streaming - Processing stream responses
645
+ - effect-ai-provider - Configuring provider layers
646
+
647
+ ## References
648
+
649
+ - Source: `packages/effect/src/unstable/ai/LanguageModel.ts`
650
+ - Chat integration: `packages/effect/src/unstable/ai/Chat.ts`
651
+ - Response types: `effect/unstable/ai/Response`
652
+ - Tool system: `effect/unstable/ai/Tool`, `effect/unstable/ai/Toolkit`