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/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"]