@effect/sql-sqlite-wasm 4.0.0-beta.98 → 4.0.0-rc.108
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/AGENTS.md +381 -0
- package/CLAUDE.md +381 -0
- package/README.md +10 -3
- package/ai-docs/README.md +44 -0
- package/ai-docs/package.json +36 -0
- package/ai-docs/src/01_effect/01_basics/01_effect-gen.ts +30 -0
- package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +39 -0
- package/ai-docs/src/01_effect/01_basics/10_creating-effects.ts +74 -0
- package/ai-docs/src/01_effect/01_basics/index.md +5 -0
- package/ai-docs/src/01_effect/02_schema/10_schema-basics.ts +43 -0
- package/ai-docs/src/01_effect/02_schema/index.md +7 -0
- package/ai-docs/src/01_effect/03_services/01_service.ts +45 -0
- package/ai-docs/src/01_effect/03_services/10_reference.ts +10 -0
- package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +70 -0
- package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +66 -0
- package/ai-docs/src/01_effect/03_services/index.md +5 -0
- package/ai-docs/src/01_effect/04_errors/01_error-handling.ts +30 -0
- package/ai-docs/src/01_effect/04_errors/10_catch-tags.ts +24 -0
- package/ai-docs/src/01_effect/04_errors/20_reason-errors.ts +64 -0
- package/ai-docs/src/01_effect/04_errors/index.md +1 -0
- package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +105 -0
- package/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts +31 -0
- package/ai-docs/src/01_effect/05_resources/30_layer-map.ts +86 -0
- package/ai-docs/src/01_effect/05_resources/index.md +3 -0
- package/ai-docs/src/01_effect/06_running/10_run-main.ts +30 -0
- package/ai-docs/src/01_effect/06_running/20_layer-launch.ts +27 -0
- package/ai-docs/src/01_effect/06_running/index.md +1 -0
- package/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts +56 -0
- package/ai-docs/src/01_effect/07_pubsub/index.md +3 -0
- package/ai-docs/src/03_stream/10_creating-streams.ts +103 -0
- package/ai-docs/src/03_stream/20_consuming-streams.ts +137 -0
- package/ai-docs/src/03_stream/30_encoding.ts +165 -0
- package/ai-docs/src/03_stream/index.md +4 -0
- package/ai-docs/src/04_integration/10_managed-runtime.ts +129 -0
- package/ai-docs/src/04_integration/index.md +5 -0
- package/ai-docs/src/05_batching/10_request-resolver.ts +89 -0
- package/ai-docs/src/05_batching/index.md +3 -0
- package/ai-docs/src/06_schedule/10_schedules.ts +110 -0
- package/ai-docs/src/06_schedule/index.md +3 -0
- package/ai-docs/src/07_datetime/10_creating-and-formatting.ts +30 -0
- package/ai-docs/src/07_datetime/20_time-zones.ts +44 -0
- package/ai-docs/src/07_datetime/index.md +5 -0
- package/ai-docs/src/08_observability/10_logging.ts +66 -0
- package/ai-docs/src/08_observability/20_otlp-tracing.ts +95 -0
- package/ai-docs/src/08_observability/index.md +7 -0
- package/ai-docs/src/09_testing/10_effect-tests.ts +55 -0
- package/ai-docs/src/09_testing/20_layer-tests.ts +138 -0
- package/ai-docs/src/09_testing/index.md +1 -0
- package/ai-docs/src/10_predicate/01_basics.ts +14 -0
- package/ai-docs/src/10_predicate/index.md +9 -0
- package/ai-docs/src/50_http-client/10_basics.ts +102 -0
- package/ai-docs/src/50_http-client/index.md +3 -0
- package/ai-docs/src/51_http-server/10_basics.ts +116 -0
- package/ai-docs/src/51_http-server/fixtures/api/Api.ts +14 -0
- package/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +36 -0
- package/ai-docs/src/51_http-server/fixtures/api/System.ts +10 -0
- package/ai-docs/src/51_http-server/fixtures/api/Users.ts +91 -0
- package/ai-docs/src/51_http-server/fixtures/domain/User.ts +12 -0
- package/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +22 -0
- package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +36 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +71 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users.ts +62 -0
- package/ai-docs/src/51_http-server/index.md +3 -0
- package/ai-docs/src/60_child-process/10_working-with-child-processes.ts +117 -0
- package/ai-docs/src/60_child-process/index.md +3 -0
- package/ai-docs/src/70_cli/10_basics.ts +136 -0
- package/ai-docs/src/70_cli/index.md +5 -0
- package/ai-docs/src/71_ai/10_language-model.ts +156 -0
- package/ai-docs/src/71_ai/20_tools.ts +226 -0
- package/ai-docs/src/71_ai/30_chat.ts +158 -0
- package/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts +9 -0
- package/ai-docs/src/71_ai/index.md +5 -0
- package/ai-docs/src/80_cluster/10_entities.ts +97 -0
- package/ai-docs/src/80_cluster/index.md +4 -0
- package/ai-docs/src/index.md +10 -0
- package/ai-docs/tsconfig.json +24 -0
- package/dist/OpfsWorker.d.ts +1 -1
- package/dist/OpfsWorker.d.ts.map +1 -1
- package/dist/OpfsWorker.js +2 -2
- package/dist/OpfsWorker.js.map +1 -1
- package/dist/SqliteClient.d.ts +2 -2
- package/dist/SqliteClient.d.ts.map +1 -1
- package/dist/SqliteClient.js +15 -6
- package/dist/SqliteClient.js.map +1 -1
- package/dist/SqliteMigrator.d.ts +2 -2
- package/dist/SqliteMigrator.js +2 -2
- package/dist/SqliteMigrator.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +12 -10
- package/src/OpfsWorker.ts +5 -2
- package/src/SqliteClient.ts +16 -8
- package/src/SqliteMigrator.ts +2 -2
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Working with child processes
|
|
3
|
+
*
|
|
4
|
+
* This example shows how to collect process output, compose pipelines, and stream long-running command output.
|
|
5
|
+
*/
|
|
6
|
+
import { NodeServices } from "@effect/platform-node"
|
|
7
|
+
import { Console, Context, Effect, Layer, Schema, Stream, String } from "effect"
|
|
8
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
|
9
|
+
|
|
10
|
+
export class DevToolsError extends Schema.TaggedError<DevToolsError>()("DevToolsError", {
|
|
11
|
+
cause: Schema.Defect()
|
|
12
|
+
}) {}
|
|
13
|
+
|
|
14
|
+
export class DevTools extends Context.Service<DevTools, {
|
|
15
|
+
readonly nodeVersion: Effect.Effect<string, DevToolsError>
|
|
16
|
+
readonly recentCommitSubjects: Effect.Effect<ReadonlyArray<string>, DevToolsError>
|
|
17
|
+
readonly runLintFix: Effect.Effect<void, DevToolsError>
|
|
18
|
+
changedTypeScriptFiles(baseRef: string): Effect.Effect<ReadonlyArray<string>, DevToolsError>
|
|
19
|
+
}>()("docs/DevTools") {
|
|
20
|
+
static readonly layer = Layer.effect(
|
|
21
|
+
DevTools,
|
|
22
|
+
Effect.gen(function*() {
|
|
23
|
+
// To run child processes, we need access to a `ChildProcessSpawner`.
|
|
24
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
25
|
+
|
|
26
|
+
// Use `spawner.string` when you want to collect the entire output of a
|
|
27
|
+
// command as a string. This runs `node --version` and collects the
|
|
28
|
+
// output.
|
|
29
|
+
const nodeVersion = spawner.string(
|
|
30
|
+
ChildProcess.make("node", ["--version"])
|
|
31
|
+
).pipe(
|
|
32
|
+
Effect.map(String.trim),
|
|
33
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const changedTypeScriptFiles = Effect.fn("DevTools.changedTypeScriptFiles")(function*(baseRef: string) {
|
|
37
|
+
yield* Effect.annotateCurrentSpan({ baseRef })
|
|
38
|
+
|
|
39
|
+
// `spawner.lines` is a convenience helper for line-oriented command
|
|
40
|
+
// output.
|
|
41
|
+
const files = yield* spawner.lines(
|
|
42
|
+
ChildProcess.make("git", ["diff", "--name-only", `${baseRef}...HEAD`])
|
|
43
|
+
).pipe(
|
|
44
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return files.filter((file) => file.endsWith(".ts"))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Build a pipeline from two command values. This runs:
|
|
51
|
+
// `git log --pretty=format:%s -n 20 | head -n 5`
|
|
52
|
+
const recentCommitSubjects = spawner.lines(
|
|
53
|
+
ChildProcess.make("git", ["log", "--pretty=format:%s", "-n", "20"]).pipe(
|
|
54
|
+
ChildProcess.pipeTo(ChildProcess.make("head", ["-n", "5"]))
|
|
55
|
+
)
|
|
56
|
+
).pipe(
|
|
57
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const runLintFix = Effect.gen(function*() {
|
|
61
|
+
// Use `spawn` when you want the process handle and stream output while
|
|
62
|
+
// the process is still running.
|
|
63
|
+
const handle = yield* spawner.spawn(
|
|
64
|
+
ChildProcess.make("pnpm", ["lint-fix"], {
|
|
65
|
+
env: { FORCE_COLOR: "1" },
|
|
66
|
+
extendEnv: true
|
|
67
|
+
})
|
|
68
|
+
).pipe(
|
|
69
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
yield* handle.all.pipe(
|
|
73
|
+
Stream.decodeText(),
|
|
74
|
+
Stream.splitLines,
|
|
75
|
+
Stream.runForEach((line) => Console.log(`[lint-fix] ${line}`)),
|
|
76
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const exitCode = yield* handle.exitCode.pipe(
|
|
80
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
84
|
+
return yield* new DevToolsError({
|
|
85
|
+
cause: new Error(`pnpm lint-fix failed with exit code ${exitCode}`)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}).pipe(
|
|
89
|
+
// `spawner.spawn` adds a `Scope` requirement to manage the lifecycle of
|
|
90
|
+
// the child process. We can use `Effect.scoped` to provide a `Scope`
|
|
91
|
+
// and close it when the effect completes.
|
|
92
|
+
Effect.scoped
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return DevTools.of({
|
|
96
|
+
nodeVersion,
|
|
97
|
+
changedTypeScriptFiles,
|
|
98
|
+
recentCommitSubjects,
|
|
99
|
+
runLintFix
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
).pipe(
|
|
103
|
+
// Provide the `ChildProcessSpawner` dependency from `NodeServices.layer`.
|
|
104
|
+
Layer.provide(NodeServices.layer)
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const program = Effect.gen(function*() {
|
|
109
|
+
const tools = yield* DevTools
|
|
110
|
+
|
|
111
|
+
const version = yield* tools.nodeVersion
|
|
112
|
+
yield* Effect.log(`node=${version}`)
|
|
113
|
+
}).pipe(
|
|
114
|
+
// `ChildProcess` requires a platform implementation of
|
|
115
|
+
// `ChildProcessSpawner`. In Node.js, `NodeServices.layer` provides it.
|
|
116
|
+
Effect.provide(DevTools.layer)
|
|
117
|
+
)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Getting started with Effect CLI modules
|
|
3
|
+
*
|
|
4
|
+
* Build a command-line app with typed arguments and flags, then wire subcommand
|
|
5
|
+
* handlers into a single executable command.
|
|
6
|
+
*/
|
|
7
|
+
import { NodeRuntime, NodeServices } from "@effect/platform-node"
|
|
8
|
+
import { Console, Effect } from "effect"
|
|
9
|
+
import { Argument, Command, Flag } from "effect/unstable/cli"
|
|
10
|
+
|
|
11
|
+
// You can define flags outside of commands and reuse them across multiple
|
|
12
|
+
// commands.
|
|
13
|
+
const workspace = Flag.string("workspace").pipe(
|
|
14
|
+
Flag.withAlias("w"),
|
|
15
|
+
Flag.withDescription("Workspace to operate on"),
|
|
16
|
+
Flag.withDefault("personal")
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// Start with a root command and explicitly share the parent flags that should
|
|
20
|
+
// be available to all subcommands.
|
|
21
|
+
const tasks = Command.make("tasks").pipe(
|
|
22
|
+
Command.withSharedFlags({
|
|
23
|
+
workspace,
|
|
24
|
+
verbose: Flag.boolean("verbose").pipe(
|
|
25
|
+
Flag.withAlias("v"),
|
|
26
|
+
Flag.withDescription("Print diagnostic output")
|
|
27
|
+
)
|
|
28
|
+
}),
|
|
29
|
+
Command.withDescription("Track and manage tasks")
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const create = Command.make(
|
|
33
|
+
"create",
|
|
34
|
+
{
|
|
35
|
+
title: Argument.string("title").pipe(
|
|
36
|
+
Argument.withDescription("Task title")
|
|
37
|
+
),
|
|
38
|
+
priority: Flag.choice("priority", ["low", "normal", "high"]).pipe(
|
|
39
|
+
Flag.withDescription("Priority for the new task"),
|
|
40
|
+
Flag.withDefault("normal")
|
|
41
|
+
)
|
|
42
|
+
},
|
|
43
|
+
Effect.fn(function*({ title, priority }) {
|
|
44
|
+
// Subcommands can read parent command input by yielding the parent command.
|
|
45
|
+
const root = yield* tasks
|
|
46
|
+
|
|
47
|
+
if (root.verbose) {
|
|
48
|
+
yield* Console.log(`workspace=${root.workspace} action=create`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
yield* Console.log(`Created "${title}" in ${root.workspace} with ${priority} priority`)
|
|
52
|
+
})
|
|
53
|
+
).pipe(
|
|
54
|
+
Command.withDescription("Create a task"),
|
|
55
|
+
Command.withExamples([
|
|
56
|
+
{
|
|
57
|
+
command: "tasks create \"Ship 4.0\" --priority high",
|
|
58
|
+
description: "Create a high-priority task"
|
|
59
|
+
}
|
|
60
|
+
])
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const list = Command.make(
|
|
64
|
+
"list",
|
|
65
|
+
{
|
|
66
|
+
status: Flag.choice("status", ["open", "done", "all"]).pipe(
|
|
67
|
+
Flag.withDescription("Filter tasks by status"),
|
|
68
|
+
Flag.withDefault("open")
|
|
69
|
+
),
|
|
70
|
+
json: Flag.boolean("json").pipe(
|
|
71
|
+
Flag.withDescription("Print machine-readable output")
|
|
72
|
+
)
|
|
73
|
+
},
|
|
74
|
+
Effect.fn(function*({ status, json }) {
|
|
75
|
+
const root = yield* tasks
|
|
76
|
+
const items = [
|
|
77
|
+
{ title: "Ship 4.0", status: "open" },
|
|
78
|
+
{ title: "Update onboarding guide", status: "done" }
|
|
79
|
+
] as const
|
|
80
|
+
const filtered = status === "all"
|
|
81
|
+
? items
|
|
82
|
+
: items.filter((item) => item.status === status)
|
|
83
|
+
|
|
84
|
+
if (root.verbose) {
|
|
85
|
+
yield* Console.log(`workspace=${root.workspace} action=list`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (json) {
|
|
89
|
+
yield* Console.log(JSON.stringify(
|
|
90
|
+
{
|
|
91
|
+
workspace: root.workspace,
|
|
92
|
+
status,
|
|
93
|
+
items: filtered
|
|
94
|
+
},
|
|
95
|
+
null,
|
|
96
|
+
2
|
|
97
|
+
))
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
yield* Console.log(`Listing ${status} tasks in ${root.workspace}`)
|
|
102
|
+
if (filtered.length === 0) {
|
|
103
|
+
yield* Console.log("- No tasks found")
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const item of filtered) {
|
|
108
|
+
yield* Console.log(`- ${item.title}`)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
).pipe(
|
|
112
|
+
Command.withDescription("List tasks"),
|
|
113
|
+
Command.withAlias("ls"),
|
|
114
|
+
Command.withExamples([
|
|
115
|
+
{
|
|
116
|
+
command: "tasks --workspace team-a list --status open",
|
|
117
|
+
description: "List open tasks in a specific workspace"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
command: "tasks --workspace team-b ls --status open",
|
|
121
|
+
description: "List open tasks in another workspace"
|
|
122
|
+
}
|
|
123
|
+
])
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
// Finally, compose the subcommands into a single command and then run it.
|
|
127
|
+
tasks.pipe(
|
|
128
|
+
Command.withSubcommands([create, list]),
|
|
129
|
+
Command.run({
|
|
130
|
+
version: "1.0.0"
|
|
131
|
+
}),
|
|
132
|
+
// Provide the services for the platform you are targeting. In this case,
|
|
133
|
+
// Node.js
|
|
134
|
+
Effect.provide(NodeServices.layer),
|
|
135
|
+
NodeRuntime.runMain
|
|
136
|
+
)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Using LanguageModel for text, objects, and streams
|
|
3
|
+
*
|
|
4
|
+
* Configure a provider once, then use `LanguageModel` for plain text
|
|
5
|
+
* generation, schema-validated object generation, and streaming responses.
|
|
6
|
+
*/
|
|
7
|
+
import { AnthropicClient, AnthropicLanguageModel } from "@effect/ai-anthropic"
|
|
8
|
+
import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai"
|
|
9
|
+
import { Config, Context, Effect, ExecutionPlan, Layer, Schema, Stream } from "effect"
|
|
10
|
+
import { AiError, LanguageModel, Model, type Response } from "effect/unstable/ai"
|
|
11
|
+
import { FetchHttpClient } from "effect/unstable/http"
|
|
12
|
+
import { LaunchPlan } from "./fixtures/domain/LaunchPlan.ts"
|
|
13
|
+
|
|
14
|
+
// You can use Config to create ai clients
|
|
15
|
+
const AnthropicClientLayer = AnthropicClient.layerConfig({
|
|
16
|
+
apiKey: Config.redacted("ANTHROPIC_API_KEY")
|
|
17
|
+
}).pipe(
|
|
18
|
+
// Providers typically require an HttpClient, but you can choose which one to
|
|
19
|
+
// use.
|
|
20
|
+
Layer.provide(FetchHttpClient.layer)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const OpenAiClientLayer = OpenAiClient.layerConfig({
|
|
24
|
+
apiKey: Config.redacted("OPENAI_API_KEY")
|
|
25
|
+
}).pipe(
|
|
26
|
+
Layer.provide(FetchHttpClient.layer)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
export class AiWriterError extends Schema.TaggedError<AiWriterError>()("AiWriterError", {
|
|
30
|
+
// AiErrorReason is a Schema, so we can include it directly in our custom
|
|
31
|
+
// error schema.
|
|
32
|
+
reason: AiError.AiErrorReason
|
|
33
|
+
}) {
|
|
34
|
+
static fromAiError(error: AiError.AiError) {
|
|
35
|
+
return new AiWriterError({
|
|
36
|
+
reason: error.reason
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// You can use `ExecutionPlan` to define a strategy for trying multiple
|
|
42
|
+
// providers with different configurations. In this example, we try a cheaper
|
|
43
|
+
// OpenAI model first, then fall back to a more expensive Anthropic model if the
|
|
44
|
+
// first one fails.
|
|
45
|
+
const DraftPlan = ExecutionPlan.make(
|
|
46
|
+
{
|
|
47
|
+
provide: OpenAiLanguageModel.model("gpt-5.2"),
|
|
48
|
+
// Attempt to use the openai model up to 3 times before falling back to the
|
|
49
|
+
// anthropic model.
|
|
50
|
+
attempts: 3
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
provide: AnthropicLanguageModel.model("claude-opus-4-6"),
|
|
54
|
+
attempts: 2
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
export class AiWriter extends Context.Service<AiWriter, {
|
|
59
|
+
draftAnnouncement(product: string): Effect.Effect<{
|
|
60
|
+
readonly provider: string
|
|
61
|
+
readonly text: string
|
|
62
|
+
}, AiWriterError>
|
|
63
|
+
extractLaunchPlan(notes: string): Effect.Effect<LaunchPlan, AiWriterError>
|
|
64
|
+
streamReleaseHighlights(version: string): Stream.Stream<string, AiWriterError>
|
|
65
|
+
}>()("docs/AiWriter") {
|
|
66
|
+
static readonly layer = Layer.effect(
|
|
67
|
+
AiWriter,
|
|
68
|
+
Effect.gen(function*() {
|
|
69
|
+
// Calling `captureRequirements` on an `ExecutionPlan` will move the
|
|
70
|
+
// requirements of the plan (in this case the ai clients) into the Layer
|
|
71
|
+
// requirements.
|
|
72
|
+
const draftsModel = yield* DraftPlan.captureRequirements
|
|
73
|
+
|
|
74
|
+
// Use a different model for the launch plan extraction
|
|
75
|
+
const launchPlanModel = yield* OpenAiLanguageModel.model("gpt-4.1").captureRequirements
|
|
76
|
+
|
|
77
|
+
const draftAnnouncement = Effect.fn("AiWriter.draftAnnouncement")(
|
|
78
|
+
function*(product: string) {
|
|
79
|
+
const model = yield* LanguageModel.LanguageModel
|
|
80
|
+
const provider = yield* Model.ProviderName
|
|
81
|
+
const response = yield* model.generateText({
|
|
82
|
+
prompt: `Write a short launch announcement for ${product}. ` +
|
|
83
|
+
"Keep it concise and include one concrete user benefit."
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// `LanguageModel.generateText` exposes convenience fields so you can
|
|
87
|
+
// inspect usage and finish reason without parsing content parts.
|
|
88
|
+
yield* Effect.logInfo(
|
|
89
|
+
`${provider} finished with ${response.finishReason}. outputTokens=${response.usage.outputTokens.total}`
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
provider,
|
|
94
|
+
text: response.text
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
// To apply an `ExecutionPlan`, we use `Effect.withExecutionPlan`
|
|
98
|
+
Effect.withExecutionPlan(draftsModel),
|
|
99
|
+
// Map AiError into our custom error type
|
|
100
|
+
Effect.mapError((error) => AiWriterError.fromAiError(error))
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const extractLaunchPlan = Effect.fn("AiWriter.extractLaunchPlan")(
|
|
104
|
+
function*(notes: string) {
|
|
105
|
+
const model = yield* LanguageModel.LanguageModel
|
|
106
|
+
const response = yield* model.generateObject({
|
|
107
|
+
objectName: "launch_plan",
|
|
108
|
+
prompt:
|
|
109
|
+
"Convert these notes into a launch plan object with audience, channels, launchDate, summary, and keyRisks:\n" +
|
|
110
|
+
notes,
|
|
111
|
+
// The generated object is validated and decoded through this schema.
|
|
112
|
+
schema: LaunchPlan
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
return response.value
|
|
116
|
+
},
|
|
117
|
+
// The .model(...) apis return a Layer that can be used with
|
|
118
|
+
// Effect.provide
|
|
119
|
+
Effect.provide(launchPlanModel),
|
|
120
|
+
// Map AiError into our custom error type
|
|
121
|
+
Effect.mapError((error) => AiWriterError.fromAiError(error))
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
const streamReleaseHighlights = (version: string) =>
|
|
125
|
+
LanguageModel.streamText({
|
|
126
|
+
prompt: `Write release highlights for version ${version} as a short bulleted list.`
|
|
127
|
+
}).pipe(
|
|
128
|
+
Stream.filter((part): part is Response.TextDeltaPart => part.type === "text-delta"),
|
|
129
|
+
Stream.map((part) => part.delta),
|
|
130
|
+
Stream.provide(launchPlanModel),
|
|
131
|
+
// Map AiError into our custom error type
|
|
132
|
+
Stream.mapError((error) => AiWriterError.fromAiError(error))
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return AiWriter.of({
|
|
136
|
+
draftAnnouncement,
|
|
137
|
+
extractLaunchPlan,
|
|
138
|
+
streamReleaseHighlights
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
).pipe(
|
|
142
|
+
// This Layer has requirements for both the OpenAI and Anthropic clients,
|
|
143
|
+
// since the ExecutionPlan includes models from both providers.
|
|
144
|
+
Layer.provide([OpenAiClientLayer, AnthropicClientLayer])
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// We can now use `AiWriter` like any other Effect service.
|
|
149
|
+
export const program: Effect.Effect<
|
|
150
|
+
void,
|
|
151
|
+
AiWriterError,
|
|
152
|
+
AiWriter
|
|
153
|
+
> = Effect.gen(function*() {
|
|
154
|
+
const writer = yield* AiWriter
|
|
155
|
+
yield* writer.draftAnnouncement("Effect Cloud")
|
|
156
|
+
})
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Defining and using AI tools
|
|
3
|
+
*
|
|
4
|
+
* Define tools with schemas, group them into toolkits, implement handlers,
|
|
5
|
+
* and pass them to `LanguageModel.generateText`.
|
|
6
|
+
*/
|
|
7
|
+
import { OpenAiClient, OpenAiLanguageModel, OpenAiTool } from "@effect/ai-openai"
|
|
8
|
+
import { Config, Context, Effect, Layer, Schema } from "effect"
|
|
9
|
+
import { AiError, LanguageModel, Tool, Toolkit } from "effect/unstable/ai"
|
|
10
|
+
import { FetchHttpClient } from "effect/unstable/http"
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// 1. Defining tools
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
const ProductId = Schema.String.pipe(Schema.brand("ProductId")).annotate({
|
|
17
|
+
description: "A unique identifier for a product, e.g. 'p-123'"
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
class Product extends Schema.Class<Product>("acme/domain/Product")({
|
|
21
|
+
id: ProductId,
|
|
22
|
+
name: Schema.String,
|
|
23
|
+
price: Schema.Finite
|
|
24
|
+
}) {}
|
|
25
|
+
|
|
26
|
+
// Each tool has a name, an optional description, a parameters schema that the
|
|
27
|
+
// model fills in, and a success schema for the handler result. The description
|
|
28
|
+
// is shown to the model to help it decide when to call the tool.
|
|
29
|
+
const SearchProducts = Tool.make("SearchProducts", {
|
|
30
|
+
description: "Search the product catalog by keyword",
|
|
31
|
+
parameters: Schema.Struct({
|
|
32
|
+
query: Schema.String.annotate({
|
|
33
|
+
// Add a description to individual parameters for even better model
|
|
34
|
+
// guidance.
|
|
35
|
+
description: "The search query, e.g. 'wireless headphones'"
|
|
36
|
+
}),
|
|
37
|
+
maxResults: Schema.Natural.pipe(Schema.withDecodingDefault(Effect.succeed(10))).annotate({
|
|
38
|
+
description: "The maximum number of results to return"
|
|
39
|
+
})
|
|
40
|
+
}),
|
|
41
|
+
success: Schema.Array(Product),
|
|
42
|
+
// The strategy used for handling errors returned from tool call handler
|
|
43
|
+
// execution.
|
|
44
|
+
//
|
|
45
|
+
// If set to `"error"` (the default), errors that occur during tool call handler
|
|
46
|
+
// execution will be returned in the error channel of the calling effect.
|
|
47
|
+
//
|
|
48
|
+
// If set to `"return"`, errors that occur during tool call handler execution
|
|
49
|
+
// will be captured and returned as part of the tool call result.
|
|
50
|
+
failureMode: "error"
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const GetInventory = Tool.make("GetInventory", {
|
|
54
|
+
description: "Check current stock level for a product",
|
|
55
|
+
parameters: Schema.Struct({
|
|
56
|
+
productId: ProductId
|
|
57
|
+
}),
|
|
58
|
+
success: Schema.Struct({
|
|
59
|
+
productId: ProductId,
|
|
60
|
+
available: Schema.Natural
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// 2. Grouping tools into a Toolkit
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
// `Toolkit.make` accepts any number of tools and produces a typed toolkit that
|
|
69
|
+
// knows the names and schemas of every tool it contains.
|
|
70
|
+
const ProductToolkit = Toolkit.make(SearchProducts, GetInventory)
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// 3. Implementing handlers via toLayer
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
// `toLayer` returns a `Layer` that satisfies the handler requirements for every
|
|
77
|
+
// tool in the toolkit. Each handler receives the decoded parameters and returns
|
|
78
|
+
// an Effect producing the success type.
|
|
79
|
+
const ProductToolkitLayer = ProductToolkit.toLayer(Effect.gen(function*() {
|
|
80
|
+
yield* Effect.log("Initializing ProductToolkitLive")
|
|
81
|
+
// Here you could access other services or resources needed to implement the
|
|
82
|
+
// handlers, e.g. a database client or external API client.
|
|
83
|
+
//
|
|
84
|
+
// const client = yield* SomeDatabaseClient
|
|
85
|
+
return ProductToolkit.of({
|
|
86
|
+
SearchProducts: Effect.fn("ProductToolkit.SearchProducts")(function*({ query, maxResults }) {
|
|
87
|
+
return [
|
|
88
|
+
new Product({ id: ProductId.make("p-1"), name: `${query} widget`, price: 19.99 }),
|
|
89
|
+
new Product({ id: ProductId.make("p-2"), name: `${query} gadget`, price: 29.99 })
|
|
90
|
+
].slice(0, maxResults)
|
|
91
|
+
}),
|
|
92
|
+
GetInventory: Effect.fn("ProductToolkit.GetInventory")(function*({ productId }) {
|
|
93
|
+
return { productId, available: 42 }
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
}))
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// 4. Using tools with LanguageModel
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
// Provider setup (same pattern as the language-model example).
|
|
103
|
+
const OpenAiClientLayer = OpenAiClient.layerConfig({
|
|
104
|
+
apiKey: Config.redacted("OPENAI_API_KEY")
|
|
105
|
+
}).pipe(Layer.provide(FetchHttpClient.layer))
|
|
106
|
+
|
|
107
|
+
export class ProductAssistantError extends Schema.TaggedError<ProductAssistantError>()(
|
|
108
|
+
"ProductAssistantError",
|
|
109
|
+
{ reason: AiError.AiErrorReason }
|
|
110
|
+
) {}
|
|
111
|
+
|
|
112
|
+
// Wrap tool-enabled generation in a service
|
|
113
|
+
export class ProductAssistant extends Context.Service<ProductAssistant, {
|
|
114
|
+
answer(question: string): Effect.Effect<{
|
|
115
|
+
readonly text: string
|
|
116
|
+
readonly toolCallCount: number
|
|
117
|
+
}, ProductAssistantError>
|
|
118
|
+
}>()("docs/ProductAssistant") {
|
|
119
|
+
static readonly layer = Layer.effect(
|
|
120
|
+
ProductAssistant,
|
|
121
|
+
Effect.gen(function*() {
|
|
122
|
+
// Access the toolkit's handlers by yielding the toolkit definition.
|
|
123
|
+
const toolkit = yield* ProductToolkit
|
|
124
|
+
|
|
125
|
+
// Choose a model to use
|
|
126
|
+
const model = yield* OpenAiLanguageModel.model("gpt-5.2").captureRequirements
|
|
127
|
+
|
|
128
|
+
const answer = Effect.fn("ProductAssistant.answer")(
|
|
129
|
+
function*(question: string) {
|
|
130
|
+
// Pass the toolkit to `generateText`. The model can call any tool in
|
|
131
|
+
// the toolkit; the framework resolves parameters, invokes handlers,
|
|
132
|
+
// and feeds results back automatically.
|
|
133
|
+
const response = yield* LanguageModel.generateText({
|
|
134
|
+
prompt: question,
|
|
135
|
+
toolkit,
|
|
136
|
+
// You can set `toolChoice` to "required" to force the model to call
|
|
137
|
+
// a tool before responding with text.
|
|
138
|
+
//
|
|
139
|
+
// By default it is set to "auto"
|
|
140
|
+
toolChoice: "required"
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// -------------------------------------------------------------------
|
|
144
|
+
// 5. Inspecting tool calls and results
|
|
145
|
+
// -------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
// `response.toolCalls` lists every tool the model invoked, each with
|
|
148
|
+
// the tool name, a unique id, and the decoded parameters.
|
|
149
|
+
for (const call of response.toolCalls) {
|
|
150
|
+
yield* Effect.log(`Tool call: ${call.name} id=${call.id}`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// `response.toolResults` lists the resolved results, each with the
|
|
154
|
+
// tool name, id, decoded result, and an `isFailure` flag.
|
|
155
|
+
for (const result of response.toolResults) {
|
|
156
|
+
yield* Effect.log(
|
|
157
|
+
`Tool result: ${result.name} id=${result.id} isFailure=${result.isFailure}`
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
text: response.text,
|
|
163
|
+
toolCallCount: response.toolCalls.length
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
// Provide the chosen model to use
|
|
167
|
+
Effect.provide(model),
|
|
168
|
+
(_) => _,
|
|
169
|
+
// Map AI errors into our domain error type
|
|
170
|
+
Effect.catchTag(
|
|
171
|
+
"AiError",
|
|
172
|
+
(error) =>
|
|
173
|
+
Effect.fail(
|
|
174
|
+
new ProductAssistantError({
|
|
175
|
+
reason: error.reason
|
|
176
|
+
})
|
|
177
|
+
),
|
|
178
|
+
// For unexpected errors, die with the original error
|
|
179
|
+
(e) => Effect.die(e)
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return ProductAssistant.of({ answer })
|
|
184
|
+
})
|
|
185
|
+
).pipe(
|
|
186
|
+
// The toolkit handler layer must be provided so the framework can invoke
|
|
187
|
+
// the tool handlers when the model makes tool calls.
|
|
188
|
+
Layer.provide(ProductToolkitLayer),
|
|
189
|
+
// Also provide the openai client required by OpenAiLanguageModel.model
|
|
190
|
+
Layer.provide(OpenAiClientLayer)
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// 6. Provider-defined tools
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
// Some providers offer built-in tools (web search, code interpreter, etc.)
|
|
199
|
+
// that run server-side. Use `Tool.providerDefined` or the pre-built
|
|
200
|
+
// definitions from provider packages.
|
|
201
|
+
|
|
202
|
+
// OpenAI's web search tool is pre-defined in `@effect/ai-openai`. Calling it
|
|
203
|
+
// produces a tool instance that can be merged into any toolkit.
|
|
204
|
+
const webSearch = OpenAiTool.WebSearch({
|
|
205
|
+
search_context_size: "medium"
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// Combine user-defined and provider-defined tools in a single toolkit.
|
|
209
|
+
const AssistantToolkit = Toolkit.make(SearchProducts, GetInventory, webSearch)
|
|
210
|
+
|
|
211
|
+
// Only user-defined tools that require handlers appear in `toLayer`. The
|
|
212
|
+
// provider-defined `WebSearch` is executed server-side by the provider.
|
|
213
|
+
export const AssistantToolkitLayer = AssistantToolkit.toLayer(Effect.gen(function*() {
|
|
214
|
+
yield* Effect.log("Initializing AssistantToolkitLive")
|
|
215
|
+
return AssistantToolkit.of({
|
|
216
|
+
SearchProducts: Effect.fn("AssistantToolkit.SearchProducts")(function*({ query, maxResults }) {
|
|
217
|
+
return [
|
|
218
|
+
new Product({ id: ProductId.make("p-1"), name: `${query} widget`, price: 19.99 }),
|
|
219
|
+
new Product({ id: ProductId.make("p-2"), name: `${query} gadget`, price: 29.99 })
|
|
220
|
+
].slice(0, maxResults)
|
|
221
|
+
}),
|
|
222
|
+
GetInventory: Effect.fn("AssistantToolkit.GetInventory")(function*({ productId }) {
|
|
223
|
+
return { productId, available: 42 }
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
}))
|