JuliusBrussee/caveman · error · ValueError

AutoGen tools and workbench are mutually exclusive

Error message

AutoGen tools and workbench are mutually exclusive

What it means

with_caveman_agent wraps an AutoGen agent's options with the Caveman middleware. AutoGen supports passing tools either as a `tools` list or as a `workbench`, but not both simultaneously. The library throws this ValueError to prevent an ambiguous configuration where the executor could not decide which tool surface to instrument while retaining tool order and the original native executor.

Solutions

  1. Remove the `tools` key from options and keep the workbench (recommended, workbench is the newer surface).
  2. Remove or set workbench=None and keep the plain tools list.
  3. If you only want tools without instrumentation, check runtime.mode or use the raw model client instead of with_caveman_agent.

Example fix

// before
options = {"model_client": client, "tools": [my_tool], "workbench": wb}
agent = with_caveman_agent(options)
// after
options = {"model_client": client, "workbench": wb}
agent = with_caveman_agent(options)
Defensive patterns

Strategy: validation

Validate before calling

if options.get("tools") and options.get("workbench") is not None:
    raise ValueError("Pass either tools or workbench to with_caveman_agent, not both")

Type guard

def has_conflicting_tool_surface(options: dict) -> bool:
    return bool(options.get("tools")) and options.get("workbench") is not None

Prevention

When it happens

Trigger: Calling with_caveman_agent(options) where options contains a non-empty `tools` list AND options['workbench'] is not None. Only raised when runtime.mode is not 'off' and the model client version is supported; in off mode the check is skipped.

Common situations: Migrating from tools=[...] to a workbench-based agent but leaving the old tools list in place; merging a shared base options dict that sets tools with per-agent overrides that set workbench; passing both because an example combined them.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/3004bc1d063eb92a. Report an issue: GitHub.

Appendix: source

Thrown at packages/middleware/python/caveman_middleware/autogen.py:499

        return _loaded(cls(workbench, runtime=runtime, scope=config.scope, runtime_key=config.runtime_key))


def with_caveman_model(model_client, *, runtime, scope, runtime_key="default"):
    """Wrap an existing client; recovery-free unless paired with the workbench."""
    return CavemanChatCompletionClient(model_client, runtime=runtime, scope=scope, runtime_key=runtime_key)


def with_caveman_agent(options: dict, *, runtime, scope, runtime_key="default") -> dict:
    """Return native AssistantAgent constructor options, leaving its loop intact.

    Accepts the native ``tools`` list or ``workbench`` (including a workbench
    list), retaining tool order and the original native executor for every call.
    """
    model = CavemanChatCompletionClient(options["model_client"], runtime=runtime, scope=scope, runtime_key=runtime_key)
    if runtime.mode == "off" or not model.version_supported:
        return {**options, "model_client": model}
    if options.get("tools") and options.get("workbench") is not None:
        raise ValueError("AutoGen tools and workbench are mutually exclusive")
    workbench = options.get("workbench")
    if workbench is None:
        tools = [t if isinstance(t, BaseTool) else FunctionTool(t, description=t.__doc__ or "") for t in options.get("tools", [])]
        workbench = StaticStreamWorkbench(tools)
    result = {**options, "model_client": model,
              "workbench": CavemanWorkbench(workbench, runtime=runtime, scope=scope, runtime_key=runtime_key)}
    result.pop("tools", None)
    return result

View on GitHub (pinned to 3ee70a1026)