Tools, Skills, and Integrations
Methods are the tools
There is no tool-registration abstraction: any visible method or attribute on self is callable from CodeAct-generated code, with doc(self) telling the LLM what exists. Mix deterministic helpers (SW1) with generation methods (SW3):
class InventoryAgent(Agent, llm=llm):
"""Checks inventory using deterministic helpers."""
def get_stock(self, item: str) -> int: # SW1: a "tool"
return self.inventory.get(item, {}).get("stock", 0)
async def can_fulfill(self, items: list[str], budget: float) -> Result:
"""Check whether the order fits the budget. Use self.get_stock()."""
... # SW3: LLM orchestrates the helpers
Design rubric: exact semantics (matching rules, parsing, arithmetic) → deterministic helper; fuzzy judgment → generation method. Give helpers precise names, type hints, and docstrings — those are their tool schema.
Built-in tools
from nooa.tools import ShellTools, TodoManager
class DevAgent(Agent, llm=llm):
def __init__(self):
super().__init__()
self.shell = ShellTools(cwd=".") # persistent shell + file ops
self.todos = TodoManager() # t = self.todos.add("step", deps=[...]); self.todos.done(t.id)
ShellTools is the one shell/file tool (it replaced the removed BashTool/FileTool). One persistent bash session — cd, export, and cwd survive across calls — and four methods:
r = await shell.run("grep -rn 'def foo' src/") # ShellResult: str subclass; .stdout/.stderr/.returncode/.success
await shell.run("python -", stdin=script) # pass payloads via stdin= instead of quoting heredocs
region = await shell.read("f.py", lines=(10, 25)) # view a file region -> Match
await shell.replace(r.matches[0], new_code) # edit at a Match anchor (pure grep/rg results carry .matches)
await shell.write_file("out.py", content) # create/overwrite
Tools attached as public attributes are automatically visible; the LLM discovers their APIs via doc(self.shell).
For an object the LLM might misuse, prefer wrapping the dangerous surface in a small deterministic method over exposing the raw object.
Agent skills (Skill / TextSkill / SkillRegistry)
Skills inject curated guidance + optional helper APIs into an agent. Three forms:
from nooa import Skill, TextSkill
class MyAgent(Agent, llm=llm):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# 1. Subclass with a docstring-as-usage-guide (methods become the API)
self.git = GitWorkflow() # class GitWorkflow(Skill): """guide..."""
# 2. Wrap a third-party library for discovery
self.pd = Skill(pd) # LLM uses self.pd.read_csv(...)
# 3. A SKILL.md directory on disk
self.frontend = TextSkill(path=Path("skills/frontend-design"))
- Agents see a
# Skillsblock (one-liner per skill) in their execution context and calldoc(self.<skill>)for the full guide. TextSkillexposesread_file(path)andawait run_script(name, *args)for files/scripts bundled with the SKILL.md.- SKILL.md frontmatter: required
name,description; optionalcompatibility,metadata,user-invocable,allowed-tools— Claude-Code-compatible format. SkillRegistry(agent)supports explicitregister(...)/activate(...)and bulkdiscover_skills_dirs(...). Register model-facingTextSkillobjects under a non-cmd.*name so the active Skills block advertises their descriptions. Discovered SKILL.md directories use thecmd.*namespace for host/user commands and are intentionally omitted from that model-facing summary.SkillManagerdoes not exist.@slash_commandon aSkillmethod marks it user-invocable via a host that reads the agent'sslash_commandsqueue (seeInteractiveAgent).- Docstring conventions: see
skills/nooa-agent-authoring/SKILL.mdandAGENTS.md.
MCP servers
Core library keeps MCP optional: uv sync --extra mcp (or uv add 'nooa[mcp]').
from nooa.mcp import MCPManager, MCPTool
class ConfluenceAgent(Agent, llm=llm):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.confluence: MCPTool = MCPManager.create_from_server("my-mcp-server")
# Or inline, no .mcp.json:
# self.confluence: MCPTool = MCPManager.create_from_server(
# "name", url="https://.../mcp",
# transport="streamable-http", headers={...},
# )
async def respond(self, prompt: str) -> str:
"""Answer using the Confluence tool."""
...
MCP tools appear alongside regular methods — the LLM calls them like any other attribute. Servers are configured in .mcp.json (VS Code/Claude Code format). Transports: stdio, SSE, streamable-http; OAuth supported (nooa.mcp.OAuthConfig).
Each returned MCPTool owns a client/connection. Treat it as stateful and keep
it per agent instance, just like ShellTools.
Agent documentation parses __init__ and infers direct constructor assignments
such as self.shell = ShellTools(...) or self.frontend = TextSkill(...).
Annotate the assignment in place when a factory, injected parameter, or other
expression obscures the type, as in the MCP example above. Class-level
annotations are an optional, source-independent alternative. SkillRegistry
activation handles dynamically discovered skills explicitly.
${VAR} placeholders in .mcp.json and inline servers configuration remain
literal. If a server needs a secret, resolve it in trusted caller code and pass
the resulting URL, header, argument, or environment value directly to
MCPManager.create_from_server().
Multimodal media
from nooa import Image, Audio, Video, File
class MediaAgent(Agent, llm=llm):
async def describe(self, image: Image) -> str:
"""Describe what you see."""
...
img = Image.from_file("photo.png") # also .from_bytes(media_type=...), .from_url(...)
await MediaAgent().describe(img)
Only works with multimodal-capable models — text-only models error when handed media. See examples/quickstart/12_multimodal.py.
Pitfalls
- A tool attribute assigned as a bare function/lambda isn't introspectable — use class methods or proper objects.
- Skills/tools attached to
selfare per-instance; create them in__init__(aftersuper().__init__()). An object assigned on the class is shared by every agent instance. - Hide internal-only tools with
Annotated[T, hidden]so they don't pollute every prompt. - External API fragility: wrap flaky calls in a small deterministic method with clear errors, so CodeAct sees a clean failure it can react to.
Related skills
nooa-agent-authoring— visibility rules and method design that make tools discoverable.nooa-capturing-traces— every tool invocation becomes atool_execution.*span you can inspect.