avp-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- avp/__init__.py +31 -0
- avp/commission.py +236 -0
- avp/content.py +273 -0
- avp/data/__init__.py +0 -0
- avp/data/prices.json +21945 -0
- avp/descriptor.py +204 -0
- avp/envelope.py +108 -0
- avp/gen_ai.py +160 -0
- avp/history.py +86 -0
- avp/pricing.py +138 -0
- avp/sink.py +62 -0
- avp/trajectory.py +530 -0
- avp_cli/__init__.py +82 -0
- avp_cli/agent.py +566 -0
- avp_cli/agent_install.py +331 -0
- avp_cli/agent_manifest.py +73 -0
- avp_cli/agents.py +258 -0
- avp_cli/brand.py +46 -0
- avp_cli/broker.py +227 -0
- avp_cli/catalog/__init__.py +128 -0
- avp_cli/catalog/capitals.json +67 -0
- avp_cli/catalog/custom.json +35 -0
- avp_cli/catalog/parsebench.json +44 -0
- avp_cli/cli.py +1858 -0
- avp_cli/commission.py +144 -0
- avp_cli/config.py +250 -0
- avp_cli/console.py +51 -0
- avp_cli/environment.py +218 -0
- avp_cli/eval/__init__.py +0 -0
- avp_cli/eval/dataset.py +37 -0
- avp_cli/eval/engine.py +426 -0
- avp_cli/eval/report.py +178 -0
- avp_cli/eval/scoring.py +260 -0
- avp_cli/eval/setup.py +69 -0
- avp_cli/images.py +119 -0
- avp_cli/library.py +95 -0
- avp_cli/live.py +185 -0
- avp_cli/observability.py +128 -0
- avp_cli/onboarding.py +80 -0
- avp_cli/osb.py +347 -0
- avp_cli/paths.py +47 -0
- avp_cli/run_manifest.py +113 -0
- avp_cli/state.py +195 -0
- avp_cli/vault.py +116 -0
- avp_cli/viz.py +303 -0
- avp_cli-0.1.0.dist-info/METADATA +359 -0
- avp_cli-0.1.0.dist-info/RECORD +49 -0
- avp_cli-0.1.0.dist-info/WHEEL +4 -0
- avp_cli-0.1.0.dist-info/entry_points.txt +2 -0
avp/descriptor.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""avp.descriptor — Pydantic types for the AVP Agent Descriptor Spec.
|
|
2
|
+
|
|
3
|
+
Defines `AgentDescriptor` (the agent's self-description shape) and the
|
|
4
|
+
declaration types it carries: `ToolDecl`, `SubagentDecl`, `SkillDecl`,
|
|
5
|
+
`McpServerDecl`. This module mirrors the
|
|
6
|
+
[Agent Descriptor spec](../../../../spec/v0.1/agent-descriptor.md).
|
|
7
|
+
|
|
8
|
+
Implementors building an `agent_described` event construct
|
|
9
|
+
`AgentDescriptor` with typed decl lists:
|
|
10
|
+
|
|
11
|
+
from avp.descriptor import AgentDescriptor, ToolDecl
|
|
12
|
+
|
|
13
|
+
AgentDescriptor(
|
|
14
|
+
agent_name="my-agent",
|
|
15
|
+
agent_version="1.0.0",
|
|
16
|
+
spec_version="0.1",
|
|
17
|
+
tools=[ToolDecl(name="Read")],
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
The decl types are also reused by `avp.trajectory` for events that share
|
|
21
|
+
the same shape (`agent_started.data["avp.tools"]`,
|
|
22
|
+
`agent_started.data["avp.mcp_servers"]`).
|
|
23
|
+
|
|
24
|
+
This module is self-contained: importing from it does not drag in
|
|
25
|
+
Trajectory / Commission / Resolver API types.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from typing import Any, Literal
|
|
31
|
+
|
|
32
|
+
from pydantic import BaseModel, Field
|
|
33
|
+
|
|
34
|
+
from avp.envelope import _OPEN, _STRICT
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ToolDecl(BaseModel):
|
|
38
|
+
"""Tool descriptor used by `AgentDescriptor.tools` and
|
|
39
|
+
`agent_started.data["avp.tools"]`.
|
|
40
|
+
|
|
41
|
+
MCP-shaped: `name` plus optional `description` and `inputSchema`. The
|
|
42
|
+
decl describes a single tool's model-facing identity. Dispatch is
|
|
43
|
+
discriminated by `avp.mcp_server_id`: when set, the tool is sourced
|
|
44
|
+
from the MCP server with that `id` in `mcp_servers[]`; when absent,
|
|
45
|
+
the tool runs locally in the agent's process. The per-invocation
|
|
46
|
+
discriminator `avp.tool.dispatch_target` on `tool_invoked` mirrors
|
|
47
|
+
presence of this field."""
|
|
48
|
+
|
|
49
|
+
model_config = _OPEN
|
|
50
|
+
name: str
|
|
51
|
+
description: str | None = None
|
|
52
|
+
inputSchema: dict[str, Any] | None = Field(default=None, alias="inputSchema")
|
|
53
|
+
mcp_server_id: str | None = Field(default=None, alias="avp.mcp_server_id")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SubagentDecl(BaseModel):
|
|
57
|
+
"""Subagent descriptor in `agent_started.data["avp.subagents"]`: what the
|
|
58
|
+
parent model sees when deciding whether to delegate. Same MCP-shaped
|
|
59
|
+
triple (`name`, `description`, `inputSchema`) tools use, so adapters
|
|
60
|
+
can render subagents to the model's tool list with no translation.
|
|
61
|
+
|
|
62
|
+
`description` is optional to match `ToolDecl`: when surfacing a
|
|
63
|
+
agent-built-in subagent (e.g. the Claude Agent SDK's `general-purpose`)
|
|
64
|
+
the agent has authoritative knowledge of the name but not the prose
|
|
65
|
+
description. Honest-null beats authored-prose-that-drifts."""
|
|
66
|
+
|
|
67
|
+
model_config = _OPEN
|
|
68
|
+
name: str
|
|
69
|
+
description: str | None = None
|
|
70
|
+
inputSchema: dict[str, Any] | None = Field(default=None, alias="inputSchema")
|
|
71
|
+
agent_type: str | None = Field(default=None, alias="avp.agent_type")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SkillDecl(BaseModel):
|
|
75
|
+
"""Skill descriptor in `AgentDescriptor.skills` and
|
|
76
|
+
`agent_started.data["avp.skills"]`: name plus optional metadata about each
|
|
77
|
+
skill the agent ships with or has loaded for the run.
|
|
78
|
+
|
|
79
|
+
Replaces the v0.1-prototype `list[str]` shape (names-only) with a
|
|
80
|
+
structured decl matching `ToolDecl` / `SubagentDecl`. Description
|
|
81
|
+
comes from the SKILL.md frontmatter when the agent surfaces it
|
|
82
|
+
(e.g. via `ClaudeSDKClient.get_context_usage()` which returns a
|
|
83
|
+
`skills` breakdown including frontmatter); `version` is the skill's
|
|
84
|
+
own version when known; `avp.source` is the SKILL.md path / URI.
|
|
85
|
+
|
|
86
|
+
All fields except `name` are optional so agents that only know
|
|
87
|
+
the name (Commission-declared without enrichment) still emit valid
|
|
88
|
+
decls."""
|
|
89
|
+
|
|
90
|
+
model_config = _OPEN
|
|
91
|
+
name: str
|
|
92
|
+
description: str | None = None
|
|
93
|
+
version: str | None = None
|
|
94
|
+
source: str | None = Field(default=None, alias="avp.source")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class McpServerDecl(BaseModel):
|
|
98
|
+
"""MCP server descriptor in `AgentDescriptor.mcp_servers` and
|
|
99
|
+
`agent_started.data["avp.mcp_servers"]`: identity + terminal dial status.
|
|
100
|
+
|
|
101
|
+
Connection material (URLs, auth, command-lines) stays inside the agent
|
|
102
|
+
process and is NOT carried on the descriptor wire. The descriptor
|
|
103
|
+
records the server's id, optional display name, optional description,
|
|
104
|
+
and the terminal dial status when known. The tools the server surfaces
|
|
105
|
+
are enumerated in the sibling `tools[]` list with `avp.mcp_server_id`
|
|
106
|
+
set to this server's `id`; only `status: "connected"` servers
|
|
107
|
+
contribute tools.
|
|
108
|
+
|
|
109
|
+
`id` is the agent's correlation key for this server across the wire
|
|
110
|
+
(descriptor entry, tool entry's `avp.mcp_server_id`). It is intentionally
|
|
111
|
+
looser than `Commission.McpServerRef.id`: the descriptor enumerates BOTH
|
|
112
|
+
Commission-resolved servers (where `id` is the supervisor-authored slug)
|
|
113
|
+
AND agent-baked-in / environment-resident servers (where `id` is whatever
|
|
114
|
+
the environment names them, e.g. `"claude.ai Dashboard Builder"`). Forcing
|
|
115
|
+
a slug here would either lose fidelity or require every agent to invent
|
|
116
|
+
the same slugification rule. Commission-authored ids stay slug-clean by
|
|
117
|
+
virtue of `Commission.McpServerRef.id`'s pattern; descriptor ids must
|
|
118
|
+
only be non-empty.
|
|
119
|
+
|
|
120
|
+
`name` is the display name when the environment provides one distinct
|
|
121
|
+
from `id` (typical for Commission-resolved servers: `id` is the
|
|
122
|
+
Commission slug, `name` is the human-readable label from the resolved
|
|
123
|
+
config). For environment-resident servers whose only identifier is
|
|
124
|
+
the display name, `id` carries that string and `name` is omitted.
|
|
125
|
+
|
|
126
|
+
`status` records the dial outcome at startup. Pre-flight `<agent> describe`
|
|
127
|
+
MAY omit it (no dial has happened); on-the-wire `agent_described` and
|
|
128
|
+
`agent_started` populate it. Values mirror the Claude Agent SDK's
|
|
129
|
+
`McpServerStatus.status` enum."""
|
|
130
|
+
|
|
131
|
+
model_config = _OPEN
|
|
132
|
+
id: str = Field(min_length=1)
|
|
133
|
+
name: str | None = None
|
|
134
|
+
description: str | None = None
|
|
135
|
+
status: Literal["connected", "failed", "needs-auth", "pending", "disabled"] | None = None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class AgentDescriptor(BaseModel):
|
|
139
|
+
"""Self-description of an AVP agent: the static surface it ships with.
|
|
140
|
+
|
|
141
|
+
Identity, capabilities, supported models, system prompt, baked-in user
|
|
142
|
+
prompt (for autonomous agents), MCP servers, tools, skills, subagents.
|
|
143
|
+
Provenance inside the agent doesn't matter on the wire: an SDK preset
|
|
144
|
+
tool (`Grep`), a runtime-bundled skill, and a hand-coded tool are all
|
|
145
|
+
just "what's in the agent" to a Descriptor consumer.
|
|
146
|
+
|
|
147
|
+
Two views, normatively consistent:
|
|
148
|
+
|
|
149
|
+
1. **Pre-flight**: `<agent> describe` prints the Descriptor as JSON.
|
|
150
|
+
2. **On the wire**: `agent_described.data["avp.descriptor"]` carries
|
|
151
|
+
the same payload during a run.
|
|
152
|
+
|
|
153
|
+
The pre-flight view MAY omit MCP-surfaced `tools[]` entries (those
|
|
154
|
+
whose `avp.mcp_server_id` is set) and per-server `mcp_servers[].status`,
|
|
155
|
+
since both require the agent to dial its MCP servers and run
|
|
156
|
+
`tools/list` — work the agent only needs to do at run-time. Every
|
|
157
|
+
other field MUST be identical between the two views.
|
|
158
|
+
|
|
159
|
+
Anything that varies per invocation (per-call prompt, run_id, thread_id,
|
|
160
|
+
additional supervisor-managed assets) belongs on the Commission, not
|
|
161
|
+
here.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
model_config = _STRICT
|
|
165
|
+
agent_name: str = Field(min_length=1)
|
|
166
|
+
agent_version: str = Field(min_length=1)
|
|
167
|
+
spec_version: Literal["0.1"]
|
|
168
|
+
default_model: str | None = None
|
|
169
|
+
# Optional whitelist of models the agent's driver / wrapped SDK can run.
|
|
170
|
+
# Each entry is a glob pattern matched against `Commission.model`
|
|
171
|
+
# (fnmatch semantics): "claude-*" matches any Claude model,
|
|
172
|
+
# "claude-haiku-4-5-*" pins to Haiku 4.5 builds, "gpt-*" matches
|
|
173
|
+
# any GPT. When None, the agent advertises support for any model
|
|
174
|
+
# the supervisor provides, but the driver may still fail at the
|
|
175
|
+
# provider call. When set, an agent SHOULD validate `Commission.model`
|
|
176
|
+
# at startup and emit `error_occurred(code: "unsupported_model")` +
|
|
177
|
+
# `agent_stopped(reason: "error")` before any model turn if the
|
|
178
|
+
# provided model is not matched.
|
|
179
|
+
supported_models: list[str] | None = None
|
|
180
|
+
# System prompt the agent ships with. Commission.system_prompt overrides
|
|
181
|
+
# when both are set (see spec §2.7).
|
|
182
|
+
system_prompt: str | None = None
|
|
183
|
+
# Baked-in user prompt for autonomous agents (cron-style runs with no
|
|
184
|
+
# per-call user message). Commission.prompt overrides when both are set.
|
|
185
|
+
prompt: str | None = None
|
|
186
|
+
# MCP servers the agent dials at startup. Connection material stays
|
|
187
|
+
# inside the agent process; only identity (id, name, description) and
|
|
188
|
+
# the terminal dial `status` are on the wire. Tools surfaced by these
|
|
189
|
+
# servers appear in `tools` with `avp.mcp_server_id` set to the
|
|
190
|
+
# server's id.
|
|
191
|
+
mcp_servers: list[McpServerDecl] | None = None
|
|
192
|
+
tools: list[ToolDecl] | None = None
|
|
193
|
+
subagents: list[SubagentDecl] | None = None
|
|
194
|
+
skills: list[SkillDecl] | None = None
|
|
195
|
+
capabilities: list[str] | None = None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
__all__ = [
|
|
199
|
+
"AgentDescriptor",
|
|
200
|
+
"McpServerDecl",
|
|
201
|
+
"SkillDecl",
|
|
202
|
+
"SubagentDecl",
|
|
203
|
+
"ToolDecl",
|
|
204
|
+
]
|
avp/envelope.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Shared CloudEvents 1.0 / OTel span scaffolding for AVP v0.1.
|
|
2
|
+
|
|
3
|
+
Private module. Consumers MUST import from the spec-scoped namespaces
|
|
4
|
+
(`avp.trajectory`, `avp.commission`, `avp.descriptor`)
|
|
5
|
+
which re-export the public bits of this file.
|
|
6
|
+
|
|
7
|
+
What lives here is the cross-cutting wire scaffolding shared by every
|
|
8
|
+
spec module:
|
|
9
|
+
|
|
10
|
+
- CloudEvents 1.0 envelope (`_CloudEventBase`).
|
|
11
|
+
- OTel span identification carried on every event's `data`
|
|
12
|
+
(`_SpanData`).
|
|
13
|
+
- Source URI (`SOURCE_AGENT`) used by every event type. The agent is
|
|
14
|
+
the sole producer on the wire (spec §8 conformance #1); supervisor
|
|
15
|
+
attribution lives in `run_requested.data` (`avp.commission` +
|
|
16
|
+
`avp.supervisor.*`), not in the envelope's `source` field.
|
|
17
|
+
- Pydantic `model_config` presets (`_STRICT`, `_OPEN`) used by every
|
|
18
|
+
spec model.
|
|
19
|
+
- ID / timestamp generators used as Pydantic field defaults
|
|
20
|
+
(`now_iso`, `new_event_id`, `new_trace_id`, `new_span_id`,
|
|
21
|
+
`ZERO_SPAN_ID`).
|
|
22
|
+
|
|
23
|
+
Nothing spec-specific belongs here. Commission, Descriptor, and
|
|
24
|
+
trajectory event types live in their own modules.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import secrets
|
|
30
|
+
import uuid
|
|
31
|
+
from datetime import UTC, datetime
|
|
32
|
+
from typing import Any, Literal
|
|
33
|
+
|
|
34
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
35
|
+
|
|
36
|
+
Iso8601 = str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def now_iso() -> str:
|
|
40
|
+
"""ISO 8601 / RFC 3339 timestamp with Z suffix."""
|
|
41
|
+
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def new_event_id() -> str:
|
|
45
|
+
"""CloudEvents 1.0 requires `id` unique within `source`. UUID v4 satisfies that."""
|
|
46
|
+
return str(uuid.uuid4())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def new_trace_id() -> str:
|
|
50
|
+
"""OTel trace ID: 16 random bytes, hex-encoded (32 lowercase chars)."""
|
|
51
|
+
return secrets.token_hex(16)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def new_span_id() -> str:
|
|
55
|
+
"""OTel span ID: 8 random bytes, hex-encoded (16 lowercase chars)."""
|
|
56
|
+
return secrets.token_hex(8)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# 16 zero hex chars: the OTel "absent parent" sentinel for top-level spans.
|
|
60
|
+
ZERO_SPAN_ID = "0" * 16
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Source URI (CloudEvents reverse-DNS). The agent is the sole producer on
|
|
64
|
+
# the wire (spec §8 conformance #1); every event carries `avp://agent`.
|
|
65
|
+
# Supervisor attribution, when applicable, rides inside
|
|
66
|
+
# `run_requested.data` (`avp.commission` + `avp.supervisor.*`).
|
|
67
|
+
SOURCE_AGENT = "avp://agent"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Pydantic model_config presets. `populate_by_name=True` lets parsers accept
|
|
71
|
+
# either the alias (wire form: dotted) or the Python attribute name. `by_alias`
|
|
72
|
+
# is passed at serialization time to emit the alias form on the wire.
|
|
73
|
+
_STRICT = ConfigDict(extra="forbid", populate_by_name=True, ser_json_omit_default=False)
|
|
74
|
+
_OPEN = ConfigDict(extra="allow", populate_by_name=True)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _SpanData(BaseModel):
|
|
78
|
+
"""Span identification carried by every AVP event's `data` payload.
|
|
79
|
+
|
|
80
|
+
`extra="allow"` lets vendor-namespaced extension attributes (e.g.,
|
|
81
|
+
`vendor.priority`, `vendor.trace_id`) round-trip through the trajectory
|
|
82
|
+
verbatim. Spec-defined attributes are validated; unknown keys pass through.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
model_config = _OPEN
|
|
86
|
+
trace_id: str = Field(min_length=32, max_length=32, pattern=r"^[0-9a-f]{32}$")
|
|
87
|
+
span_id: str = Field(min_length=16, max_length=16, pattern=r"^[0-9a-f]{16}$")
|
|
88
|
+
parent_span_id: str = Field(min_length=16, max_length=16, pattern=r"^[0-9a-f]{16}$")
|
|
89
|
+
meta: dict[str, Any] | None = Field(default=None, alias="avp.meta")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class _CloudEventBase(BaseModel):
|
|
93
|
+
"""Shared CloudEvents 1.0 envelope fields. Specific events override
|
|
94
|
+
`type` and `source` with Literal constants and define `data: <Type>Data`.
|
|
95
|
+
|
|
96
|
+
Per CloudEvents §1: required `specversion`, `id`, `source`, `type`.
|
|
97
|
+
Optional: `subject`, `time`, `datacontenttype`, `dataschema`. AVP uses
|
|
98
|
+
`subject` to carry run_id.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
model_config = _STRICT
|
|
102
|
+
specversion: Literal["1.0"] = "1.0"
|
|
103
|
+
id: str = Field(min_length=1, default_factory=new_event_id)
|
|
104
|
+
time: Iso8601 = Field(default_factory=now_iso)
|
|
105
|
+
subject: str | None = Field(default=None, min_length=1) # run_id
|
|
106
|
+
datacontenttype: str | None = "application/json"
|
|
107
|
+
dataschema: str | None = None
|
|
108
|
+
correlation_id: str | None = Field(default=None, min_length=1, alias="avp.correlation_id")
|
avp/gen_ai.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""avp.gen_ai — Project AVP trajectory events into OpenTelemetry GenAI attributes.
|
|
2
|
+
|
|
3
|
+
OTel GenAI semantic conventions registry:
|
|
4
|
+
https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/
|
|
5
|
+
|
|
6
|
+
AVP's wire format carries attributes under its own `avp.*` namespace.
|
|
7
|
+
Consumers forwarding the same data into an OTel-native backend (OTLP
|
|
8
|
+
collectors, Honeycomb / Datadog / Grafana GenAI views) call
|
|
9
|
+
`to_gen_ai_attrs(event)` to derive a dict of `gen_ai.*` attributes ready
|
|
10
|
+
to attach to a span. The AVP wire stays put; this is the projection
|
|
11
|
+
layer.
|
|
12
|
+
|
|
13
|
+
The projection is one-way (AVP wire → OTel attrs), one event at a time.
|
|
14
|
+
AVP-specific fields without an OTel equivalent (`avp.cost_usd`,
|
|
15
|
+
`avp.refusal.category`, ...) are intentionally NOT projected. See
|
|
16
|
+
`FOUNDATIONS.md` for the mapping table and rationale.
|
|
17
|
+
|
|
18
|
+
## Un-projected OTel GenAI attributes
|
|
19
|
+
|
|
20
|
+
The following registry attributes are NOT in the projection:
|
|
21
|
+
|
|
22
|
+
**Spec gaps — present in OTel, absent from AVP wire today:**
|
|
23
|
+
|
|
24
|
+
- Sampling parameters: `gen_ai.request.{max_tokens, temperature, top_p,
|
|
25
|
+
top_k, frequency_penalty, presence_penalty, seed, stop_sequences,
|
|
26
|
+
choice_count, stream}`. Belong on `Commission` / `agent_started`.
|
|
27
|
+
- `gen_ai.response.id` — provider-assigned response id (e.g. OpenAI's
|
|
28
|
+
`id`). Would live on `AssistantMessageData`.
|
|
29
|
+
- `gen_ai.tool.{type, description, definitions}` — tool classification
|
|
30
|
+
and per-tool metadata. AVP has descriptions on
|
|
31
|
+
`agent_started.tools[]` decls but not on dispatch events.
|
|
32
|
+
|
|
33
|
+
**Projector-shape limitation:**
|
|
34
|
+
|
|
35
|
+
- `gen_ai.input.messages` — requires the prior event stream, not a
|
|
36
|
+
single event. Build via `avp.history.to_messages(events_so_far)`
|
|
37
|
+
and attach manually when entering an `assistant_message` span.
|
|
38
|
+
|
|
39
|
+
**Out of scope for AVP (see [trajectory.md §1.1](../../../spec/v0.1/trajectory.md) non-goals):**
|
|
40
|
+
|
|
41
|
+
- Retrieval: `gen_ai.retrieval.{query.text, documents}` — RAG-specific.
|
|
42
|
+
- Evaluation: `gen_ai.evaluation.{name, score.value, score.label,
|
|
43
|
+
explanation}` — post-hoc annotation, not runtime.
|
|
44
|
+
- Workflow / data source: `gen_ai.{workflow.name, data_source.id}` —
|
|
45
|
+
supervisor-framework concerns above the wire.
|
|
46
|
+
- Embeddings: `gen_ai.request.encoding_formats`,
|
|
47
|
+
`gen_ai.embeddings.dimension.count` — AVP isn't designed for
|
|
48
|
+
embedding workloads.
|
|
49
|
+
- Per-token granularity: `gen_ai.token.type`.
|
|
50
|
+
- Output modality: `gen_ai.output.type`.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
from typing import Any
|
|
56
|
+
|
|
57
|
+
from avp.trajectory import (
|
|
58
|
+
AgentDescribedEvent,
|
|
59
|
+
AgentStartedEvent,
|
|
60
|
+
AssistantMessageEvent,
|
|
61
|
+
Event,
|
|
62
|
+
SubagentInvokedEvent,
|
|
63
|
+
SubagentReturnedEvent,
|
|
64
|
+
ToolInvokedEvent,
|
|
65
|
+
ToolReturnedEvent,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _drop_none(attrs: dict[str, Any]) -> dict[str, Any]:
|
|
70
|
+
return {k: v for k, v in attrs.items() if v is not None}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def to_gen_ai_attrs(event: Event) -> dict[str, Any]:
|
|
74
|
+
"""Project an AVP `Event` into a dict of OTel `gen_ai.*` attributes.
|
|
75
|
+
|
|
76
|
+
Keys are the OTel GenAI registry names; values are passed through
|
|
77
|
+
from the AVP payload unchanged (no unit conversion). Returns `{}`
|
|
78
|
+
for events with no GenAI projection (`run_requested`, `agent_stopped`,
|
|
79
|
+
`mcp_*`, `error_occurred`, `UnknownEvent`).
|
|
80
|
+
"""
|
|
81
|
+
if isinstance(event, AgentStartedEvent):
|
|
82
|
+
d = event.data
|
|
83
|
+
return _drop_none(
|
|
84
|
+
{
|
|
85
|
+
"gen_ai.provider.name": d.provider_name,
|
|
86
|
+
"gen_ai.operation.name": d.operation_name,
|
|
87
|
+
"gen_ai.request.model": d.request_model,
|
|
88
|
+
"gen_ai.conversation.id": d.thread_id,
|
|
89
|
+
"gen_ai.system_instructions": d.system_prompt,
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if isinstance(event, AssistantMessageEvent):
|
|
94
|
+
d = event.data
|
|
95
|
+
u = d.usage
|
|
96
|
+
output_messages = [
|
|
97
|
+
{
|
|
98
|
+
"role": "assistant",
|
|
99
|
+
"content": [
|
|
100
|
+
b.model_dump(by_alias=True, exclude_none=True, mode="json") for b in d.content
|
|
101
|
+
],
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
return _drop_none(
|
|
105
|
+
{
|
|
106
|
+
"gen_ai.provider.name": d.provider_name,
|
|
107
|
+
"gen_ai.request.model": d.request_model,
|
|
108
|
+
"gen_ai.response.model": d.response_model,
|
|
109
|
+
"gen_ai.response.finish_reasons": d.response_finish_reasons,
|
|
110
|
+
"gen_ai.response.time_to_first_chunk": d.response_time_to_first_chunk,
|
|
111
|
+
"gen_ai.usage.input_tokens": u.input_tokens,
|
|
112
|
+
"gen_ai.usage.output_tokens": u.output_tokens,
|
|
113
|
+
"gen_ai.usage.cache_read.input_tokens": u.cache_read_input_tokens,
|
|
114
|
+
"gen_ai.usage.cache_creation.input_tokens": u.cache_creation_input_tokens,
|
|
115
|
+
"gen_ai.usage.reasoning.output_tokens": u.reasoning_output_tokens,
|
|
116
|
+
"gen_ai.output.messages": output_messages,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if isinstance(event, ToolInvokedEvent):
|
|
121
|
+
d = event.data
|
|
122
|
+
return {
|
|
123
|
+
"gen_ai.tool.name": d.tool_name,
|
|
124
|
+
"gen_ai.tool.call.id": d.tool_call_id,
|
|
125
|
+
"gen_ai.tool.call.arguments": d.tool_input,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if isinstance(event, ToolReturnedEvent):
|
|
129
|
+
d = event.data
|
|
130
|
+
return {
|
|
131
|
+
"gen_ai.tool.name": d.tool_name,
|
|
132
|
+
"gen_ai.tool.call.id": d.tool_call_id,
|
|
133
|
+
"gen_ai.tool.call.result": d.tool_result.content,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if isinstance(event, SubagentInvokedEvent):
|
|
137
|
+
d = event.data
|
|
138
|
+
return _drop_none(
|
|
139
|
+
{
|
|
140
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
141
|
+
"gen_ai.agent.name": d.subagent_name,
|
|
142
|
+
"gen_ai.agent.description": d.subagent_description,
|
|
143
|
+
"gen_ai.agent.id": d.subagent_run_id,
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if isinstance(event, SubagentReturnedEvent):
|
|
148
|
+
return {"gen_ai.agent.name": event.data.subagent_name}
|
|
149
|
+
|
|
150
|
+
if isinstance(event, AgentDescribedEvent):
|
|
151
|
+
desc = event.data.descriptor
|
|
152
|
+
return {
|
|
153
|
+
"gen_ai.agent.name": desc.agent_name,
|
|
154
|
+
"gen_ai.agent.version": desc.agent_version,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
__all__ = ["to_gen_ai_attrs"]
|
avp/history.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""avp.history — Reconstruct a provider-style message history from a trajectory.
|
|
2
|
+
|
|
3
|
+
A trajectory is the agent's stream-of-events record. A *message history*
|
|
4
|
+
is the provider's input shape: a list of `{role, content}` records. This
|
|
5
|
+
module converts the former to the latter, faithfully enough that the
|
|
6
|
+
same conversation could be replayed against any provider's chat API.
|
|
7
|
+
|
|
8
|
+
The mapping is:
|
|
9
|
+
|
|
10
|
+
- `agent_started.avp.system_prompt` → one `system` message.
|
|
11
|
+
- `agent_started.avp.prompt` → one `user` message (initial turn).
|
|
12
|
+
- `assistant_message.avp.content` → one `assistant` message per turn.
|
|
13
|
+
- Each `tool_returned.avp.tool_result` between two assistant turns
|
|
14
|
+
bundles into a single `user` message preceding the next assistant turn
|
|
15
|
+
(mirroring how providers shuttle tool results in user-role messages).
|
|
16
|
+
|
|
17
|
+
Other event types (`tool_invoked`, `mcp_*`, `error_occurred`, `agent_*`,
|
|
18
|
+
`UnknownEvent`, ...) are observability or run-control facts that don't
|
|
19
|
+
contribute to message history; they are skipped.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from collections.abc import Iterable
|
|
25
|
+
from typing import Literal
|
|
26
|
+
|
|
27
|
+
from pydantic import BaseModel
|
|
28
|
+
|
|
29
|
+
from avp.content import AVPContentBlock, TextBlock
|
|
30
|
+
from avp.envelope import _OPEN
|
|
31
|
+
from avp.trajectory import (
|
|
32
|
+
AgentStartedEvent,
|
|
33
|
+
AssistantMessageEvent,
|
|
34
|
+
Event,
|
|
35
|
+
ToolReturnedEvent,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Message(BaseModel):
|
|
40
|
+
"""One entry of a provider-style message history."""
|
|
41
|
+
|
|
42
|
+
model_config = _OPEN
|
|
43
|
+
role: Literal["user", "assistant", "system"]
|
|
44
|
+
content: list[AVPContentBlock]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def to_messages(events: Iterable[Event]) -> list[Message]:
|
|
48
|
+
"""Reconstruct a provider-style message history from a trajectory.
|
|
49
|
+
|
|
50
|
+
See the module docstring for the event-to-message mapping.
|
|
51
|
+
"""
|
|
52
|
+
messages: list[Message] = []
|
|
53
|
+
pending: list[AVPContentBlock] = []
|
|
54
|
+
|
|
55
|
+
def flush() -> None:
|
|
56
|
+
if pending:
|
|
57
|
+
messages.append(Message(role="user", content=list(pending)))
|
|
58
|
+
pending.clear()
|
|
59
|
+
|
|
60
|
+
for event in events:
|
|
61
|
+
if isinstance(event, AgentStartedEvent):
|
|
62
|
+
if event.data.system_prompt:
|
|
63
|
+
messages.append(
|
|
64
|
+
Message(
|
|
65
|
+
role="system",
|
|
66
|
+
content=[TextBlock(text=event.data.system_prompt)],
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
if event.data.prompt:
|
|
70
|
+
messages.append(
|
|
71
|
+
Message(
|
|
72
|
+
role="user",
|
|
73
|
+
content=[TextBlock(text=event.data.prompt)],
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
elif isinstance(event, AssistantMessageEvent):
|
|
77
|
+
flush()
|
|
78
|
+
messages.append(Message(role="assistant", content=list(event.data.content)))
|
|
79
|
+
elif isinstance(event, ToolReturnedEvent):
|
|
80
|
+
pending.append(event.data.tool_result)
|
|
81
|
+
|
|
82
|
+
flush()
|
|
83
|
+
return messages
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
__all__ = ["Message", "to_messages"]
|