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 ADDED
@@ -0,0 +1,31 @@
1
+ """avp — Python reference implementation for the Agent Voyager Project (v0.1 model).
2
+
3
+ The wire format is built on CloudEvents 1.0, OpenTelemetry GenAI semantic
4
+ conventions, OTel spans, JSON-RPC 2.0, MCP, Agent Skills, and JSON Schema.
5
+ See FOUNDATIONS.md for the full mapping.
6
+
7
+ Public API lives in the spec-scoped submodules; this package's top level
8
+ exposes only version metadata. Import wire types and helpers directly from
9
+ the module that owns them:
10
+
11
+ from avp.commission import Commission, McpServerHttp, McpServerStdio, Skill
12
+ from avp.descriptor import AgentDescriptor
13
+ from avp.trajectory import (
14
+ AgentStartedEvent,
15
+ Event,
16
+ parse_event,
17
+ event_to_wire,
18
+ )
19
+ from avp.tracer import AVPTracer, current_tracer
20
+ from avp.io import iter_events, write_event
21
+ from avp.enums import ErrorCode, StopReason
22
+ from avp.pricing import compute_cost, load_default_prices
23
+
24
+ Doing it this way keeps the spec ↔ module mapping 1:1 and prevents drift
25
+ into a single "everything-bag" import surface.
26
+ """
27
+
28
+ __version__ = "0.1.0"
29
+ SCHEMA_VERSION = "0.1"
30
+
31
+ __all__ = ["SCHEMA_VERSION", "__version__"]
avp/commission.py ADDED
@@ -0,0 +1,236 @@
1
+ """avp.commission — Pydantic types for the AVP Commission Spec.
2
+
3
+ Defines the Commission shape (supervisor → agent setup message) and the
4
+ managed-asset entries the supervisor declares inline. This module mirrors
5
+ the [Commission spec](../../../../spec/v0.1/commission.md).
6
+
7
+ Consumers wanting only the run-config object can:
8
+
9
+ from avp.commission import Commission, McpServerHttp, McpServerStdio
10
+
11
+ …without dragging in Trajectory / Descriptor types.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Annotated, Any, Literal
17
+
18
+ from pydantic import BaseModel, Field, field_validator
19
+
20
+ from avp.envelope import _STRICT
21
+
22
+ _ID_PATTERN = r"^[a-z0-9_-]+$"
23
+
24
+ # `model` is a canonical models.dev slug: "<origin>/<model>" (e.g.
25
+ # "anthropic/claude-opus-4-8", "openai/gpt-4o"). The origin segment is the
26
+ # model's home namespace and the pricing key; it is independent of the
27
+ # `Provider.id` storefront that actually serves the tokens.
28
+ _MODEL_SLUG_PATTERN = r"^[^/]+/.+$"
29
+
30
+
31
+ class SecretRef(BaseModel):
32
+ """A reference to a secret the supervisor resolves out of band.
33
+
34
+ Carries a `vault` handle, never the secret value. The supervisor maps the
35
+ handle to material (env var, secrets file, broker) at run time; the value
36
+ never appears on the wire or in the trajectory. Used by `Provider.credential`
37
+ and `McpServerHttp.auth`.
38
+ """
39
+
40
+ model_config = _STRICT
41
+ vault: str = Field(min_length=1, pattern=_ID_PATTERN)
42
+
43
+
44
+ class McpServerHttp(BaseModel):
45
+ """Inline HTTP MCP server entry in Commission.mcp_servers."""
46
+
47
+ model_config = _STRICT
48
+ id: str = Field(min_length=1, pattern=_ID_PATTERN)
49
+ type: Literal["http"]
50
+ url: str = Field(min_length=1)
51
+ # Non-secret request headers. Credentials go in `auth` (a SecretRef the
52
+ # supervisor resolves out of band), not here.
53
+ headers: dict[str, str] | None = None
54
+ auth: SecretRef | None = None
55
+
56
+
57
+ class McpServerStdio(BaseModel):
58
+ """Inline stdio MCP server entry in Commission.mcp_servers."""
59
+
60
+ model_config = _STRICT
61
+ id: str = Field(min_length=1, pattern=_ID_PATTERN)
62
+ type: Literal["stdio"]
63
+ command: list[str] = Field(min_length=1)
64
+ args: list[str] | None = None
65
+ env: dict[str, str] | None = None
66
+
67
+
68
+ McpServer = Annotated[McpServerHttp | McpServerStdio, Field(discriminator="type")]
69
+
70
+
71
+ class Provider(BaseModel):
72
+ """Optional LLM routing override: which storefront serves the model.
73
+
74
+ Absent → the agent uses its native default (whatever its own environment
75
+ configures). Present → the supervisor directs the agent at a specific
76
+ endpoint. `id` selects the protocol/auth family (e.g. "anthropic",
77
+ "openai", "openrouter"); `base_url` overrides the endpoint; `credential`
78
+ references the API key by vault handle (never the value).
79
+
80
+ The model's origin (the `Commission.model` slug's first segment) and the
81
+ storefront `id` are independent axes: `model: "openai/gpt-4o"` with
82
+ `provider.id: "openrouter"` reads as "OpenAI's gpt-4o, bought through
83
+ OpenRouter". An agent that cannot speak the requested provider's protocol
84
+ MUST fail (error_occurred + agent_stopped reason=error), never silently
85
+ run elsewhere.
86
+ """
87
+
88
+ model_config = _STRICT
89
+ id: str = Field(min_length=1, pattern=_ID_PATTERN)
90
+ base_url: str | None = None
91
+ credential: SecretRef | None = None
92
+
93
+
94
+ class Skill(BaseModel):
95
+ """Inline skill entry in Commission.skills."""
96
+
97
+ model_config = _STRICT
98
+ id: str = Field(min_length=1, pattern=_ID_PATTERN)
99
+ files: dict[str, str]
100
+
101
+ @field_validator("files")
102
+ @classmethod
103
+ def _require_skill_md(cls, v: dict[str, str]) -> dict[str, str]:
104
+ if "SKILL.md" not in v:
105
+ raise ValueError("files must contain 'SKILL.md'")
106
+ return v
107
+
108
+ def _frontmatter_value(self, key: str) -> str | None:
109
+ content = self.files.get("SKILL.md", "")
110
+ if not content.startswith("---"):
111
+ return None
112
+ end = content.find("---", 3)
113
+ if end == -1:
114
+ return None
115
+ for line in content[3:end].splitlines():
116
+ if line.startswith(f"{key}:"):
117
+ return line[len(key) + 1 :].strip() or None
118
+ return None
119
+
120
+ @property
121
+ def name(self) -> str | None:
122
+ return self._frontmatter_value("name")
123
+
124
+ @property
125
+ def description(self) -> str | None:
126
+ return self._frontmatter_value("description")
127
+
128
+
129
+ class SupervisorPreamble(BaseModel):
130
+ """Identifies the supervisor that is requesting the run.
131
+
132
+ Carried inside `Commission.supervisor` and projected onto the
133
+ `run_requested` event's `data` (`avp.supervisor.name` +
134
+ `avp.supervisor.version`) so a trajectory consumer can attribute the
135
+ run to the originating supervisor without an out-of-band lookup. The
136
+ event's `source` is `avp://agent` (the agent is the sole producer on
137
+ the wire); supervisor attribution lives inside `data`.
138
+
139
+ `name` SHOULD be a stable identifier for the supervisor implementation
140
+ or instance (e.g. `"avp-cli"`, `"acme.scheduler"`).
141
+ `version` is optional but recommended; it travels with the trajectory
142
+ and lets auditors correlate a run with the exact supervisor build
143
+ that requested it.
144
+ """
145
+
146
+ model_config = _STRICT
147
+ name: str = Field(min_length=1)
148
+ version: str | None = None
149
+
150
+
151
+ class Commission(BaseModel):
152
+ """Supervisor's declaration of the supervisor-managed environment slice.
153
+
154
+ Managed asset entries (`mcp_servers`, `skills`) carry inline connection
155
+ material; no resolver round-trip is needed. The agent dials MCP servers
156
+ and injects skill content directly from these fields at startup.
157
+
158
+ Anything the agent provides on its own (in-process tools, baked-in
159
+ skills) is invisible to AVP and the Commission entirely. The agent's own
160
+ contribution surfaces in `agent_described.data["avp.descriptor"]` so
161
+ consumers can audit what the agent showed up with. The agent's runtime
162
+ layer merges its internal contribution with the Commission-managed assets
163
+ into one bag the loop dispatches against; collisions on `id` are a
164
+ startup error.
165
+ """
166
+
167
+ model_config = _STRICT
168
+
169
+ schema_version: Literal["0.1"]
170
+ run_id: str = Field(min_length=1)
171
+
172
+ # Supervisor identity. Optional but RECOMMENDED. When present, the agent
173
+ # stamps `run_requested.data.avp.supervisor.*` from this field so the
174
+ # trajectory records who requested the run. When absent, the agent
175
+ # still emits `run_requested` but with `avp.supervisor.name="unknown"`.
176
+ supervisor: SupervisorPreamble | None = None
177
+
178
+ # Supervisor-managed assets. Connection material is inline; agents dial
179
+ # MCP servers and load skill content directly at startup.
180
+ mcp_servers: list[McpServer] | None = None
181
+ skills: list[Skill] | None = None
182
+
183
+ # Optional LLM routing override. Absent → the agent's native default.
184
+ provider: Provider | None = None
185
+
186
+ # Allow-lists over the agent's Descriptor-declared surface. Each list
187
+ # gates the parallel `descriptor.*` field for this run.
188
+ #
189
+ # - None (absent) → every descriptor entry of that kind is exposed
190
+ # (default).
191
+ # - [] → none are exposed.
192
+ # - [n1, n2, …] → only the listed names/ids are exposed; the agent
193
+ # hides the rest from the model and runtime-blocks
194
+ # any hallucinated invocation with a `tool_returned`
195
+ # (isError=True) (or, for subagents, a
196
+ # `subagent_returned` with `reason=error`).
197
+ #
198
+ # Names MUST appear in the corresponding descriptor field at startup or
199
+ # the agent emits `error_occurred(code: "commission_collision")` and
200
+ # stops with `reason=error`. Subtractive-only: these have no effect on
201
+ # supervisor-managed assets (those are gated by being-in-the-Commission).
202
+ # `enabled_builtin_mcp_servers` filters `descriptor.mcp_servers[].id`;
203
+ # disabling a server prevents the agent from dialing it, so its tools
204
+ # are unavailable for the run.
205
+ enabled_builtin_tools: list[str] | None = None
206
+ enabled_builtin_subagents: list[str] | None = None
207
+ enabled_builtin_skills: list[str] | None = None
208
+ enabled_builtin_mcp_servers: list[str] | None = None
209
+
210
+ output_schema: dict[str, Any] | None = None
211
+
212
+ # Agent plane (what the agent runs)
213
+ prompt: str | None = None
214
+ system_prompt: str | None = None
215
+ # Canonical models.dev slug "<origin>/<model>" (e.g. "anthropic/claude-opus-4-8").
216
+ # The pattern requires a non-empty origin and model id. Required: the origin
217
+ # segment is the pricing key and the native-default routing hint; agents
218
+ # split it off to get the SDK-native model id.
219
+ model: str = Field(min_length=1, pattern=_MODEL_SLUG_PATTERN)
220
+
221
+ # Metadata
222
+ thread_id: str | None = None
223
+ tags: list[str] | None = None
224
+ meta: dict[str, Any] | None = None
225
+
226
+
227
+ __all__ = [
228
+ "Commission",
229
+ "McpServer",
230
+ "McpServerHttp",
231
+ "McpServerStdio",
232
+ "Provider",
233
+ "SecretRef",
234
+ "Skill",
235
+ "SupervisorPreamble",
236
+ ]
avp/content.py ADDED
@@ -0,0 +1,273 @@
1
+ """avp.content — AVP content block types for assistant message content.
2
+
3
+ Normalized content union covering the message-history shapes of Anthropic
4
+ Messages, OpenAI Chat Completions + Responses, Google Gemini
5
+ generateContent, AWS Bedrock Converse, Cohere Chat, and Mistral Chat.
6
+ The goal is non-lossy round-trip of agent history across providers.
7
+
8
+ Every block sets `model_config = ConfigDict(extra="allow")` so unmodeled
9
+ provider-specific fields (Anthropic `cache_control`, OpenAI
10
+ `encrypted_content`, Gemini `thought_signature`, future additions)
11
+ round-trip unchanged without spec churn.
12
+
13
+ Discriminate on the `type` field. Serialize with
14
+ `model_dump(by_alias=True, mode="json")`.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Annotated, Any, Literal
20
+
21
+ from pydantic import BaseModel, Field
22
+
23
+ from avp.envelope import _OPEN
24
+
25
+ # ── Source variants ───────────────────────────────────────────────────────────
26
+
27
+
28
+ class Base64Source(BaseModel):
29
+ """Inline base64-encoded media. Anthropic `source.type=base64`, Gemini
30
+ `inline_data`, Bedrock `source.bytes`."""
31
+
32
+ model_config = _OPEN
33
+ type: Literal["base64"] = "base64"
34
+ media_type: str
35
+ data: str
36
+
37
+
38
+ class UrlSource(BaseModel):
39
+ """External URL. Anthropic `source.type=url`, OpenAI `image_url`,
40
+ Gemini `file_data` (when `file_uri` is a public URL)."""
41
+
42
+ model_config = _OPEN
43
+ type: Literal["url"] = "url"
44
+ url: str
45
+
46
+
47
+ class FileSource(BaseModel):
48
+ """Provider-hosted file reference. OpenAI Files API `file_id`, Anthropic
49
+ Files API `file_id`, Gemini `file_data.file_uri` (Files API URI)."""
50
+
51
+ model_config = _OPEN
52
+ type: Literal["file"] = "file"
53
+ file_id: str
54
+
55
+
56
+ Source = Annotated[Base64Source | UrlSource | FileSource, Field(discriminator="type")]
57
+
58
+
59
+ # ── Citations / annotations ───────────────────────────────────────────────────
60
+
61
+
62
+ class Citation(BaseModel):
63
+ """Span-anchored attribution on a text or document block. Unifies
64
+ Anthropic citations (`char_location`, `page_location`,
65
+ `content_block_location`), OpenAI annotations (`url_citation`,
66
+ `file_citation`, `file_path`), and Gemini grounding chunks. `type`
67
+ carries the provider's raw citation kind verbatim so downstream
68
+ consumers can normalize without re-deriving it."""
69
+
70
+ model_config = _OPEN
71
+ type: str
72
+ cited_text: str | None = None
73
+ start_index: int | None = Field(default=None, ge=0)
74
+ end_index: int | None = Field(default=None, ge=0)
75
+ source_id: str | None = None
76
+ source_url: str | None = None
77
+ source_title: str | None = None
78
+
79
+
80
+ # ── Blocks ────────────────────────────────────────────────────────────────────
81
+
82
+
83
+ class TextBlock(BaseModel):
84
+ """Plain text content. Anthropic `text`, OpenAI `text` /
85
+ `output_text` / `input_text`, Gemini text part, Bedrock `text`,
86
+ Cohere `text`, Mistral `text`. `citations` carries Anthropic citations,
87
+ OpenAI annotations, and Gemini grounding spans anchored into this text."""
88
+
89
+ model_config = _OPEN
90
+ type: Literal["text"] = "text"
91
+ text: str
92
+ citations: list[Citation] | None = None
93
+
94
+
95
+ class ThinkingBlock(BaseModel):
96
+ """Reasoning / chain-of-thought emitted by the model.
97
+
98
+ Anthropic extended thinking, OpenAI o-series `reasoning` items,
99
+ Gemini `thought` parts, Bedrock `reasoningContent`, Mistral thinking.
100
+ `signature` is the opaque blob the provider requires echoed back on
101
+ the next turn for continued reasoning: Anthropic's cryptographic
102
+ signature, OpenAI's `encrypted_content`, or Gemini's
103
+ `thought_signature`. `redacted` flags blocks whose plaintext is
104
+ unavailable (encrypted-only form)."""
105
+
106
+ model_config = _OPEN
107
+ type: Literal["thinking"] = "thinking"
108
+ thinking: str
109
+ signature: str | None = None
110
+ redacted: bool | None = None
111
+
112
+
113
+ class ImageBlock(BaseModel):
114
+ """Image content. Anthropic `image`, OpenAI `image_url` /
115
+ `input_image`, Gemini `inline_data` / `file_data` image, Bedrock
116
+ `image`."""
117
+
118
+ model_config = _OPEN
119
+ type: Literal["image"] = "image"
120
+ source: Source
121
+
122
+
123
+ class AudioBlock(BaseModel):
124
+ """Audio content. OpenAI `input_audio` (input) and `audio` (output),
125
+ Gemini `inline_data` audio, Bedrock `audio`. `transcript` carries
126
+ OpenAI's output-audio transcript when present."""
127
+
128
+ model_config = _OPEN
129
+ type: Literal["audio"] = "audio"
130
+ source: Source
131
+ transcript: str | None = None
132
+
133
+
134
+ class VideoBlock(BaseModel):
135
+ """Video content. Gemini `inline_data` / `file_data` video, Bedrock
136
+ `video`."""
137
+
138
+ model_config = _OPEN
139
+ type: Literal["video"] = "video"
140
+ source: Source
141
+
142
+
143
+ class DocumentBlock(BaseModel):
144
+ """Document / file content (typically PDFs). Anthropic `document`
145
+ (with citation support), OpenAI `input_file`, Gemini `file_data`,
146
+ Bedrock `document`. `title` is the document name used as the
147
+ citation target; `context` is supplementary metadata Anthropic
148
+ surfaces alongside the document for the model."""
149
+
150
+ model_config = _OPEN
151
+ type: Literal["document"] = "document"
152
+ source: Source
153
+ title: str | None = None
154
+ context: str | None = None
155
+ citations: list[Citation] | None = None
156
+
157
+
158
+ class ToolUseBlock(BaseModel):
159
+ """Model invokes a client-dispatched tool. Anthropic `tool_use`,
160
+ OpenAI `function_call` / `tool_calls`, Gemini `function_call`,
161
+ Bedrock `toolUse`, Cohere tool_calls, Mistral tool_calls."""
162
+
163
+ model_config = _OPEN
164
+ type: Literal["tool_use"] = "tool_use"
165
+ id: str
166
+ name: str
167
+ input: dict[str, Any]
168
+
169
+
170
+ ToolResultContent = Annotated[TextBlock | ImageBlock | DocumentBlock, Field(discriminator="type")]
171
+
172
+
173
+ class ToolResultBlock(BaseModel):
174
+ """Result of a client-dispatched tool call. Anthropic `tool_result`,
175
+ OpenAI `function_call_output` / tool-role message, Gemini
176
+ `function_response`, Bedrock `toolResult`. Anthropic permits nested
177
+ text/image/document content blocks; other providers serialize a
178
+ flat string. `structured_content` carries a programmatic payload
179
+ alongside the human-readable `content` (MCP's `structuredContent`,
180
+ Gemini `function_response.response`, Bedrock `toolResult.content.json`);
181
+ the two channels are complementary, not alternatives. `is_error`
182
+ flags rejections."""
183
+
184
+ model_config = _OPEN
185
+ type: Literal["tool_result"] = "tool_result"
186
+ tool_use_id: str
187
+ content: str | list[ToolResultContent]
188
+ structured_content: dict[str, Any] | None = None
189
+ is_error: bool | None = None
190
+
191
+
192
+ class ServerToolUseBlock(BaseModel):
193
+ """Built-in tool executed by the provider rather than the agent.
194
+ Anthropic `server_tool_use` (web_search, code_execution), OpenAI
195
+ Responses `web_search_call` / `file_search_call` / `computer_call` /
196
+ `code_interpreter_call`, Gemini `executable_code` / `google_search`.
197
+ `name` carries the tool kind (e.g. "web_search", "code_interpreter",
198
+ "computer_use", "google_search"). Distinct from `tool_use` because
199
+ the agent never dispatches these; they are observability of a
200
+ provider-side action."""
201
+
202
+ model_config = _OPEN
203
+ type: Literal["server_tool_use"] = "server_tool_use"
204
+ id: str
205
+ name: str
206
+ input: dict[str, Any]
207
+
208
+
209
+ class ServerToolResultBlock(BaseModel):
210
+ """Result of a provider-executed built-in tool. Pairs with
211
+ `ServerToolUseBlock`. Anthropic `web_search_tool_result`, OpenAI
212
+ Responses `*_call_output`, Gemini `code_execution_result`.
213
+ `content` is provider-shaped (search-result rows, code stdout,
214
+ computer-use screenshots, ...)."""
215
+
216
+ model_config = _OPEN
217
+ type: Literal["server_tool_result"] = "server_tool_result"
218
+ tool_use_id: str
219
+ name: str
220
+ content: Any
221
+ is_error: bool | None = None
222
+
223
+
224
+ class RefusalBlock(BaseModel):
225
+ """Structured refusal distinct from generated text. OpenAI assistant
226
+ message `refusal` field and Responses `output_refusal` item. Other
227
+ providers emit refusals as plain text plus a finish reason; this
228
+ block represents only providers that ship a typed refusal."""
229
+
230
+ model_config = _OPEN
231
+ type: Literal["refusal"] = "refusal"
232
+ refusal: str
233
+
234
+
235
+ # ── Discriminated union ───────────────────────────────────────────────────────
236
+
237
+
238
+ AVPContentBlock = Annotated[
239
+ TextBlock
240
+ | ThinkingBlock
241
+ | ImageBlock
242
+ | AudioBlock
243
+ | VideoBlock
244
+ | DocumentBlock
245
+ | ToolUseBlock
246
+ | ToolResultBlock
247
+ | ServerToolUseBlock
248
+ | ServerToolResultBlock
249
+ | RefusalBlock,
250
+ Field(discriminator="type"),
251
+ ]
252
+
253
+
254
+ __all__ = [
255
+ "AVPContentBlock",
256
+ "AudioBlock",
257
+ "Base64Source",
258
+ "Citation",
259
+ "DocumentBlock",
260
+ "FileSource",
261
+ "ImageBlock",
262
+ "RefusalBlock",
263
+ "ServerToolResultBlock",
264
+ "ServerToolUseBlock",
265
+ "Source",
266
+ "TextBlock",
267
+ "ThinkingBlock",
268
+ "ToolResultBlock",
269
+ "ToolResultContent",
270
+ "ToolUseBlock",
271
+ "UrlSource",
272
+ "VideoBlock",
273
+ ]
avp/data/__init__.py ADDED
File without changes