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.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- 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`
|