From 5d430348125ce5e8d37353ed322a7656def305e5 Mon Sep 17 00:00:00 2001 From: Lloyd Richards Date: Fri, 24 Apr 2026 11:13:27 +0200 Subject: [PATCH 1/4] chore: upgrade effect language service and improve ts config --- .vscode/settings.json | 4 ++-- apps/client/tsconfig.json | 7 +------ apps/server-mcp/tsconfig.json | 7 +------ apps/server/tsconfig.json | 7 +------ bun.lock | 9 ++++++++- package.json | 2 ++ packages/config-typescript/base.json | 18 ++++++++++++++++-- tsconfig.json | 5 +++++ 8 files changed, 36 insertions(+), 23 deletions(-) create mode 100644 tsconfig.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 3954dde..aecd4d0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,4 @@ { - "typescript.tsdk": "node_modules/typescript/lib", "editor.defaultFormatter": "esbenp.prettier-vscode", "[javascript][typescript][javascriptreact][typescriptreact][json][jsonc][css][graphql]": { "editor.defaultFormatter": "biomejs.biome" @@ -14,5 +13,6 @@ "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" }, - "js/ts.tsdk.path": "node_modules/typescript/lib" + "js/ts.tsdk.path": "./node_modules/typescript/lib", + "js/ts.tsdk.promptToUseWorkspaceVersion": true } diff --git a/apps/client/tsconfig.json b/apps/client/tsconfig.json index 3ae9f01..55ab063 100644 --- a/apps/client/tsconfig.json +++ b/apps/client/tsconfig.json @@ -7,12 +7,7 @@ "paths": { "@/*": ["./src/*"], "~/*": ["./public/*"] - }, - "plugins": [ - { - "name": "@effect/language-service" - } - ] + } }, "references": [{ "path": "./tsconfig.config.json" }], "include": ["src", "test"], diff --git a/apps/server-mcp/tsconfig.json b/apps/server-mcp/tsconfig.json index 0d48597..e3e6a17 100644 --- a/apps/server-mcp/tsconfig.json +++ b/apps/server-mcp/tsconfig.json @@ -3,12 +3,7 @@ "compilerOptions": { "outDir": "dist", "noEmit": true, - "types": ["@types/bun"], - "plugins": [ - { - "name": "@effect/language-service" - } - ] + "types": ["@types/bun"] }, "references": [{ "path": "../../packages/domain" }], "include": ["src/**/*"], diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 11e36b9..a64227b 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -4,12 +4,7 @@ "rootDir": "../..", "outDir": "dist", "noEmit": true, - "types": ["@types/bun"], - "plugins": [ - { - "name": "@effect/language-service" - } - ] + "types": ["@types/bun"] }, "include": ["src/**/*", "../../packages/ai/src/LanguageModel.ts"], "exclude": ["node_modules", "dist"] diff --git a/bun.lock b/bun.lock index 399fb3f..9df9edf 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "base_bevr-stack", "devDependencies": { "@biomejs/biome": "2.4.8", + "@effect/language-service": "0.85.1", "@playwright/test": "^1.58.2", "@types/bun": "^1.3.11", "turbo": "^2.8.20", @@ -261,7 +262,7 @@ "@effect/experimental": ["@effect/experimental@0.60.0", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/platform": "^0.96.0", "effect": "^3.21.0", "ioredis": "^5", "lmdb": "^3" }, "optionalPeers": ["ioredis", "lmdb"] }, "sha512-i5zIg7Xup2KgHyqHlYtkgqSE1bNzCL0GbbTQxrpIzKF0q/ebknOk/ox8B/gIq2vImjoEE81h/oxU+6i1NH210g=="], - "@effect/language-service": ["@effect/language-service@0.81.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-7MYFsq9w9l2MkUw5/33fiG3YAkgnT6U1mwV0QvhokhnLhPW9cIetwAHNtXwsgr5omPQheLuflTIAFvPaZLQcPw=="], + "@effect/language-service": ["@effect/language-service@0.85.1", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-EXnJjIy6zQ3nUO/MZ+ynWUb8B895KZPotd1++oTs9JjDkplwM7cb6zo8Zq2zU6piwq+KflO7amXbEfj1UMpHkw=="], "@effect/opentelemetry": ["@effect/opentelemetry@0.63.0", "", { "peerDependencies": { "@effect/platform": "^0.96.0", "@opentelemetry/api": "^1.9", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^3.21.0" } }, "sha512-2yUG2QWNATi1uKP0kwhaP5eLp+c5NDzAL3EOpIcGLBAC0cbXZrx4n9Qw/QwUKxpuV+pbhrBUPCiByyWAFKfuCw=="], @@ -1609,6 +1610,8 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "client/@effect/language-service": ["@effect/language-service@0.81.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-7MYFsq9w9l2MkUw5/33fiG3YAkgnT6U1mwV0QvhokhnLhPW9cIetwAHNtXwsgr5omPQheLuflTIAFvPaZLQcPw=="], + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1637,6 +1640,10 @@ "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + "server/@effect/language-service": ["@effect/language-service@0.81.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-7MYFsq9w9l2MkUw5/33fiG3YAkgnT6U1mwV0QvhokhnLhPW9cIetwAHNtXwsgr5omPQheLuflTIAFvPaZLQcPw=="], + + "server-mcp/@effect/language-service": ["@effect/language-service@0.81.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-7MYFsq9w9l2MkUw5/33fiG3YAkgnT6U1mwV0QvhokhnLhPW9cIetwAHNtXwsgr5omPQheLuflTIAFvPaZLQcPw=="], + "vitest/vite": ["vite@8.0.1", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.10", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw=="], "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], diff --git a/package.json b/package.json index a97e625..7c1ea87 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,13 @@ "lint": "biome lint .", "format": "biome check --write .", "format:check": "biome check .", + "prepare": "effect-language-service patch", "test": "turbo run test", "test:e2e": "playwright test", "type-check": "turbo run type-check" }, "devDependencies": { + "@effect/language-service": "0.85.1", "@biomejs/biome": "2.4.8", "@playwright/test": "^1.58.2", "@types/bun": "^1.3.11", diff --git a/packages/config-typescript/base.json b/packages/config-typescript/base.json index f0a6ba2..010a0dc 100644 --- a/packages/config-typescript/base.json +++ b/packages/config-typescript/base.json @@ -1,5 +1,5 @@ { - "$schema": "https://json.schemastore.org/tsconfig", + "$schema": "../../node_modules/@effect/language-service/schema.json", "display": "Default", "compilerOptions": { "moduleResolution": "bundler", @@ -29,7 +29,21 @@ "sourceMap": true, "allowImportingTsExtensions": false, "verbatimModuleSyntax": false, - "skipLibCheck": true + "skipLibCheck": true, + "plugins": [ + { + "name": "@effect/language-service", + "barrelImportPackages": ["effect"], + "includeSuggestionsInTsc": true, + "quickinfoMaximumLength": 1200, + "diagnosticSeverity": { + "cryptoRandomUUIDInEffect": "suggestion", + "globalDateInEffect": "suggestion", + "layerMergeAllWithDependencies": "warning", + "missingEffectServiceDependency": "warning" + } + } + ] }, "exclude": ["node_modules", "dist", "build", ".next", "coverage"] } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c974311 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,5 @@ +{ + "$schema": "./node_modules/@effect/language-service/schema.json", + "extends": "./packages/config-typescript/base.json", + "files": [] +} From 62db972e30cee4e339f6243f47ee255bfb3c7c96 Mon Sep 17 00:00:00 2001 From: Lloyd Richards Date: Fri, 24 Apr 2026 11:39:47 +0200 Subject: [PATCH 2/4] refactor: replace Date.now with DateTime.now for type-safe timestamps --- apps/server/src/Rpc/Presence.ts | 4 ++-- packages/ai/src/toolkits/SampleToolkit.ts | 12 +++++++----- packages/domain/src/WebSocket.ts | 8 ++++---- packages/presence/src/services/PresenceService.ts | 10 +++++++--- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/apps/server/src/Rpc/Presence.ts b/apps/server/src/Rpc/Presence.ts index 998fc70..7704d53 100644 --- a/apps/server/src/Rpc/Presence.ts +++ b/apps/server/src/Rpc/Presence.ts @@ -4,7 +4,7 @@ import { WebSocketRpc, } from "@repo/domain/WebSocket"; import { PresenceService } from "@repo/presence"; -import { Effect, Mailbox, Queue, Stream } from "effect"; +import { DateTime, Effect, Mailbox, Queue, Stream } from "effect"; export const PresenceRpcLive = WebSocketRpc.toLayer( Effect.gen(function* () { @@ -16,7 +16,7 @@ export const PresenceRpcLive = WebSocketRpc.toLayer( yield* Effect.log("New presence subscription"); const clientId = presence.generateClientId(); - const connectedAt = Date.now(); + const connectedAt = yield* DateTime.now; const clientInfo: ClientInfo = { clientId, status: "online", diff --git a/packages/ai/src/toolkits/SampleToolkit.ts b/packages/ai/src/toolkits/SampleToolkit.ts index cd938c8..2f17bd9 100644 --- a/packages/ai/src/toolkits/SampleToolkit.ts +++ b/packages/ai/src/toolkits/SampleToolkit.ts @@ -1,5 +1,5 @@ import { Tool, Toolkit } from "@effect/ai"; -import { Effect, Schema } from "effect"; +import { DateTime, Effect, Schema } from "effect"; /** * Calculator Tool - Safely evaluates mathematical expressions @@ -77,13 +77,15 @@ export const SampleToolkitLive = SampleToolkit.toLayer( getCurrentTime: () => Effect.gen(function* () { - const now = new Date(); - const timeString = now.toLocaleString("en-US", { - timeZone: "UTC", + const now = yield* DateTime.now; + const timeString = DateTime.formatUtc(now, { + locale: "en-US", + dateStyle: "medium", + timeStyle: "medium", }); yield* Effect.log(`Current time (UTC): ${timeString}`); return yield* Effect.succeed( - `Current time in UTC: ${timeString} (ISO: ${now.toISOString()})`, + `Current time in UTC: ${timeString} (ISO: ${DateTime.formatIso(now)})`, ); }), }; diff --git a/packages/domain/src/WebSocket.ts b/packages/domain/src/WebSocket.ts index 4c424e0..a90168c 100644 --- a/packages/domain/src/WebSocket.ts +++ b/packages/domain/src/WebSocket.ts @@ -9,7 +9,7 @@ export type ClientStatus = Schema.Schema.Type; export const ClientInfo = Schema.Struct({ clientId: ClientId, status: ClientStatus, - connectedAt: Schema.Number, + connectedAt: Schema.DateTimeUtcFromNumber, }); export type ClientInfo = Schema.Schema.Type; @@ -17,7 +17,7 @@ export const WebSocketEvent = Schema.Union( // Initial connection acknowledgment with assigned ClientId Schema.TaggedStruct("connected", { clientId: ClientId, - connectedAt: Schema.Number, + connectedAt: Schema.DateTimeUtcFromNumber, }), // Broadcast when a user joins Schema.TaggedStruct("user_joined", { @@ -27,12 +27,12 @@ export const WebSocketEvent = Schema.Union( Schema.TaggedStruct("status_changed", { clientId: ClientId, status: ClientStatus, - changedAt: Schema.Number, + changedAt: Schema.DateTimeUtcFromNumber, }), // Broadcast when a user disconnects Schema.TaggedStruct("user_left", { clientId: ClientId, - disconnectedAt: Schema.Number, + disconnectedAt: Schema.DateTimeUtcFromNumber, }), ); export type WebSocketEvent = Schema.Schema.Type; diff --git a/packages/presence/src/services/PresenceService.ts b/packages/presence/src/services/PresenceService.ts index a71422c..133a7af 100644 --- a/packages/presence/src/services/PresenceService.ts +++ b/packages/presence/src/services/PresenceService.ts @@ -4,7 +4,7 @@ import { type ClientStatus, type WebSocketEvent, } from "@repo/domain/WebSocket"; -import { Effect, PubSub, Ref } from "effect"; +import { DateTime, Effect, PubSub, Ref } from "effect"; export type PresenceEventType = typeof WebSocketEvent.Type; @@ -43,6 +43,8 @@ export class PresenceService extends Effect.Service()( const client = clients.get(clientId); if (client) { + const disconnectedAt = yield* DateTime.now; + yield* Ref.update(clientsRef, (clients) => { const newClients = new Map(clients); newClients.delete(clientId); @@ -52,7 +54,7 @@ export class PresenceService extends Effect.Service()( yield* PubSub.publish(pubsub, { _tag: "user_left", clientId, - disconnectedAt: Date.now(), + disconnectedAt, }); yield* Effect.log(`Client removed: ${clientId}`); @@ -68,6 +70,8 @@ export class PresenceService extends Effect.Service()( const client = clients.get(clientId); if (client) { + const changedAt = yield* DateTime.now; + const updatedClient: ClientInfo = { ...client, status, @@ -84,7 +88,7 @@ export class PresenceService extends Effect.Service()( _tag: "status_changed", clientId, status, - changedAt: Date.now(), + changedAt, }); yield* Effect.log(`Client ${clientId} status changed to ${status}`); From 1c6dada0db8fc62767d7fef116a5e18eae171df7 Mon Sep 17 00:00:00 2001 From: Lloyd Richards Date: Fri, 24 Apr 2026 11:47:06 +0200 Subject: [PATCH 3/4] refactor: replace JSON.stringify with Inspectable --- packages/ai/src/workflow/AgenticLoop.ts | 29 +++++++++++++---------- packages/ai/src/workflow/MailboxEvents.ts | 7 ++++-- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/ai/src/workflow/AgenticLoop.ts b/packages/ai/src/workflow/AgenticLoop.ts index 85bdaaf..bec88ad 100644 --- a/packages/ai/src/workflow/AgenticLoop.ts +++ b/packages/ai/src/workflow/AgenticLoop.ts @@ -1,14 +1,8 @@ import type { Chat, Tool, Toolkit } from "@effect/ai"; import type { ChatStreamPart } from "@repo/domain/Chat"; -import { Effect, type Mailbox, Ref, Schema, Stream } from "effect"; +import { Effect, Inspectable, type Mailbox, Ref, Schema, Stream } from "effect"; import { createMailboxEvents } from "./MailboxEvents"; -export const AgenticLoopState = Schema.Struct({ - finishReason: Schema.String, - iteration: Schema.Number, -}); -// Schema for parsing tool parameters (JSON string -> object with unknown keys/values) - export const ToolParamsSchema = Schema.parseJson( Schema.Record({ key: Schema.String, value: Schema.Unknown }), ); @@ -109,7 +103,7 @@ const loop = >({ )(toolCall.params?.trim() || "{}").pipe( Effect.tapError((error) => Effect.logError( - `Failed to parse tool arguments for ${toolCall.name}: ${JSON.stringify(error)}`, + `Failed to parse tool arguments for ${toolCall.name}: ${Inspectable.toStringUnknown(error, 2)}`, ), ), Effect.orElseSucceed(() => ({})), @@ -127,11 +121,15 @@ const loop = >({ } case "tool-result": { - const resultText = part.isFailure - ? part.result - : typeof part.result === "string" + const resultText = + part.isFailure || typeof part.result === "string" ? part.result - : JSON.stringify(part.result); + : yield* Effect.orElseSucceed( + Schema.encode(Schema.parseJson({ space: 2 }))( + part.result, + ), + () => Inspectable.toStringUnknown(part.result, 2), + ); if (part.isFailure) { yield* Effect.logError( @@ -162,7 +160,12 @@ const loop = >({ yield* events.error( typeof part.error === "string" ? part.error - : JSON.stringify(part.error), + : yield* Effect.orElseSucceed( + Schema.encode(Schema.parseJson({ space: 2 }))( + part.error, + ), + () => Inspectable.toStringUnknown(part.error, 2), + ), false, ); break; diff --git a/packages/ai/src/workflow/MailboxEvents.ts b/packages/ai/src/workflow/MailboxEvents.ts index a4f739e..9c538de 100644 --- a/packages/ai/src/workflow/MailboxEvents.ts +++ b/packages/ai/src/workflow/MailboxEvents.ts @@ -1,5 +1,5 @@ import type { ChatStreamPart } from "@repo/domain/Chat"; -import { Effect, type Mailbox } from "effect"; +import { Effect, Inspectable, type Mailbox, Schema } from "effect"; /** * MailboxEvents - Typed event emitter for ChatStreamPart @@ -34,7 +34,10 @@ export const createMailboxEvents = ( }); // Delta (stream arguments as JSON) - const argsJson = JSON.stringify(params.arguments, null, 2); + const argsJson = yield* Effect.orElseSucceed( + Schema.encode(Schema.parseJson({ space: 2 }))(params.arguments), + () => Inspectable.toStringUnknown(params.arguments, 2), + ); yield* mailbox.offer({ _tag: "tool-call-delta", id, From 259b9d9bba1463586793ce9e8e5c250d8b39f3e0 Mon Sep 17 00:00:00 2001 From: Lloyd Richards Date: Fri, 24 Apr 2026 11:58:26 +0200 Subject: [PATCH 4/4] refactor: use appropriate log levels --- apps/client/src/lib/atoms/tick-atom.ts | 2 +- apps/client/src/lib/web-socket-client.ts | 2 +- apps/server-mcp/src/index.ts | 2 +- apps/server/src/Rpc/Event.ts | 6 +-- apps/server/src/Rpc/Presence.ts | 10 ++-- apps/server/src/index.test.ts | 2 +- apps/server/src/index.ts | 12 ++--- packages/ai/src/services/ChatService.ts | 6 +-- packages/ai/src/toolkits/SampleToolkit.ts | 6 +-- packages/ai/src/workflow/AgenticLoop.ts | 2 +- packages/observability/AGENTS.md | 1 + packages/observability/README.md | 8 +++ packages/observability/src/index.ts | 52 +++++++++++++++++-- .../presence/src/services/PresenceService.ts | 10 ++-- 14 files changed, 87 insertions(+), 34 deletions(-) diff --git a/apps/client/src/lib/atoms/tick-atom.ts b/apps/client/src/lib/atoms/tick-atom.ts index eaf1283..e831037 100644 --- a/apps/client/src/lib/atoms/tick-atom.ts +++ b/apps/client/src/lib/atoms/tick-atom.ts @@ -11,7 +11,7 @@ export const tickAtom: Atom.AtomResultFn< > = runtime.fn(({ abort = false }: { readonly abort?: boolean }) => Stream.unwrap( Effect.gen(function* () { - yield* Effect.log("Starting Tick Atom Stream"); + yield* Effect.logDebug("Starting Tick Atom Stream"); const rpc = yield* RpcClient; return rpc.client.tick({ ticks: 10 }); }).pipe((self) => (abort ? Effect.interrupt : self)), diff --git a/apps/client/src/lib/web-socket-client.ts b/apps/client/src/lib/web-socket-client.ts index 0ec5b8f..5675213 100644 --- a/apps/client/src/lib/web-socket-client.ts +++ b/apps/client/src/lib/web-socket-client.ts @@ -27,7 +27,7 @@ export const presenceSubscriptionAtom: Atom.AtomResultFn< RpcClientError | Cause.NoSuchElementException > = WebSocketClient.runtime.fn(() => Effect.gen(function* () { - yield* Effect.log("Starting presence subscription stream"); + yield* Effect.logDebug("Starting presence subscription stream"); const client = yield* WebSocketClient; return client("subscribe", {}); }).pipe( diff --git a/apps/server-mcp/src/index.ts b/apps/server-mcp/src/index.ts index 6ad05ff..069d619 100644 --- a/apps/server-mcp/src/index.ts +++ b/apps/server-mcp/src/index.ts @@ -92,7 +92,7 @@ const DevToolsLive = Effect.gen(function* () { if (!config.enableDevTools) { return Layer.empty; } - yield* Effect.log("Enabling DevTools Layer"); + yield* Effect.logInfo("Enabling DevTools Layer"); return DevTools.layer(); }).pipe(Layer.unwrapEffect); diff --git a/apps/server/src/Rpc/Event.ts b/apps/server/src/Rpc/Event.ts index 9bfb0e9..b375677 100644 --- a/apps/server/src/Rpc/Event.ts +++ b/apps/server/src/Rpc/Event.ts @@ -6,10 +6,10 @@ import { Effect, Mailbox } from "effect"; export const EventRpcLive = EventRpc.toLayer( Effect.gen(function* () { const bot = yield* ChatService; - yield* Effect.log("Starting Event RPC Live Implementation"); + yield* Effect.logInfo("Starting Event RPC Live Implementation"); return { tick: Effect.fn(function* (payload) { - yield* Effect.log("Creating new tick stream"); + yield* Effect.logDebug("Creating new tick stream"); const mailbox = yield* Mailbox.make(); yield* Effect.forkScoped( Effect.gen(function* () { @@ -20,7 +20,7 @@ export const EventRpcLive = EventRpc.toLayer( yield* mailbox.offer({ _tag: "tick" }); } yield* mailbox.offer({ _tag: "end" }); - yield* Effect.log("End event sent"); + yield* Effect.logDebug("End event sent"); }).pipe(Effect.ensuring(mailbox.end)), ); return mailbox; diff --git a/apps/server/src/Rpc/Presence.ts b/apps/server/src/Rpc/Presence.ts index 7704d53..fd5edf9 100644 --- a/apps/server/src/Rpc/Presence.ts +++ b/apps/server/src/Rpc/Presence.ts @@ -9,11 +9,11 @@ import { DateTime, Effect, Mailbox, Queue, Stream } from "effect"; export const PresenceRpcLive = WebSocketRpc.toLayer( Effect.gen(function* () { const presence = yield* PresenceService; - yield* Effect.log("Starting Presence RPC Live Implementation"); + yield* Effect.logInfo("Starting Presence RPC Live Implementation"); return { subscribe: Effect.fn(function* () { - yield* Effect.log("New presence subscription"); + yield* Effect.logDebug("New presence subscription"); const clientId = presence.generateClientId(); const connectedAt = yield* DateTime.now; @@ -49,7 +49,7 @@ export const PresenceRpcLive = WebSocketRpc.toLayer( yield* Queue.shutdown(subscription); yield* presence.removeClient(clientId); yield* mailbox.end; - yield* Effect.log( + yield* Effect.logDebug( `Presence subscription ended for ${clientId}`, ); }), @@ -82,7 +82,7 @@ export const PresenceRpcLive = WebSocketRpc.toLayer( }), setStatus: Effect.fn(function* (payload) { - yield* Effect.log( + yield* Effect.logDebug( `Setting status for ${payload.clientId} to ${payload.status}`, ); yield* presence.setStatus(payload.clientId, payload.status); @@ -91,7 +91,7 @@ export const PresenceRpcLive = WebSocketRpc.toLayer( getPresence: Effect.fn(function* () { const clients = yield* presence.getClients(); - yield* Effect.log(`Returning ${clients.length} clients`); + yield* Effect.logDebug(`Returning ${clients.length} clients`); return { clients: [...clients] }; }), }; diff --git a/apps/server/src/index.test.ts b/apps/server/src/index.test.ts index d1e0298..61a2d0b 100644 --- a/apps/server/src/index.test.ts +++ b/apps/server/src/index.test.ts @@ -42,7 +42,7 @@ describe("Server", () => { it.effect("can log messages", () => Effect.gen(function* () { // Act: Run an effect that logs (logs are suppressed in test context) - yield* Effect.log("Test log message"); + yield* Effect.logDebug("Test log message"); // Assert: Effect completes successfully expect(true).toBe(true); diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 17ccfde..74cebab 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -72,7 +72,7 @@ const DevToolsLive = Effect.gen(function* () { if (!config.enableDevTools) { return Layer.empty; } - yield* Effect.log("Enabling DevTools Layer"); + yield* Effect.logInfo("Enabling DevTools Layer"); return DevTools.layer(); }).pipe(Layer.unwrapEffect); @@ -80,11 +80,11 @@ const HttpLive = Effect.gen(function* () { const config = yield* ServerConfig; const allowedOrigins = config.allowedOrigins.split(",").map((o) => o.trim()); - yield* Effect.log(`CORS allowed origins: ${allowedOrigins.join(", ")}`); - yield* Effect.log("Starting server with:"); - yield* Effect.log(" - HTTP API at /"); - yield* Effect.log(" - HTTP RPC at /rpc (EventRpc)"); - yield* Effect.log(" - WebSocket RPC at /ws (PresenceRpc)"); + yield* Effect.logInfo(`CORS allowed origins: ${allowedOrigins.join(", ")}`); + yield* Effect.logInfo("Starting server with:"); + yield* Effect.logInfo(" - HTTP API at /"); + yield* Effect.logInfo(" - HTTP RPC at /rpc (EventRpc)"); + yield* Effect.logInfo(" - WebSocket RPC at /ws (PresenceRpc)"); const AllRouters = Layer.mergeAll( ApiRouter, diff --git a/packages/ai/src/services/ChatService.ts b/packages/ai/src/services/ChatService.ts index 6c64621..3f3949f 100644 --- a/packages/ai/src/services/ChatService.ts +++ b/packages/ai/src/services/ChatService.ts @@ -15,7 +15,7 @@ export class ChatService extends Effect.Service()("ChatService", { // Fork the agentic loop to run in background yield* Effect.forkScoped( Effect.gen(function* () { - yield* Effect.log( + yield* Effect.logInfo( `[craftsman] Creating chat with ${1 + history.length} messages`, ); const systemMessage = String.stripMargin(` @@ -28,10 +28,10 @@ export class ChatService extends Effect.Service()("ChatService", { Prompt.make(history).pipe(Prompt.setSystem(systemMessage)), ); - yield* Effect.log( + yield* Effect.logTrace( Prompt.make(history).pipe(Prompt.setSystem(systemMessage)), ); - yield* Effect.log(yield* session.exportJson); + yield* Effect.logTrace(yield* session.exportJson); const toolkit = yield* Toolkit.merge(SampleToolkit); diff --git a/packages/ai/src/toolkits/SampleToolkit.ts b/packages/ai/src/toolkits/SampleToolkit.ts index 2f17bd9..03997c7 100644 --- a/packages/ai/src/toolkits/SampleToolkit.ts +++ b/packages/ai/src/toolkits/SampleToolkit.ts @@ -42,7 +42,7 @@ export const SampleToolkitLive = SampleToolkit.toLayer( return { calculate: (params) => Effect.gen(function* () { - yield* Effect.log(`Calculating: ${params.expression}`); + yield* Effect.logDebug(`Calculating: ${params.expression}`); // Simple safe evaluation for basic math // Whitelist allowed characters @@ -71,7 +71,7 @@ export const SampleToolkitLive = SampleToolkit.toLayer( echo: (params) => Effect.gen(function* () { - yield* Effect.log(`Echo: ${params.message}`); + yield* Effect.logDebug(`Echo: ${params.message}`); return yield* Effect.succeed(`Echo: ${params.message}`); }), @@ -83,7 +83,7 @@ export const SampleToolkitLive = SampleToolkit.toLayer( dateStyle: "medium", timeStyle: "medium", }); - yield* Effect.log(`Current time (UTC): ${timeString}`); + yield* Effect.logDebug(`Current time (UTC): ${timeString}`); return yield* Effect.succeed( `Current time in UTC: ${timeString} (ISO: ${DateTime.formatIso(now)})`, ); diff --git a/packages/ai/src/workflow/AgenticLoop.ts b/packages/ai/src/workflow/AgenticLoop.ts index bec88ad..12506f3 100644 --- a/packages/ai/src/workflow/AgenticLoop.ts +++ b/packages/ai/src/workflow/AgenticLoop.ts @@ -213,7 +213,7 @@ export const runAgenticLoop = >({ const finishReason = yield* loop({ chat, mailbox, toolkit }); - yield* Effect.log( + yield* Effect.logDebug( `Iteration ${iteration} completed with finishReason: ${finishReason}`, ); diff --git a/packages/observability/AGENTS.md b/packages/observability/AGENTS.md index 40f7d9f..09bd46d 100644 --- a/packages/observability/AGENTS.md +++ b/packages/observability/AGENTS.md @@ -9,6 +9,7 @@ environment variables are set and no-ops otherwise. ## Environment +- `LOG_LEVEL` - `OTEL_EXPORTER_OTLP_ENDPOINT` - `OTEL_SERVICE_NAME` diff --git a/packages/observability/README.md b/packages/observability/README.md index 5f36149..f84da0c 100644 --- a/packages/observability/README.md +++ b/packages/observability/README.md @@ -9,9 +9,14 @@ providing environment variables instead of wiring exporters per app. ## Environment +- `LOG_LEVEL` - `OTEL_EXPORTER_OTLP_ENDPOINT` - `OTEL_SERVICE_NAME` +`LOG_LEVEL` controls the minimum runtime log level for the whole Effect runtime. +Supported values: `All`, `Trace`, `Debug`, `Info`, `Warning`, `Error`, +`Fatal`, `None`. The parser also accepts `warn`. + When both are set, tracing is enabled and spans are exported via OTLP over HTTP. If either is missing, tracing is disabled with a log message. @@ -29,7 +34,10 @@ const HttpLive = HttpLayerRouter.serve(Router).pipe( ## API +- `LogLevelLive`: Layer that loads `LOG_LEVEL` from config and applies it as the + runtime minimum log level. - `ObservabilityLive`: Layer that configures NodeSdk when env vars are set. + It also applies `LogLevelLive`. - `Observability`: re-export of `NodeSdk` for advanced configuration. ## Removing From Apps diff --git a/packages/observability/src/index.ts b/packages/observability/src/index.ts index 8bad174..f54217b 100644 --- a/packages/observability/src/index.ts +++ b/packages/observability/src/index.ts @@ -1,7 +1,38 @@ import { NodeSdk } from "@effect/opentelemetry"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"; -import { Config, Effect, Layer, Option } from "effect"; +import { Config, Effect, Layer, Logger, LogLevel, Option } from "effect"; + +const parseLogLevel = (value: string) => { + switch (value.trim().toLowerCase()) { + case "all": + return LogLevel.All; + case "trace": + return LogLevel.Trace; + case "debug": + return LogLevel.Debug; + case "info": + return LogLevel.Info; + case "warn": + case "warning": + return LogLevel.Warning; + case "error": + return LogLevel.Error; + case "fatal": + return LogLevel.Fatal; + case "none": + return LogLevel.None; + default: + throw new Error( + `Invalid LOG_LEVEL: ${value}. Expected one of All, Trace, Debug, Info, Warning, Error, Fatal, None.`, + ); + } +}; + +const RuntimeLogLevelConfig = Config.string("LOG_LEVEL").pipe( + Config.withDefault("Info"), + Config.mapAttempt(parseLogLevel), +); const TracingConfig = Config.all({ exporterEndpoint: Config.option(Config.string("OTEL_EXPORTER_OTLP_ENDPOINT")), @@ -10,19 +41,28 @@ const TracingConfig = Config.all({ export const Observability = NodeSdk; -export const ObservabilityLive = Effect.gen(function* () { +export const LogLevelLive = Effect.gen(function* () { + const logLevel = yield* RuntimeLogLevelConfig; + return Logger.minimumLogLevel(logLevel); +}).pipe(Layer.unwrapEffect); + +const TracingLive = Effect.gen(function* () { + const logLevel = yield* RuntimeLogLevelConfig; + const logWithConfiguredLevel = Logger.withMinimumLogLevel(logLevel); const tracing = yield* TracingConfig; const endpoint = Option.getOrUndefined(tracing.exporterEndpoint); const serviceName = Option.getOrUndefined(tracing.serviceName); if (!endpoint || !serviceName) { - yield* Effect.log( + yield* Effect.logInfo( "OTEL tracing disabled (set OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_SERVICE_NAME to enable)", - ); + ).pipe(logWithConfiguredLevel); return Layer.empty; } - yield* Effect.log(`OTEL tracing enabled: ${serviceName} -> ${endpoint}`); + yield* Effect.logInfo( + `OTEL tracing enabled: ${serviceName} -> ${endpoint}`, + ).pipe(logWithConfiguredLevel); return NodeSdk.layer(() => ({ resource: { serviceName }, spanProcessor: new BatchSpanProcessor( @@ -30,3 +70,5 @@ export const ObservabilityLive = Effect.gen(function* () { ), })); }).pipe(Layer.unwrapEffect); + +export const ObservabilityLive = Layer.mergeAll(LogLevelLive, TracingLive); diff --git a/packages/presence/src/services/PresenceService.ts b/packages/presence/src/services/PresenceService.ts index 133a7af..91a3173 100644 --- a/packages/presence/src/services/PresenceService.ts +++ b/packages/presence/src/services/PresenceService.ts @@ -12,7 +12,7 @@ export class PresenceService extends Effect.Service()( "PresenceService", { effect: Effect.gen(function* () { - yield* Effect.log("Initializing PresenceService"); + yield* Effect.logInfo("Initializing PresenceService"); const clientsRef = yield* Ref.make( new Map(), @@ -34,7 +34,7 @@ export class PresenceService extends Effect.Service()( client: info, }); - yield* Effect.log(`Client added: ${clientId}`); + yield* Effect.logDebug(`Client added: ${clientId}`); }); const removeClient = (clientId: typeof ClientId.Type) => @@ -57,7 +57,7 @@ export class PresenceService extends Effect.Service()( disconnectedAt, }); - yield* Effect.log(`Client removed: ${clientId}`); + yield* Effect.logDebug(`Client removed: ${clientId}`); } }); @@ -91,7 +91,9 @@ export class PresenceService extends Effect.Service()( changedAt, }); - yield* Effect.log(`Client ${clientId} status changed to ${status}`); + yield* Effect.logDebug( + `Client ${clientId} status changed to ${status}`, + ); } });