Skip to content

DO NOT MERGE: thread per-agent llm_metadata to AgentConfig::with_metadata#328

Draft
brentrager wants to merge 2 commits into
mainfrom
llm-request-metadata
Draft

DO NOT MERGE: thread per-agent llm_metadata to AgentConfig::with_metadata#328
brentrager wants to merge 2 commits into
mainfrom
llm-request-metadata

Conversation

@brentrager

@brentrager brentrager commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

DRAFT / DO NOT MERGE — stacked on the core PR SmooAI/smooth-operator-core#100. Cannot merge until that publishes (see the [patch.crates-io] note below).

What

Wires this operator to the new engine metadata seam so a host resolver can tag each agent's LLM spend:

  • AgentBehaviorConfig gains llm_metadata: Option<serde_json::Map<String, Value>> (#[serde(default)] — absent ⇒ None, byte-identical to before; included in is_empty()).
  • The reference handler digests it into a new TurnRequest.request_metadata field.
  • The runner forwards it to the engine via AgentConfig::with_metadata(...), right beside the existing with_model_ceiling — so it rides both the streaming and non-streaming request bodies core builds.
  • None/empty ⇒ no metadata on the wire.

Why

Per-agent LLM spend attribution. The LiteLLM gateway records a top-level metadata object on its spend logs; a monorepo billing cron partitions by metadata.smooai_agent_slug. The core PR added the passthrough; this PR lets a resolved per-agent config carry the tag through to it. Tracking: pearl th-b8ea4c (epic SMOODEV-2708).

Dormant in this repo: nothing here populates llm_metadata — the analyst-agent resolver that sets { smooai_agent_slug: "observability-analyst" } lives in the monorepo (copilot-ws, not touched here). DB-row-resolved agents (from_row_values) carry no metadata; it is set programmatically only. This is the seam that resolver plugs into.

Mirrors the with_model_ceiling pattern (no trait widening)

The Arc<dyn LlmProvider> the runner holds is only used for the injected/mock path. For a live turn the engine builds the concrete LlmClient itself, from AgentConfig — that's how with_model_ceiling already reaches the wire (AgentConfig::with_model_ceiling → engine builds LlmClient::new(...).with_model_ceiling(...) in both run loops). Metadata mirrors it exactly: AgentConfig::with_metadata (in the core PR) + a runner call here. No new trait method needed.

[patch.crates-io] — the DO NOT MERGE bit

The core methods (AgentConfig::with_metadata / LlmClient::with_metadata) are unpublished. rust/Cargo.toml has a temporary [patch.crates-io] pointing smooai-smooth-operator-core at the core PR's branch so the workspace + kind smoke build compile against the real new API. It cannot be removed without breaking the compile, so it stays and is marked loudly inline. Before merge: bump the smooai-smooth-operator-core pin to the version that ships those methods, then delete the patch section.

Verification

cargo test -p smooai-smooth-operator -p smooai-smooth-operator-server -p smooai-smooth-operator-lambda470 passed, 1 ignored (against the core branch via the patch). cargo fmt --all --check clean; cargo clippy clean on touched files. Two test altitudes:

  • ParseAgentBehaviorConfig deserializes llm_metadata, defaults to None, reflects it in is_empty().
  • Wire round-trip (tests/llm_metadata_wire.rs) — drives the REAL LlmClient (built by the engine from AgentConfig) against a capturing mock: analyst-shaped request_metadata arrives as top-level metadata.smooai_agent_slug; None sends no metadata key. Covers the one link the unit tests bypass — the run loop's AgentConfig::with_metadata → built-client path.

Release / changeset

No changeset included — the release is held for the release owner (same as the core PR). Whoever finalizes adds the @smooai/smooth-operator changeset when flipping the pin. Note the repo's sync-versions.mjs step-3 skip convention for operator-core releases applies at that time.

🤖 Generated with Claude Code

https://claude.ai/code/session_018tRq2Rge3gGHCjJdNx6xDD

Wire the operator to the new engine metadata seam so a host resolver can tag
each agent's LLM spend. A per-agent `AgentBehaviorConfig` gains
`llm_metadata: Option<serde_json::Map<String, Value>>` (serde default, so
absent ⇒ None, byte-identical to before); the reference handler digests it
into `TurnRequest.request_metadata`, and the runner forwards it to the
engine via `AgentConfig::with_metadata(...)` alongside the existing
`with_model_ceiling`. `None`/empty ⇒ no `metadata` on the wire.

Nothing in this repo populates `llm_metadata` yet — the analyst-agent
resolver that sets `{ smooai_agent_slug: ... }` lives in the monorepo
(copilot-ws). This is the dormant seam it plugs into. DB-row-resolved agents
(`from_row_values`) carry no metadata; it is set programmatically only.

DO NOT MERGE until smooth-operator-core with `AgentConfig::with_metadata`
publishes. A temporary `[patch.crates-io]` points the core dep at that PR's
branch so the workspace + smoke build compile against the real new API;
before merge, bump the core pin and delete the patch. No changeset included —
the release is held.

Tests: `AgentBehaviorConfig` deserializes `llm_metadata`, defaults to None,
and reflects it in `is_empty()`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tRq2Rge3gGHCjJdNx6xDD
@changeset-bot

changeset-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 44f7adb

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

…completions body

Drives the REAL LlmClient (built by the engine from AgentConfig) against a
local mock that captures the outbound body: analyst-shaped request_metadata
arrives as top-level `metadata.smooai_agent_slug`; None sends no `metadata`
key. Covers the one link the unit tests bypass — the run loop's
`AgentConfig::with_metadata` → built-client path (mock-provider tests skip it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tRq2Rge3gGHCjJdNx6xDD
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant