---
title: "Describe an agent"
description: "Describe an agent's tools, skills and instructions, keep credentials out of its definition, and fix a definition that breaks a rule."
source: "Guardian Agent SDK"
---
An agent's definition is what it is: its models, instructions, tools, skills, memory and sub-agents ([Definitions](../concepts/definitions.mdx)). The framework integrations build it from the agent they make. For an agent you call a model client for yourself, you describe it, with the same keyword arguments either way:

```python
import guardian_agent as ga

guardian = ga.Guardian()

researcher = guardian.agent("researcher", model=..., instructions=..., tools=[...])

definition = ga.Definition(model=..., instructions=..., tools=[...])
researcher = guardian.agent("researcher", definition=definition)
```

A definition is checked when it's made, so a mistake fails at the line that made it, and nothing is sent until the agent registers it. This page covers tools, skills and instructions, credentials, and what a definition that breaks a rule tells you. [Definition fields](./definition-fields.mdx) covers every other field.

## Tools

`@ga.tool` records a function as a tool and returns it unchanged, so it still works with anything else that calls it:

{/* generated file sdks/python/snippets/describe_tools.py python */}

```python
import guardian_agent as ga


@ga.tool
def search_docs(query: str, limit: int = 10) -> list[str]:
    """Search the internal documentation.

    Args:
        query: Words to look for.
        limit: The most results to return.
    """
    return []


@ga.tool(name="search_wiki", description="Full-text search over the wiki.")
async def wiki_search(query: str) -> list[str]:
    return []


# A tool that isn't a Python function, such as one served elsewhere
lookup_ticket = ga.Tool(
    "lookup_ticket",
    "Fetch a support ticket by ID.",
    {"type": "object", "properties": {"id": {"type": "string"}}, "required": ["id"]},
)

definition = ga.Definition(
    description="Answers questions from the internal docs.",
    model=ga.Model("openai", "gpt-4.1", temperature=0),
    instructions="You research questions using the internal docs.",
    tools=[search_docs, wiki_search, lookup_ticket],
)

for described in definition.document("researcher")["tools"]:
    print(described["name"], "-", described["description"])
```

{/* end generated */}

- **Name and description:** the tool's name is the function's and its description is the docstring's summary, unless you pass `name=` or `description=`. A function with no description (no docstring summary and no `description=`), or a blank one, raises `ValueError`. A `ga.Tool` you build yourself isn't checked.
- **Input schema:** it comes from the type hints, through Pydantic, with each Google-style `Args:` entry as that parameter's description. A `Literal`, a Pydantic model and `Annotated[..., Field(...)]` describe themselves, and a `Field` description wins over the docstring's. A function that takes `*args` or `**kwargs`, or a parameter whose type has no JSON Schema, raises `TypeError`.
- **Source hash:** the function's source is hashed into the tool (`sourceHash`), so editing a tool gives the agent a new definition. Pass `hash_source=False` to leave it out.
- **Other tools:** for a tool that isn't a Python function, such as one served elsewhere, build a `ga.Tool(name, description, input_schema)` yourself. LangChain tools, with the `langchain` extra, are taken as they are.

The definition records the tool as the model sees it. Giving the model the same tools is your code's job when you call a model client yourself: pass them in your requests, as the framework integrations do for you.

## Skills and instructions

A skill is a directory of files, with a `SKILL.md` describing it. Instructions are memory the agent reads but can't change, such as an `AGENTS.md`. Both are part of what the agent is, so the proxy stores their files and the definition records them by their content ID. This example reads the skill and instructions files [Before you start](./index.mdx#the-files-they-read) shows, and uses the `planner` agent:

{/* generated file sdks/python/snippets/skills_and_instructions.py python */}

```python
import guardian_agent as ga

guardian = ga.Guardian()

# Each call uploads the files to the proxy, which returns their content ID
triage = guardian.skill("triage", "skills/triage")
agents_md = guardian.instructions(
    "/AGENTS.md", "docs/agent-notes.md", backend="filesystem"
)

planner = guardian.agent(
    "planner",
    model=ga.Model("openai", "gpt-4.1"),
    instructions="You plan the work and hand each step to a tool.",
    skills=[triage],
    memory=[agents_md],
)
print(f"triage is stored as {triage.cid}")
print(f"planner is registered as {planner.register()}")
```

{/* end generated */}

Each call uploads the files to the proxy, which returns a content ID for them, so editing any file gives the agent a new definition. The proxy signs it with the agent's requests, and the Agent Gateway checks a custom agent's skills and instructions as it checks a coding agent's skills and `CLAUDE.md`: it's the same content ID Viper gives a coding agent's skill of the same files.

- **What's read:** every regular file in the directory, hidden ones included, at most 16 levels deep. Symlinks inside it are skipped, as Viper skips them in a coding agent's skill; the directory itself may be a link.
- **`guardian.instructions(path, local_path, backend=...)`** takes where the agent reads the instructions first (`"/AGENTS.md"`), then the local file or directory to read them from (`"docs/agent-notes.md"`). A file is stored under the last part of the first path (`AGENTS.md` above), whatever it's called locally. For a directory, give the first path a trailing `/` (`"/memories/"`).
- **`backend`** names the kind of storage the agent reads it from, as your framework names it, such as `"filesystem"`.
- **Checked before anything is sent:** the proxy's limits (`capabilities().limits`: 1,024 files, 1 MiB each, 8 MiB together) and credentials, in each file and each file's path. A file that breaks a rule raises `ga.CollectionInvalid`, `ga.CollectionTooLarge` or `ga.DefinitionContainsSecret`, naming the file where it can.
- **Async:** `await guardian.askill(...)` and `await guardian.ainstructions(...)` read the files on a worker thread.
- **Already stored:** `ga.Skill(name, cid=...)` and `ga.Memory.instructions(path, backend=..., cid=...)` take a CID directly, for files already on the proxy. Registering a definition that names files the proxy doesn't hold is refused.

Like the tools, the definition records what the agent has. Loading a skill or instructions into the agent's prompt is your code's job, or your framework's.

## Credentials and `exclude`

A definition is stored, signed and shown to reviewers, so it must never hold a credential. Before anything is sent, the SDK scans every string and map key the way the proxy does: API keys, cloud and GitHub tokens, bearer tokens, JWTs, private keys, credential-named fields in open maps, and credentials in MCP server URLs and arguments. A match raises `ga.DefinitionContainsSecret`, naming each path, never the value.

To leave a field out on purpose, name it in `exclude` as a JSON Pointer into the definition as sent:

{/* generated file sdks/python/snippets/exclude_and_problems.py python */}

```python
import guardian_agent as ga

# Leave a field out on purpose: its path is recorded, never its value
definition = ga.Definition(
    model=ga.Model("openai", "gpt-4.1"),
    middleware=[ga.Middleware("Router", config={"client": "the-router-client"})],
    exclude=["/middleware/0/config/client"],
)
print(definition.document("planner")["extensions"])

# A definition that breaks the rules fails where it's made, naming every problem
try:
    ga.Definition(
        model=ga.Model("openai", "gpt-4.1"),
        permissions=["read:docs", ""],
    )
except ga.DefinitionInvalid as error:
    for problem in error.problems:
        print(problem.path, "-", problem.rule)
```

{/* end generated */}

The value is left out, and its path is recorded under `extensions.guardian.excluded`, so the Guardian Platform reads "deliberately not recorded" rather than "absent". The first `print` above shows `{'guardian': {'excluded': ['/middleware/0/config/client']}}`.

`exclude` leaves out fields, not list items, and a pointer that names nothing raises `ValueError`, so a typo can't quietly record a secret. A `ga.SubAgent` takes its own `exclude`, with pointers into the sub-agent, recorded in its own extensions.

## When a definition breaks a rule

`ga.DefinitionInvalid` lists every problem in `problems`, each with the `path` to the value and the `rule` it breaks, never the value. The second half of the snippet above prints `/permissions/0 - pattern`: an empty permission breaks the schema's pattern. As a message:

```text
guardian_agent.errors.DefinitionInvalid: the definition breaks 1 rule
  /permissions/0: breaks the 'pattern' rule
hint: fix these in the definition; nothing was sent
```

- **Paths point into the definition as sent.** Lists whose order means nothing (tools, sub-agents, skills, memory, MCP servers, sources, permissions, guardrails, the graph) are sorted first, so the empty permission, second as written, is `/permissions/0`. `definition.document(name)` shows the definition as sent. A missing field is named by its own path, and a map key that isn't one of the schema's field names reads `<key>`.
- **Rules:** a rule is the JSON Schema keyword that states it (`type`, `required`, `pattern` and the like), or one the proxy applies on top of the schema, such as byte lengths, sub-agent depth and MCP transports ([Problem](../api/definition.mdx#problem)).
- **The proxy has the last word.** It checks sizes (the whole definition's, and its prompts') and parses URLs in full when the definition is registered, and refuses with `ga.DefinitionTooLarge` or `ga.DefinitionInvalid`.