agentleFS
lauren-frameworklauren-framework/lauren-ai/llms.txt

lauren-ai

llms.txt0 starsChanged 6 months agoMITView on GitHub

First-party AI/LLM companion for the Lauren web framework. lauren-ai integrates large-language-model completions, agentic loops, tool execution, memory management, and structured workflows into Lauren applications using the same decorator-first, DI-driven, module-scoped paradigm as Lauren itself. Rules: - Must be invoked with parentheses: @tool() not @tool - Schema is generated from type annotations + Google-style Args: docstring section - Any parameter annotated as ToolContext (or ToolContext | None) is injected by the runner and excluded from the JSON schema — the parameter…

  • Reads credentials
  • Installs packages

The file

We split this llms.txt into the rules it gives an agent

3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Must be invoked with parentheses: @tool() not @tool

    line 90
  2. Very specific:

    If an agent omits model=..., AgentModule.for_root() fills in the model from the imported LLMConfig during module wiring.

    line 168
  3. Very specific:

    No named AgentRunnerBase subclass needed.

    line 322

≈ “Common” is an estimate: at least 1 in 5 of the 3,592 llms.txt files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
llms.txt
# lauren-ai

> First-party AI/LLM companion for the Lauren web framework.

`lauren-ai` integrates large-language-model completions, agentic loops, tool
execution, memory management, and structured workflows into Lauren applications
using the same decorator-first, DI-driven, module-scoped paradigm as Lauren itself.

## Installation

```bash
pip install "lauren-ai[anthropic]"   # Anthropic Claude (default)
pip install "lauren-ai[openai]"      # OpenAI / Ollama
pip install "lauren-ai[all]"         # All providers + all extras
```

## Core Concepts

### LLMConfig — provider connection

```python
from lauren_ai import LLMConfig

# Direct construction (provider is required)
config = LLMConfig(
    provider="anthropic",        # "anthropic" | "openai" | "ollama" | "litellm"
    model="claude-opus-4-6",     # Model ID
    api_key="sk-ant-...",        # API key (or reads env var)
    max_tokens=4096,             # Max output tokens (default: 4096)
    temperature=1.0,             # Sampling temperature (default: 1.0)
    timeout=60.0,                # Request timeout in seconds (default: 60.0)
    max_retries=3,               # Retries on transient errors (default: 3)
)

# Convenience factories (recommended)
config = LLMConfig.for_anthropic(model="claude-opus-4-6")   # reads ANTHROPIC_API_KEY
config = LLMConfig.for_openai(model="gpt-4o")               # reads OPENAI_API_KEY
config = LLMConfig.for_ollama(model="llama3.2")             # no key needed

# Test factory (zero network calls)
cfg, mock = LLMConfig.for_testing()                         # returns (LLMConfig, MockTransport)
```

### AgentConfig — runtime behaviour

```python
from lauren_ai import AgentConfig

config = AgentConfig(
    system_prompt="You are a helpful assistant.",  # Default system prompt
    max_turns=10,                                  # Max agentic loop turns (default: 10)
    max_tokens_per_turn=4096,                      # Max output tokens per turn (default: 4096)
    temperature=1.0,                               # Per-agent temperature override (default: 1.0)
    memory_window_tokens=40_000,                   # Sliding memory window (default: 40_000)
    max_cost_usd=None,                             # USD budget cap per run (None = unlimited)
    parallel_tool_calls=False,                     # Execute tools in parallel (default: False)
    tool_error_policy="return_error",              # "return_error" | "raise" | "skip"
    # Extended thinking (Anthropic only)
    thinking=False,
    thinking_budget_tokens=8_000,
    # OpenAI reasoning models
    reasoning_effort=None,                         # "low" | "medium" | "high" | None
)
```

### LLMModule — wiring into Lauren

```python
from lauren_ai import LLMModule, LLMConfig

LLMProvider = LLMModule.for_root(LLMConfig.for_anthropic())
```

### @tool() — define a tool for an agent

```python
from lauren_ai import tool, ToolContext

@tool()
async def get_weather(city: str, ctx: ToolContext | None = None) -> dict:
    """Return current weather for a city.

    Args:
        city: City name.
    """
    return {"city": city, "temperature": 22, "condition": "sunny"}
```

Rules:
- Must be invoked with parentheses: `@tool()` not `@tool`
- Schema is generated from type annotations + Google-style `Args:` docstring section
- Any parameter annotated as `ToolContext` (or `ToolContext | None`) is injected by
  the runner and excluded from the JSON schema — the parameter may have **any name**
- `from __future__ import annotations` is supported, but tool annotations still
  need to resolve when `@tool()` builds the schema. Avoid unresolved forward
  refs or circular imports in function-form tool files.
- Class-form tools get `@injectable(scope=SINGLETON)` applied automatically; use
  `@tool() @injectable(scope=Scope.REQUEST)` to override the scope
- Subclasses of `@tool()`-decorated classes must re-apply `@tool()` themselves;
  the registry raises `MetadataInheritanceError` otherwise

### @agent() + @use_tools() — declare an agent

```python
from lauren_ai import agent, use_tools

@agent(system="You are a helpful assistant.")
@use_tools(get_weather)
class WeatherAgent: ...
```

- `@agent()` is the **outermost** decorator; `@use_tools()` is below it (applied first)
- Mandatory ordering: `@agent` → `@remember` → `@use_guardrails` → `@use_tools`
- Agents are singletons in the DI container by default

### AgentRunner (Protocol) / AgentRunnerBase (concrete)

`AgentRunner` is a `@runtime_checkable Protocol`. The concrete implementation is
`AgentRunnerBase`. In production, runners are created by `AgentModule.for_root()`.
For testing, instantiate `AgentRunnerBase` directly:

```python
from lauren_ai import AgentRunner, AgentRunnerBase

runner = AgentRunnerBase(transport=transport)
isinstance(runner, AgentRunner)  # True

response = await runner.run(
    agent_instance,              # @agent()-decorated class instance
    "What's the weather?",       # User message string
    conversation_id="sess-1",
    metadata={"user_id": "u1"},
    execution_context=exec_ctx,  # lauren ExecutionContext (optional, from route handler)
)

print(response.content)       # str — final text output
print(response.turns)         # int — agentic loop iterations
print(response.stop_reason)   # "end_turn" | "max_turns" | "budget_exceeded" | "error"
print(response.total_usage)   # TokenUsage — cumulative tokens
print(response.tool_calls_made)  # list[ToolCall]
print(response.reasoning_traces) # list[str] — extended thinking traces (Anthropic)
```

**Rule: every `AgentModule` MUST have its own runner.**
Each `AgentModule.for_root()` call auto-generates a unique `AgentRunnerBase`
subclass and registers `AgentRunner[agent_cls]` for every agent in `agents=`.

- **Single module:** inject with `runner: AgentRunner` (structural Protocol scan).
- **Multiple modules:** inject with `runner: AgentRunner[MyAgent]` — a cached real
  subclass used as an unambiguous DI token.  No named `AgentRunnerBase` subclass
  boilerplate needed.

```python
from lauren_ai import AgentRunner

class BankingController:
    def __init__(
        self,
        crm_runner:      AgentRunner[CRMAgent],
        transfer_runner: AgentRunner[TransferAgent],
    ) -> None: ...
```

Both `@agent(memory=ShortTermMemory(...))` and `@agent(conversation_store=InMemoryConversationStore())`
declare per-agent state.  `AgentModule.for_root()` **no longer accepts** `memory=` or
`conversation_store=` at the module level.

If an agent omits `model=...`, `AgentModule.for_root()` fills in the model from the
imported `LLMConfig` during module wiring.

### @use_guardrails() — attach guardrail instances to an agent

```python
from lauren_ai import use_guardrails, PromptInjectionFilter, PIIRedactor, LengthFilter, TopicFilter

@agent(model="claude-opus-4-6")
@use_guardrails(
    input=[PromptInjectionFilter(), PIIRedactor()],
    output=[LengthFilter(max_chars=8000)],
)
@use_tools(my_tool)
class SafeAgent: ...
```

Built-ins: `PromptInjectionFilter`, `PIIRedactor`, `LengthFilter`, `TopicFilter`, `LLMGuardrail`.

### @guardrail() — DI-injectable guardrail class

```python
from lauren_ai import guardrail, GuardrailDecision, GuardrailContext

@guardrail(kind="input")   # also marks as @injectable(scope=Scope.SINGLETON)
class ProfanityFilter:
    async def check(self, message: str, ctx: GuardrailContext) -> GuardrailDecision:
        if "badword" in message.lower():
            return GuardrailDecision(action="block", violation="Profanity",
                                     guardrail_name="ProfanityFilter")
        return GuardrailDecision(action="pass", guardrail_name="ProfanityFilter")
```

`kind` may be `"input"`, `"output"`, or `"any"` (default).

### @remember() — persistent user memory

```python
from lauren_ai import remember, InMemoryUserMemoryStore

_memory = InMemoryUserMemoryStore()

@agent(model="claude-opus-4-6")
@remember(store=_memory, extract=True, inject=True, top_k=5)
@use_guardrails(input=[PromptInjectionFilter()])
@use_tools(my_tool)
class PersonalAgent: ...
```

- Stack between `@agent()` and `@use_guardrails()`
- `extract=True`: facts extracted from each turn and stored
- `inject=True`: top-k relevant facts prepended to system prompt

### LLMService — low-level completions

```python
from lauren_ai import LLMService, Message

class MyController:
    def __init__(self, llm: LLMService) -> None:
        self._llm = llm

    @get("/complete")
    async def complete(self) -> dict:
        result = await self._llm.complete(
            [Message.user("Say hello!")]
        )
        return {"text": result.content}
```

### ToolContext — injected run context

```python
from lauren_ai import ToolContext

# Any parameter name works — annotation determines injection:
@tool()
async def my_tool(query: str, ctx: ToolContext | None = None) -> dict:
    """..."""
    # ctx.agent_context      — AgentContext for the running agent
    # ctx.tool_use_id        — Provider-assigned tool call ID
    # ctx.turn               — Current agentic loop iteration (0-based)
    # ctx.request            — Originating HTTP request (or None)
    # ctx.execution_context  — lauren ExecutionContext (route/handler metadata, or None)
    # ctx.state              — Mutable dict for per-call local storage
    # ctx.get_metadata("key")  — Read from agent_context.metadata
    return {"result": query}
```

### Memory

```python
from lauren_ai import ShortTermMemory, InMemoryVectorStore, InMemoryConversationStore

mem = ShortTermMemory(max_tokens=40_000)
mem.add_user("Hello!")

store = InMemoryVectorStore()
conv = InMemoryConversationStore()
```

`ShortTermMemory` validates provider-neutral tool-call exchanges before every
provider request. Use `begin_tool_exchange()`, `commit_tool_exchange()`, and
`validate_tool_history()` for a batch: every non-empty unique assistant call ID
must have exactly one adjacent result. Cancellations, approval filtering, and
partial execution synthesize bounded error results for unresolved calls;
unknown, duplicate, empty, or non-adjacent IDs raise
`ToolConversationIntegrityError` before network I/O. Snapshots retain active
exchange state, and the lifecycle signals include
`ToolExchangeStarted`, `ToolExchangeResultRecorded`, `ToolExchangeCommitted`,
`ToolExchangeAborted`, and `ToolExchangeRepaired`.

### Signals — observability hooks

```python
from lauren_ai import SignalBus, ModelCallComplete, AgentRunComplete

bus = SignalBus()

@bus.on(ModelCallComplete)
async def on_complete(event: ModelCallComplete) -> None:
    print(f"Cost: ${event.cost_usd:.6f}, tokens: {event.usage.total_tokens}")

@bus.on(AgentRunComplete)
async def on_run_done(event: AgentRunComplete) -> None:
    print(f"Agent {event.agent_class.__name__} — turns={event.turns}, "
          f"total_cost=${event.total_cost_usd:.6f}")
```

### @team() — multi-agent teams

```python
from lauren_ai import team, TeamRunner, TeamResult

@team(name="research", mode="coordinator", max_rounds=4, model="openai/gpt-4o-mini")
class ResearchTeam:
    def __init__(self, researcher: ResearchAgent, writer: WriterAgent) -> None:
        self.researcher = researcher
        self.writer = writer

runner = TeamRunner(team_cls=ResearchTeam, llm=llm_service, agent_runner=agent_runner)
result: TeamResult = await runner.run("Summarise AI safety research.")
print(result.final_answer)
print(result.worker_outputs)  # {"researcher": "...", "writer": "..."}
print(result.rounds)
```

Stream events via `runner.run_stream(...)`:
`TeamWorkerStarted`, `TeamWorkerFinished`, `TeamCoordinatorDecision`, `TeamFinalAnswer`.

### Delegation pattern — cross-module tool routing

The delegation tool lives in the **calling module's `tools=`**. The calling module
imports the target module so `AgentRunner[TargetAgent]` is visible to DI.
No named `AgentRunnerBase` subclass needed.

```python

from lauren_ai import AgentRunner, tool, ToolContext

@tool()
class DelegateToSpecialist:
    """Delegate a task to the SpecialistAgent.

    Args:
        task: Full description of the task.
    """
    def __init__(
        self,
        agent: SpecialistAgent,
        runner: AgentRunner[SpecialistAgent],   # ← parameterized token — no boilerplate
    ) -> None:
        self._agent = agent
        self._runner = runner

    async def run(self, ctx: ToolContext, task: str) -> dict:
        response = await self._runner.run(self._agent, task,
                                          execution_context=ctx.execution_context)
        return {"result": response.content}

# Target module: AgentRunner[SpecialistAgent] is auto-registered
SpecialistMod = AgentModule.for_root(
    agents=[SpecialistAgent],
    tools=[SpecialistTool1],
    imports=[LLMProvider],
)

# Calling module: owns the delegation tool; imports target module so
# AgentRunner[SpecialistAgent] is visible to DI when resolving DelegateToSpecialist
OrchestratorMod = AgentModule.for_root(
    agents=[OrchestratorAgent],
    tools=[DelegateToSpecialist],          # ← delegation tool lives HERE
    imports=[LLMProvider, SpecialistMod],  # ← makes AgentRunner[SpecialistAgent] visible
)
```

**Rule:** The delegation tool belongs in the **calling module's `tools=`**. The
calling module imports the target module so the target runner token is visible.

### @traced() — tracing & observability

```python
from lauren_ai import traced, SpanKind, TraceStore, InMemoryTraceExporter, set_trace_store

exporter = InMemoryTraceExporter()
store = TraceStore()
store._exporters = [exporter]
set_trace_store(store)

@traced(name="my_handler", kind=SpanKind.AGENT)
async def my_handler(request): ...
```

### Cost & Rate Tracking

```python
from lauren_ai import CostTracker, TokenBudget, RateLimiter, default_pricing_table

tracker = CostTracker(pricing=default_pricing_table())

budget = TokenBudget(max_tokens_per_conversation=100_000)
# Raises BudgetExceededError when token limit hit

limiter = RateLimiter(requests_per_minute=60)
await limiter.acquire()
# Raises RateLimitExhaustedError when ceiling breached
```

### StructuredLLM — typed LLM outputs

```python
from lauren_ai import StructuredLLM, LLMService, Message
from pydantic import BaseModel

class Sentiment(BaseModel):
    label: str
    score: float

structured = llm_service.with_structured_output(Sentiment)
result: Sentiment = await structured.complete([Message.user("Great product!")])
```

### Prompt Templates & Chains

```python
from lauren_ai import PromptTemplate, ChatPromptTemplate, Chain, StrOutputParser

tmpl = PromptTemplate("Summarise: {text}")
chain = tmpl | llm_service | StrOutputParser()
result = await chain.invoke({"text": "Some long document..."})
```

### Output Parsers

```python
from lauren_ai import StrOutputParser, JSONOutputParser, PydanticOutputParser

parser = PydanticOutputParser(MyModel)
chain = prompt | llm_service | parser
```

Available: `StrOutputParser`, `JSONOutputParser`, `RegexParser`,
`CommaSeparatedListParser`, `MarkdownCodeBlockParser`, `PydanticOutputParser`,
`RetryOutputParser`.

### SemanticRouter

```python
from lauren_ai import SemanticRouter, Route, RouteMatch

router = SemanticRouter(
    routes=[Route(name="weather", examples=["What's the weather?", "Is it raining?"])],
    embed_fn=embed_fn,
    min_confidence=0.7,
)
await router.compile()
match: RouteMatch = await router.route("Will it snow today?")
```

### Multimodal inputs

```python
from lauren_ai import ImageContent, AudioContent, DocumentContent, Message

msg = Message.from_multimodal("user", [
    "Describe this image:",
    ImageContent(url="https://example.com/img.png"),
])
```

## Decorator quick-reference

| Decorator | Purpose | Parentheses required |
|-----------|---------|---------------------|
| `@tool()` | Expose a function or class as an agent tool | Yes |
| `@agent()` | Declare an agent class | Yes |
| `@use_tools(t1, t2)` | Attach tools to an agent | Yes |
| `@use_knowledge_sources(ks, ...)` | Opt agent into specific KB sources (opt-in — no decorator = no KB tools) | Yes |
| `@use_guardrails(...)` | Attach guardrail instances to an agent | Yes |
| `@guardrail()` | Mark a class as a DI-injectable guardrail | Yes |
| `@remember()` | Enable persistent user memory | Yes |
| `@team()` | Declare a multi-agent team | Yes |
| `@traced()` | Wrap with tracing spans | Yes |

## Decorator ordering (mandatory)

```
@agent()                    ← outermost
@remember()                 ← optional
@use_guardrails()           ← optional
@use_knowledge_sources()    ← optional (KB opt-in; must be below @agent)
@use_tools()                ← innermost
class Agent: ...
```

## Critical rule — tool annotations must resolve

`from __future__ import annotations` is supported in both agent and tool files, but
`@tool()` still resolves parameter types when the module is imported. Keep tool
annotations importable and avoid unresolved forward references or circular imports,
especially in function-form tools.

## Links

- Documentation: https://docs.lauren-framework.dev/lauren-ai/
- GitHub: https://github.com/lauren-framework/lauren-ai
- PyPI: https://pypi.org/project/lauren-ai/
- Changelog: https://github.com/lauren-framework/lauren-ai/blob/main/CHANGELOG.md

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.