Skip to content
Merged
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
32 changes: 27 additions & 5 deletions docs/experimental-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
> **Experimental.** Protocol v2 is a draft. Import it from `acp.experimental` and
> expect its API and generated models to change with the upstream schema.

The bindings use `schema-v2.0.0-alpha.5`.

The v2 runtime is separate from the stable v1 API. Its methods accept and return
generated request and response models directly. Install update handlers on the
client before opening a session because updates are independent connection
Expand All @@ -29,18 +31,38 @@ initialized = await connection.initialize(
session = await connection.new_session(
v2.schema.NewSessionRequest(cwd="/workspace")
)
await connection.prompt(
accepted = await connection.prompt(
v2.schema.PromptRequest(
session_id=session.session_id,
prompt=[v2.schema.TextContentBlock(text="Hello")],
)
)
```

`session/prompt` returns when the agent accepts the prompt. It does not define a
boundary for session updates: they may arrive before, during, or after that
request, and they do not carry a prompt identifier. Applications decide how to
buffer or present them.
`session/prompt` returns after the agent inserts the user message into the ACP
conversation, without waiting for processing to finish. The response requires a
non-null `message_id`. Agents return `v2.schema.PromptResponse(message_id=...)`
and echo the user message in a `UserMessageUpdate` or `UserMessageChunk` carrying
the same ID. That update may arrive before or after the response; use
`accepted.message_id` to match it. Other session updates are independent traffic
and do not carry a prompt identifier.

Agents can send `v2.schema.SessionNotice(severity="warning", title="Context is nearly full")`
in an `UpdateSessionNotification`. V2 notices require no client capability and
are live advisory events, outside retained session history. Clients may ignore
them. Titles must be non-empty, and severity also accepts custom or future strings.

For patch fields in session updates, omit a field to leave its current
value unchanged, or explicitly pass `None` to clear it. For example,
`v2.schema.SessionToolCallUpdate(tool_call_id="tool-1", name=None)` clears the
tool name, while omitting `name` leaves it unchanged. This also applies to
terminal updates and patch metadata. When applying received patches, use
`update.model_dump(by_alias=True, exclude_unset=True)` to retain that distinction.

Setting `replay_from=v2.schema.ReplayFromStartVariant()` on a `ResumeSessionRequest`
requests all retained conversation history; agents need not retain every message.
Accepted elicitation content validates scalar values and string lists; nested
objects are not valid form values.

Agents that serve both versions use `AgentProtocolRouter`:

Expand Down
2 changes: 1 addition & 1 deletion schema/v2/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
refs/tags/schema-v2.0.0-alpha.3
refs/tags/schema-v2.0.0-alpha.5
130 changes: 118 additions & 12 deletions schema/v2/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -874,7 +874,7 @@
]
},
"name": {
"description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nProgrammatic name of the tool being invoked.\n\nThis field is optional and has patch semantics. Omission means no\nchange, `null` clears the name, and a string replaces it. For a tool\ncall ID the client has not seen before, omission or `null` means that no\ntool name is available.",
"description": "Programmatic name of the tool being invoked.\n\nThis field is optional and has patch semantics. Omission means no\nchange, `null` clears the name, and a string replaces it. For a tool\ncall ID the client has not seen before, omission or `null` means that no\ntool name is available.",
"type": [
"string",
"null"
Expand Down Expand Up @@ -5812,9 +5812,17 @@
"x-method": "session/set_config_option"
},
"PromptResponse": {
"description": "Response acknowledging that a user prompt was accepted.\n\nThis response does not indicate that the agent has finished processing.\nProcessing and completion are reported through `state_update` session updates.\n\nSee protocol docs: [Prompt Accepted](https://agentclientprotocol.com/protocol/v2/draft/prompt-lifecycle#2-prompt-accepted)",
"description": "Response acknowledging that a user prompt was inserted into the ACP conversation.\n\nThis response does not indicate that the prompt was merely received or queued, nor that the\nagent has finished processing it.\nProcessing and completion are reported through `state_update` session updates.\n\nSee protocol docs: [Prompt Accepted](https://agentclientprotocol.com/protocol/v2/draft/prompt-lifecycle#2-prompt-accepted)",
"type": "object",
"properties": {
"messageId": {
"description": "Identifies the user message inserted into the ACP conversation.\n\nRequired and non-null. Omission and explicit `null` are both invalid.\n\nThe corresponding user-message session update carries this same identifier and may arrive\nbefore or after this response. Agents must echo the message during the live session, but are\nnot required to retain it. If retained and replayed, the message keeps this identifier.",
"allOf": [
{
"$ref": "#/$defs/MessageId"
}
]
},
"_meta": {
"description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)",
"type": [
Expand All @@ -5825,9 +5833,16 @@
"additionalProperties": true
}
},
"required": [
"messageId"
],
"x-side": "agent",
"x-method": "session/prompt"
},
"MessageId": {
"description": "Unique identifier for a message within a session.",
"type": "string"
},
"StartNesResponse": {
"description": "Response to `nes/start`.",
"type": "object",
Expand Down Expand Up @@ -6853,6 +6868,24 @@
}
]
},
{
"description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nAdvisory information for the user that is not part of session history.\n\nNo Client capability is required. Clients that do not understand or\npresent notices may ignore them.",
"type": "object",
"properties": {
"sessionUpdate": {
"type": "string",
"const": "notice"
}
},
"required": [
"sessionUpdate"
],
"allOf": [
{
"$ref": "#/$defs/Notice"
}
]
},
{
"description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA context compaction has been created or updated.",
"type": "object",
Expand Down Expand Up @@ -7108,6 +7141,18 @@
"sessionUpdate"
]
},
{
"type": "object",
"properties": {
"sessionUpdate": {
"type": "string",
"const": "notice"
}
},
"required": [
"sessionUpdate"
]
},
{
"type": "object",
"properties": {
Expand Down Expand Up @@ -7138,10 +7183,6 @@
}
]
},
"MessageId": {
"description": "Unique identifier for a message within a session.",
"type": "string"
},
"ContentChunk": {
"description": "A streamed item of message content.",
"type": "object",
Expand Down Expand Up @@ -8416,6 +8457,71 @@
"size"
]
},
"NoticeSeverity": {
"description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nSeverity hint for a session notice.",
"anyOf": [
{
"description": "Informational notice.",
"type": "string",
"const": "info"
},
{
"description": "Warning notice.",
"type": "string",
"const": "warning"
},
{
"description": "Error notice.",
"type": "string",
"const": "error"
},
{
"title": "other",
"description": "Custom or future notice severity.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Other unknown values are reserved for future ACP severities.",
"type": "string"
}
]
},
"Notice": {
"description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nFire-and-forget advisory information for the user.\n\nNotices are live events rather than session history. Agents must not rely on\na notice being received, displayed, or seen by the user.\nNo Client capability is required, and unsupported Clients may ignore notices.\n\nSee RFD: [Session Notices](https://agentclientprotocol.com/rfds/session-notices)",
"type": "object",
"properties": {
"severity": {
"description": "Presentation severity hint.",
"allOf": [
{
"$ref": "#/$defs/NoticeSeverity"
}
]
},
"title": {
"description": "Required non-empty plain-text title that can stand alone.",
"type": "string",
"minLength": 1
},
"description": {
"description": "Optional plain-text detail or guidance.\n\nOmitted and `null` are equivalent and mean no description was supplied.",
"type": [
"string",
"null"
],
"x-deserialize-default-on-error": true
},
"_meta": {
"description": "Metadata scoped to this notice.\n\nOmitted and `null` are equivalent and mean no metadata was supplied.",
"type": [
"object",
"null"
],
"x-deserialize-default-on-error": true,
"additionalProperties": true
}
},
"required": [
"severity",
"title"
]
},
"CompactionId": {
"description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nUnique identifier for a context compaction within a session.",
"type": "string"
Expand Down Expand Up @@ -8727,7 +8833,7 @@
},
{
"title": "ResumeSessionRequest",
"description": "Resumes an existing session.\n\nThe agent should resume the session context, allowing the conversation\nto continue. If `replayFrom` is set, the agent should replay\nconversation history before responding.",
"description": "Resumes an existing session.\n\nThe agent should resume the session context, allowing the conversation\nto continue. If `replayFrom` is set, the agent should replay\nretained conversation history before responding.",
"allOf": [
{
"$ref": "#/$defs/ResumeSessionRequest"
Expand All @@ -8754,7 +8860,7 @@
},
{
"title": "PromptRequest",
"description": "Processes a user prompt within a session.\n\nThis request accepts the prompt:\n- Receives user messages with optional context (files, images, etc.)\n- Returns once the prompt is accepted\n\nAfter acceptance, the Agent reports the accepted user message,\nprocessing state, output, tool calls, and completion through\n`session/update` notifications.\n\nSee protocol docs: [Prompt Lifecycle](https://agentclientprotocol.com/protocol/v2/draft/prompt-lifecycle)",
"description": "Processes a user prompt within a session.\n\nAcceptance means insertion into the ACP conversation:\n- Receives user messages with optional context (files, images, etc.)\n- Returns the inserted user message's ID without waiting for processing to finish\n\nThe Agent reports the user message with the same ID through `session/update`;\nthis notification may arrive before or after the response. Processing state,\noutput, tool calls, and completion are also reported through session updates.\n\nSee protocol docs: [Prompt Lifecycle](https://agentclientprotocol.com/protocol/v2/draft/prompt-lifecycle)",
"allOf": [
{
"$ref": "#/$defs/PromptRequest"
Expand Down Expand Up @@ -9695,7 +9801,7 @@
"x-method": "session/fork"
},
"ResumeSessionRequest": {
"description": "Request parameters for resuming an existing session.\n\nResumes an existing session and optionally replays prior conversation\nhistory according to `replayFrom`.",
"description": "Request parameters for resuming an existing session.\n\nResumes an existing session and optionally replays retained conversation\nhistory according to `replayFrom`.",
"type": "object",
"properties": {
"sessionId": {
Expand Down Expand Up @@ -9733,7 +9839,7 @@
"x-deserialize-skip-invalid-items": true
},
"replayFrom": {
"description": "Inclusive cursor describing where conversation replay should begin.\n\nOptional. Omitted or `null` both mean the Agent should resume without\nreplaying previous conversation history. Replay cursors are inclusive:\nreplay includes the position identified by the cursor. Supplying\n`{ \"type\": \"start\" }` means the Agent should replay the whole\nconversation before responding.",
"description": "Inclusive cursor describing where conversation replay should begin.\n\nOptional. Omitted or `null` both mean the Agent should resume without\nreplaying previous conversation history. Replay cursors are inclusive:\nreplay includes the position identified by the cursor. Supplying\n`{ \"type\": \"start\" }` means the Agent should replay all retained\nconversation history before responding.",
"anyOf": [
{
"$ref": "#/$defs/ReplayFrom"
Expand Down Expand Up @@ -9765,7 +9871,7 @@
"description": "Inclusive cursor describing where replayed session history should begin.\n\nReplay includes the position identified by the cursor.",
"anyOf": [
{
"description": "Replay the whole conversation from its first replayable entry.",
"description": "Replay all retained conversation history from its first replayable entry.",
"type": "object",
"properties": {
"type": {
Expand Down Expand Up @@ -9825,7 +9931,7 @@
]
},
"ReplayFromStart": {
"description": "Inclusive replay cursor requesting replay from the start of the conversation.",
"description": "Inclusive replay cursor requesting replay from the start of retained conversation history.",
"type": "object",
"properties": {
"_meta": {
Expand Down
3 changes: 2 additions & 1 deletion scripts/_schema_semantics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from dataclasses import dataclass
from dataclasses import dataclass, field
from importlib import import_module
from pathlib import Path
from typing import Protocol, cast
Expand All @@ -23,6 +23,7 @@ class SchemaSemantics:
base_class: str
model_name_map: dict[str, str]
compatibility_aliases: str = ""
field_type_overrides: dict[str, str] = field(default_factory=dict)


class _SemanticsModule(Protocol):
Expand Down
1 change: 1 addition & 0 deletions scripts/gen_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ def render_schema(semantics: SchemaSemantics) -> str:
infer_union_variant_names=True,
naming_strategy=NamingStrategy.PrimaryFirst,
model_name_map=semantics.model_name_map,
type_overrides=semantics.field_type_overrides,
strict_refs=True,
schema_version="2020-12",
schema_version_mode=VersionMode.Strict,
Expand Down
5 changes: 4 additions & 1 deletion scripts/gen_schema_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
"ConfigOptionUpdate",
"SessionInfoUpdate",
"UsageUpdate",
"SessionNotice",
"SessionCompactionUpdate",
"SessionCompactionSummaryChunk",
),
Expand Down Expand Up @@ -161,7 +162,7 @@
inline_model_ref("NesSuggestion", ("anyOf", 4), ("object", None)): "OtherNesSuggestion",
inline_model_ref("ElicitationPropertySchema", ("anyOf", 5), ("object", None)): ("ElicitationOtherPropertySchema"),
inline_model_ref("MultiSelectItems", ("anyOf", 1), ("object", None)): "OtherMultiSelectItems",
inline_model_ref("SessionUpdate", ("anyOf", 19), ("object", None)): "OtherSessionUpdate",
inline_model_ref("SessionUpdate", ("anyOf", 20), ("object", None)): "OtherSessionUpdate",
inline_model_ref("SessionUpdate", ("anyOf", 6), ("allOf", 0), ("allOf", None)): ("RunningSessionStateUpdateBase"),
inline_model_ref("SessionUpdate", ("anyOf", 6), ("allOf", 1), ("allOf", None)): ("IdleSessionStateUpdateBase"),
inline_model_ref("SessionUpdate", ("anyOf", 6), ("allOf", 2), ("allOf", None)): (
Expand Down Expand Up @@ -229,4 +230,6 @@
schema_out=ROOT / "src" / "acp" / "experimental" / "v2" / "schema.py",
base_class="acp.experimental.v2._schema_base.BaseModel",
model_name_map=MODEL_NAME_MAP,
# The generator loses additionalProperties types on nullable objects.
field_type_overrides={"ElicitationAcceptAction.content": "acp._schema_base.ElicitationContent"},
)
2 changes: 2 additions & 0 deletions src/acp/_schema_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

from ._deserialize import use_default_on_error

ElicitationContent = dict[str, str | int | float | bool | list[str]]


class BaseModel(pydantic.BaseModel):
"""Runtime behavior shared by generated ACP schema models."""
Expand Down
1 change: 0 additions & 1 deletion src/acp/experimental/v2/_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ def notification(method: str, handler: str, params_type: Any) -> NotificationSpe
"prompt",
schema.PromptRequest,
schema.PromptResponse,
empty_response=True,
),
request(AGENT_METHODS["mcp_message"], "mcp_message", schema.MessageMcpRequest, Any),
request(AGENT_METHODS["session_list"], "list_sessions", schema.ListSessionsRequest, schema.ListSessionsResponse),
Expand Down
1 change: 1 addition & 0 deletions src/acp/experimental/v2/_schema_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class BaseModel(_BaseModel):
"compaction_summary_chunk",
"compaction_update",
"config_option_update",
"notice",
"plan_removed",
"plan_update",
"session_info_update",
Expand Down
2 changes: 1 addition & 1 deletion src/acp/experimental/v2/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@


def _dump(model: BaseModel) -> dict[str, Any]:
return model.model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True)
return model.model_dump(mode="json", by_alias=True, exclude_unset=True)


class _AgentRouter:
Expand Down
2 changes: 1 addition & 1 deletion src/acp/experimental/v2/meta.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Generated from schema/v2/meta.json. Do not edit by hand.
# Schema ref: refs/tags/schema-v2.0.0-alpha.3
# Schema ref: refs/tags/schema-v2.0.0-alpha.5
AGENT_METHODS = {
"initialize": "initialize",
"auth_login": "auth/login",
Expand Down
Loading
Loading