EQTY LabDocs

Describe an agent

Describe an agent's tools, skills and instructions, keep credentials out of its definition, and fix a definition that breaks a rule.

View as Markdown

An agent’s definition is what it is: its models, instructions, tools, skills, memory and sub-agents (Definitions). 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 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:

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"])
  • 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 shows, and uses the planner agent:

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()}")

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:

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)

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).
  • 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.