JuliusBrussee/caveman · error · Error

caveman agent: tool timeoutMs must be a positive integer

Error message

caveman agent: tool timeoutMs must be a positive integer

What it means

tool() validates the per-call timeout: timeoutMs must be a safe positive integer (default 30_000 ms). Fractional timeouts, zero, negatives, NaN, or numeric strings are rejected because the runtime schedules calls with this exact value. Throws synchronously at definition time.

Source

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

  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
      : undefined;
    if (converted === undefined && standard.jsonSchema !== undefined) {
      try {
        converted = standard.jsonSchema.input({ target: "draft-07" });
      } catch (error) {
        throw new Error("caveman agent: Standard Schema cannot emit draft-07 input JSON Schema", {
          cause: error,
        });
      }
    }

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Pass a positive integer of milliseconds, e.g. timeoutMs: 60_000
  2. Omit timeoutMs to accept the 30 s default
  3. Convert and round user input: Math.max(1, Math.round(Number(raw)))
  4. There is no 'no timeout' value — pick the largest ceiling you accept

Example fix

// before
tool({ name: "run", effect: "external", timeoutMs: Number(cfg.timeout), execute: ... }); // cfg.timeout = "30s" -> NaN

// after
const secs = parseFloat(cfg.timeout);
tool({
  name: "run",
  effect: "external",
  timeoutMs: Number.isFinite(secs) ? Math.max(1, Math.round(secs * 1000)) : 30_000,
  execute: ...,
});
Defensive patterns

Strategy: validation

Validate before calling

const t = Number(cfg.timeoutMs);
if (!Number.isSafeInteger(t) || t <= 0) {
  throw new Error(`timeoutMs must be positive integer milliseconds, got ${JSON.stringify(cfg.timeoutMs)}`);
}

Type guard

const isPositiveInt = (v: unknown): v is number =>
  Number.isSafeInteger(v) && (v as number) > 0;

Prevention

When it happens

Trigger: Passing timeoutMs: 0 (often meant as 'no timeout'), timeoutMs: 1.5, timeoutMs: "30000", or timeoutMs: -1. Computed values like seconds-to-ms math gone wrong (0.5 * 1000 with truncation elsewhere) also land here.

Common situations: Config-driven timeouts parsed from YAML/CLI as strings, milliseconds/seconds confusion (passing 30 meaning 30 ms when 30 s was intended — valid but a footgun — or 0.03 * 1000 = 30.000000000000004 float), or copying a default of -1 from another library meaning 'infinite'.

Understand the failure class

Related errors


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