JuliusBrussee/caveman · error · Error

caveman agent: unknown tool effect ${JSON.stringify(options.

Error message

caveman agent: unknown tool effect ${JSON.stringify(options.effect)}

What it means

tool() requires an effect of exactly one of "read", "write", "idempotent", or "external" — the effect taxonomy the runtime uses for planning, replay, and permission decisions. Any other string (including casing variants like "Read", typos like "readonly", or "side-effect") throws immediately with the invalid value embedded.

Source

Thrown at packages/agent/src/primitives.ts:132

export function tool<TInput extends TSchema, TResult>(
  options: ToolOptions<TInput, TResult>,
): ToolDefinition<Static<TInput>, TResult>;
export function tool<Input, Output, TResult>(
  options: StandardToolOptions<Input, Output, TResult>,
): ToolDefinition<Output, TResult>;
export function tool<Input, Output, TResult>(
  options: StandardJSONToolOptions<Input, Output, TResult>,
): ToolDefinition<Output, TResult>;
export function tool(
  options: ToolOptions<TSchema, unknown> |
    StandardToolOptions<unknown, unknown, unknown> |
    StandardJSONToolOptions<unknown, unknown, unknown>,
): ToolDefinition<unknown, unknown> {
  if (!/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(options.name)) {
    throw new Error(`caveman agent: invalid tool name ${JSON.stringify(options.name)}`);
  }
  if (!["read", "write", "idempotent", "external"].includes(options.effect)) {
    throw new Error(`caveman agent: unknown tool effect ${JSON.stringify(options.effect)}`);
  }
  const result = typeof options.result === "object"
    ? artifactResultPolicy(options.result)
    : options.result ?? "auto";
  if (!["auto", "inline", "page", "compress", "exact_ccr"].includes(result)) {
    throw new Error(`caveman agent: unknown tool result policy ${JSON.stringify(result)}`);
  }
  const timeoutMs = options.timeoutMs ?? 30_000;
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
    throw new Error("caveman agent: tool timeoutMs must be a positive integer");
  }
  const standard = standardToolSchema(options.input);
  let input: TSchema;
  if (standard === undefined) {
    input = options.input as TSchema;
  } else {
    let converted = "inputJSONSchema" in options
      ? options.inputJSONSchema

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Use one of the four exact literals: "read", "write", "idempotent", "external"
  2. Pick by side-effect profile: no state change -> "read"; repeated same-call is safe -> "idempotent"; state changes and is not safely repeatable -> "write"; external system interaction -> "external"
  3. If migrating, map old vocabularies explicitly through a lookup table with a fallback that throws your own descriptive error

Example fix

// before
const t = tool({ name: "save", effect: "readonly", execute: ... });

// after
const t = tool({ name: "save", effect: "write", execute: ... });
Defensive patterns

Strategy: type-guard

Validate before calling

const EFFECTS = ["read", "write", "idempotent", "external"] as const;
if (!EFFECTS.includes(cfg.effect)) {
  throw new Error(`effect must be one of ${EFFECTS.join(", ")}, got ${JSON.stringify(cfg.effect)}`);
}

Type guard

type ToolEffect = "read" | "write" | "idempotent" | "external";
const isToolEffect = (v: unknown): v is ToolEffect =>
  typeof v === "string" && ["read", "write", "idempotent", "external"].includes(v);

Prevention

When it happens

Trigger: Passing effect: "readonly", "Read", "pure", "mutation", "io", or undefined. The field is mandatory with no default; omitting it entirely surfaces as an invalid undefined value in this same error.

Common situations: Migrating from another agent framework whose effect vocabulary differs (e.g. "destructive"/"safe"), assuming case-insensitivity, or optional-spread code paths that accidentally drop the field.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/7f673f636c29961e. Report an issue: GitHub.