Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ Use the tool's common name (e.g., GitHub Copilot, Cursor, etc.).
| Telemetry import errors | Run `uv sync` to install OpenTelemetry deps |
| Silent empty strings from async backends | Check for `asyncio.gather(..., return_exceptions=True)` — exceptions become values silently; use `return_exceptions=False` unless callers explicitly handle `BaseException` values |
| GitHub Actions workflow injection warning | Never use `${{ expression }}` directly inside `run:` shell commands — always route through `env:` (`env: MY_VAR: ${{ expr }}` then `"$MY_VAR"` in the script). This rule applies only to `run:` steps; `${{ }}` in `if:` conditions and `with:` action inputs is fine. |
| Docstring quality gate false-flags "missing Raises section" with no actual `raise` in the function | `tooling/docs-autogen/audit_coverage.py`'s check is `if "raise " in source` — a substring match over the whole function source, including comments and docstrings, not an AST check for real `raise` statements. A comment containing the literal text `raise ` (e.g. "can't raise X here") triggers it. Reword the comment to avoid the substring; don't add a fake `Raises:` section. |

## 10. Self-Review (before notifying user)
1. `uv run pytest test/ -m "not qualitative"` passes?
Expand Down
12 changes: 6 additions & 6 deletions docs/docs/how-to/execute-tool-calls.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,10 @@ from mellea.stdlib.context import ChatContext
async def simple_react(goal: str, backend, tools: list, max_steps: int = 5):
"""Simple ReACT: Think → Act → Observe → Repeat"""
ctx = ChatContext().add(Message("user", f"Goal: {goal}"))

for step in range(max_steps):
print(f"\n--- Step {step + 1} ---")

# Think & Act: Generate with tool calls enabled
result, ctx = await aact(
Message("system", "Reason about the goal, then call a tool if needed."),
Expand All @@ -188,22 +188,22 @@ async def simple_react(goal: str, backend, tools: list, max_steps: int = 5):
await_result=True,
)
print(f"Thought: {result.value[:200]}...")

# Check for final answer
if "FINAL ANSWER" in result.value:
return result.value

# Observe: Execute tools
tool_messages = await acall_tools(result, backend)
if not tool_messages:
print("No tools called. Stopping.")
break

# Add observations to context
for msg in tool_messages:
ctx = ctx.add(msg)
print(f"Observation: {msg.name} → {msg.content[:100]}...")

return "Max steps reached"
```

Expand Down
6 changes: 3 additions & 3 deletions docs/docs/how-to/primitives-vs-high-level.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,16 +137,16 @@ if safe_calls:
```python
async def custom_react(goal, backend, tools):
ctx = ChatContext().add(Message("user", goal))

for step in range(max_steps):
# Think
result, ctx = await aact(
system_prompt, ctx, backend, tool_calls=True
)

# Act (manual tool execution gives you control)
tool_messages = await acall_tools(result, backend)

# Observe
for msg in tool_messages:
ctx = ctx.add(msg)
Expand Down
71 changes: 71 additions & 0 deletions docs/docs/observability/tracing.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,77 @@ span event per streamed chunk, carrying its index, added text length, and the ap
time since the previous chunk (omitted on the first chunk). This is
opt-in and off by default, since a long response produces one event per chunk.

#### `adapter_function` span and its phase children

Covers the adapter-function lifecycle (Epic #929) — LoRA/aLoRA adapters used
for RAG, safety, and core capability checks (`answerability`,
`requirement_check`, etc.). On `mellea.backend`: adapter/model lifecycle work
is a backend concern, not a user-facing operation.

**As of #1466, this covers `prepare`/`activate`/`deactivate` only.**
`generate`/`parse` fire no spans yet — that lands with #1465, which wires real
generation through `AdapterMixin.adapter_scope`. `release` fires no span at
all: `WeightsBinding.release()` runs outside any invocation and has no
hook-firing site (see below).

One `adapter_function` parent span per invocation:

| Attribute | Description |
| --------- | ----------- |
| `mellea.adapter_function.name` | Adapter function name (e.g. `answerability`) |
| `mellea.adapter_function.revision` | Catalog revision (Hugging Face SHA); omitted when unpinned |
| `mellea.adapter_function.binding_type` | Weight-binding reality (e.g. `local_file`) |
| `mellea.adapter_function.adapter_type` | Adapter mechanism (`lora` or `alora`) |
| `mellea.adapter_function.outcome` | `success`, `schema_error`, or `error` — set when the span closes |

**Two distinct kinds of invocation exist in this architecture, not one:**
`LocalFileBinding.prepare()` typically runs once at setup and opens its own
single-phase invocation (a parent span with just an `adapter_function.prepare`
child); `AdapterMixin.adapter_scope()` opens a separate invocation per call,
wrapping `adapter_function.activate` and `adapter_function.deactivate` (and,
once #1465 lands, `generate`/`parse`). `prepare()` and a later `adapter_scope()`
call on the same adapter do **not** share a parent span.

One `adapter_function.<phase>` child span per lifecycle phase that ran:

| Attribute | Description |
| --------- | ----------- |
| `mellea.adapter_function.phase` | `prepare`, `activate`, `deactivate` (`generate`/`parse` once #1465 lands) |
| `mellea.adapter_function.revision` | Same revision as the parent, recorded directly on the phase span too |

A phase that raises opens its child span but never fires its own completion
event (matching `mellea.adapter_function.phase_duration`'s metric semantics:
a phase that didn't finish contributes no duration sample) — the enclosing
invocation's own close defensively ends that child span with `ERROR` status
instead, so the in-flight span registry still drains to zero.

**Nesting is unconditional across Python versions, unlike every other span
pair in this document, with one edge exception.**
Every other family nests via ambient OTel context attach, which needs
Python 3.12+ (see the note under "Span hierarchy" below) — `adapter_function`
children instead parent explicitly via `trace.set_span_in_context`, because
`ADAPTER_FUNCTION_*_START`/`_COMPLETE` fire from **synchronous** code
(`adapter_scope`, `prepare()`) through `_run_async_in_thread`, under which
ambient attach can't establish a parent/child edge at all (each dispatched
hook call gets an independent `contextvars` snapshot of the calling thread).
So `adapter_function.<phase>` nests under `adapter_function` the same way on
Python 3.11 and 3.12+. The edge exception is not version-related: every
firing site swallows a failed dispatch (an observability failure must never
block the operation it observes), so if an invocation's *start* dispatch
fails, its parent span never opens and that invocation's phase spans open
unparented, falling back to whatever ambient context exists.

**Known gap: no exemplar linkage to `mellea.adapter_function.phase_duration`.**
Because no span in this family is attached as ambient context (see above), the
`AdapterFunctionMetricsPlugin` histogram sample (a separate plugin subscribed
to the same hooks) never has the `adapter_function`/`adapter_function.<phase>`
span ambiently current when it records — at best it would sample whatever
*enclosing application span* happens to be ambient (e.g. `action`), not the
adapter-function span the metric is actually about — regardless of the two
plugins' firing order. Fixing this would mean recording the metric from inside
`AdapterFunctionTracingPlugin` itself, so it can pass the span's context
explicitly — a larger change left for a follow-up.

### Span hierarchy

Backend spans nest inside application spans:
Expand Down
4 changes: 2 additions & 2 deletions docs/examples/m_serve/pii/pii_serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def pii_remove_validate(
text: str,
requirements: list[str] | None = None,
loop_budget: int = 3,
model_options: None | dict = None,
model_options: dict | None = None,
) -> ModelOutputThunk | SamplingResult | str:
"""PII scrubbing in mellea with validation."""
# Extra requirements if any.
Expand Down Expand Up @@ -74,7 +74,7 @@ def pii_remove_validate(
def serve(
input: list[ChatMessage],
requirements: list[str] | None = None,
model_options: None | dict = None,
model_options: dict | None = None,
) -> ModelOutputThunk | SamplingResult | str:
"""Simple serve example to do PII stuff."""
message = input[-1].get_text_content()
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/m_serve/simple/m_serve_example_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def validate_email_len(email: str) -> bool:
def serve(
input: list[ChatMessage],
requirements: list[str] | None = None,
model_options: None | dict = None,
model_options: dict | None = None,
) -> ModelOutputThunk | SamplingResult:
"""Takes a prompt as input and runs it through an M program."""
requirements = requirements if requirements else []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def _extract_mellea_tools_from_model_options(
def serve(
input: list[ChatMessage],
requirements: list[str] | None = None,
model_options: None | dict = None,
model_options: dict | None = None,
) -> ModelOutputThunk:
"""Serve function that handles tool calling.

Expand Down
2 changes: 1 addition & 1 deletion docs/examples/notebooks/m_serve_example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
"def serve(\n",
" input: list[ChatMessage],\n",
" requirements: list[str] | None = None,\n",
" model_options: None | dict = None,\n",
" model_options: dict | None = None,\n",
") -> ModelOutputThunk | SamplingResult:\n",
" \"\"\"Takes a prompt as input and runs it through an M program.\"\"\"\n",
" requirements = requirements if requirements else []\n",
Expand Down
Loading
Loading