106 lines · 3.9 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// 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<T> =  | { readonly ok: true; readonly data: T; readonly http?: ToolHttpMeta }  | { readonly ok: false; readonly error: ToolError }; export const ToolResult = {  ok: <T>(data: T, meta?: { readonly http?: ToolHttpMeta }): ToolResult<T> => ({    ok: true,    data,    ...(meta?.http ? { http: meta.http } : {}),  }),  fail: <T = never>(error: ToolError): ToolResult<T> => ({ 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<unknown> =>  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<void> => {  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" });};