different-ai/openwork · error · ToolRuntimeError

InvalidToolInput

InvalidToolInput

Error message

Tool '${name}' expects exactly one input object.

What it means

Callable tools in codemode accept exactly one argument: a single input object matching the tool's schema. When the script calls a tool with zero arguments or more than one positional argument, the runtime throws InvalidToolInput before the tool runs. Spread the arguments into one object instead.

Solutions

  1. Wrap all arguments into one object: call the tool as tool({ param1: a, param2: b })
  2. Check the tool's declared input schema and supply exactly one object whose fields match it
  3. If arguments are already in an array, pass the object itself, not the spread: tool(argsObj) instead of tool(...args)
  4. Log externalArgs before the call to confirm only one value is being passed

Example fix

// before
const result = yield* callTool('search', query, limit)
// after
const result = yield* callTool('search', { query, limit })
Defensive patterns

Strategy: validation

Validate before calling

function validateToolCall(args) {
  if (!Array.isArray(args) || args.length !== 1 || typeof args[0] !== 'object' || args[0] === null) {
    throw new Error('Tool calls must pass exactly one input object: tool({ ... })');
  }
}

Type guard

function isSingleInputObject(args: unknown[]): args is [Record<string, unknown>] {
  return args.length === 1 && typeof args[0] === 'object' && args[0] !== null;
}

Try / catch

try {
  yield* callTool(name, input)
} catch (e) {
  if (e instanceof ToolRuntimeError && e.code === 'InvalidToolInput') {
    console.error(`Bad input for ${name}: wrap args in one object`, e.message)
  } else throw e
}

Prevention

When it happens

Trigger: Calling a tool from a codemode script as tool() or tool(a, b, c) — i.e. externalArgs.length !== 1 — instead of tool({...}) with one object argument.

Common situations: Porting scripts written for APIs where tools take individual positional parameters; forgetting the single input object entirely; dynamically forwarding multiple script variables to a tool via apply/spread without wrapping them.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/d52af0b3c5b9cb41. Report an issue: GitHub.

Appendix: source

Thrown at packages/codemode/src/tool-runtime.ts:773

  return {
    root: new ToolReference([]),
    calls,
    keys: (path) => namespaceKeys(callableTools, path),
    invoke: (path, args) =>
      Effect.gen(function* () {
        const name = path.join(".")
        const externalArgs = args.map((arg) => copyOut(copyIn(arg, `Arguments for tool '${name}'`)))
        const call = { name }
        const recordAndObserve = (input: unknown) =>
          Effect.sync(() => {
            recordCall(call)
            return calls.length - 1
          }).pipe(Effect.tap((index) => hooks?.onToolCallStart?.({ index, name, input }) ?? Effect.void))
        const tool = resolve(callableTools, path)
        let describedInput: unknown
        if (isDefinition(tool)) {
          if (externalArgs.length !== 1)
            throw new ToolRuntimeError("InvalidToolInput", `Tool '${name}' expects exactly one input object.`)
          describedInput = yield* Effect.try({
            try: () => decodeToolInput(tool, externalArgs[0]),
            catch: (cause) =>
              new ToolRuntimeError("InvalidToolInput", `Invalid input for tool '${name}': ${String(cause)}`),
          })
        }
        const input = isDefinition(tool) ? describedInput : externalArgs
        const index = yield* recordAndObserve(input)
        const currentCall = { index, name, input }
        if (isDefinition(tool)) {
          return yield* observeEnd(
            Effect.gen(function* () {
              const raw = yield* runHost(Effect.suspend(() => tool.run(describedInput)))
              const result = yield* Effect.try({
                try: () => decodeToolOutput(tool, raw),
                catch: () => new ToolRuntimeError("InvalidToolOutput", `Invalid output from tool '${name}'.`),
              })
              return yield* decodeOutput(result, name)

View on GitHub (pinned to 2b7df46e8a)