// --------------------------------------------------------------------------- // ToolResult — typed value-based discriminated union returned by tool // handlers and `invokeTool`. Domain success and expected failure both // resolve through Effect's success channel; only true infra defects use // the Effect failure channel. // --------------------------------------------------------------------------- import { Effect, Schema } from "effect"; export const ToolErrorSchema = Schema.Struct({ code: Schema.String, message: Schema.String, status: Schema.optional(Schema.Number), details: Schema.optional(Schema.Unknown), retryable: Schema.optional(Schema.Boolean), }); export type ToolError = typeof ToolErrorSchema.Type; export const ToolHttpMetaSchema = Schema.Struct({ status: Schema.Number, headers: Schema.Record(Schema.String, Schema.String), }); /** * Transport metadata for HTTP-backed tools (OpenAPI). Kept beside `data` * rather than wrapped around it: `data` stays the upstream payload, while * cross-cutting transport facts (pagination Link headers, rate-limit * headers) remain reachable for callers that need them. */ export type ToolHttpMeta = typeof ToolHttpMetaSchema.Type; export const ToolFileSchema = Schema.TaggedStruct("ToolFile", { name: Schema.optional(Schema.String), mimeType: Schema.String, encoding: Schema.Literal("base64"), data: Schema.String.annotate({ description: "Base64-encoded file bytes.", contentEncoding: "base64", }), byteLength: Schema.Int.annotate({ description: "Raw file size in bytes before base64 encoding.", }), }); export type ToolFile = typeof ToolFileSchema.Type; export const ToolFileJsonSchema = Schema.toJsonSchemaDocument(ToolFileSchema).schema; const matchesToolFileSchema = Schema.is(ToolFileSchema); export const isToolFile = (value: unknown): value is ToolFile => matchesToolFileSchema(value); export type ToolResult = | { readonly ok: true; readonly data: T; readonly http?: ToolHttpMeta } | { readonly ok: false; readonly error: ToolError }; export const ToolResult = { ok: (data: T, meta?: { readonly http?: ToolHttpMeta }): ToolResult => ({ ok: true, data, ...(meta?.http ? { http: meta.http } : {}), }), fail: (error: ToolError): ToolResult => ({ ok: false, error }), } as const; const ToolResultSchema = Schema.Union([ Schema.Struct({ ok: Schema.Literal(true), data: Schema.Unknown, http: Schema.optional(ToolHttpMetaSchema), }), Schema.Struct({ ok: Schema.Literal(false), error: ToolErrorSchema }), ]); const isUnknownToolResult = Schema.is(ToolResultSchema); export const isToolResult = (value: unknown): value is ToolResult => isUnknownToolResult(value); /** * Annotate the current span with the outcome of a tool invocation. * * `ToolResult.fail` rides the Effect *success* channel by design (expected * failures are values, not defects), which means the tracer records those * spans as healthy. Without this, "user keeps hitting 4xx walls" is invisible * to telemetry — the exact class of signal that lets us catch product issues * before they're reported. Stamped attributes: * * - `executor.tool.outcome` — "ok" | "fail" (always, on ToolResults) * - `executor.tool.error_code` — ToolError.code (fail only) * - `executor.tool.error_status` — upstream HTTP status (fail, when present) * * Codes/statuses are enumerable identifiers, never user content — safe span * attributes. Non-ToolResult values (raw success payloads) annotate "ok". */ export const annotateToolResultOutcome = (value: unknown): Effect.Effect => { if (isToolResult(value) && !value.ok) { return Effect.annotateCurrentSpan({ "executor.tool.outcome": "fail", "executor.tool.error_code": value.error.code, ...(value.error.status != null ? { "executor.tool.error_status": value.error.status } : {}), }); } return Effect.annotateCurrentSpan({ "executor.tool.outcome": "ok" }); };