From 2d255a34dedc8d37a56f380ae0aa3dfec1982f8f Mon Sep 17 00:00:00 2001 From: yyjeqhc <1772413353@qq.com> Date: Wed, 21 Oct 2026 15:48:07 +0800 Subject: [PATCH] refactor(communication): share request payloads across tool and Console routes --- .../src/tests/tool_call_contracts.rs | 2 +- .../webcodex-tool-contracts/src/tool_call.rs | 1 + .../src/tool_call/canonical_enum.rs | 260 ++-------------- .../src/tool_call/communication_inputs.rs | 273 +++++++++++++++++ .../src/tool_call/parsing.rs | 42 ++- .../src/tool_definition/discovery.rs | 95 ++---- .../src/tool_definition/sessions.rs | 124 +++----- .../src/tool_definition/skills.rs | 20 +- .../src/tool_audit.rs | 183 ++++-------- .../src/tool_audit/tests/computer_privacy.rs | 31 +- src/agent_continuation_tests.rs | 153 ++++++---- src/mcp_tests/agent_continuation_app.rs | 52 ++-- src/runtime_console_http/communication.rs | 279 +++--------------- src/runtime_console_http/goals.rs | 10 +- src/runtime_console_http/tests/goals.rs | 22 +- src/tool_runtime/communication.rs | 268 ++++++++--------- src/tool_runtime/dispatch/agents.rs | 148 +--------- src/tool_runtime/dispatch/routing.rs | 22 +- .../tests/agent_continuation_refs.rs | 56 +++- .../tests/agent_task_attempt_refs.rs | 12 +- src/tool_runtime/tests/agent_tasks.rs | 36 ++- src/tool_runtime/tests/agent_waits.rs | 54 ++-- src/tool_runtime/tests/control_sidecars.rs | 24 +- src/tool_runtime/tests/goal_workflow.rs | 88 +++--- src/tool_runtime/tests/goals.rs | 106 ++++--- src/tool_runtime/tests/workbench.rs | 46 +-- 26 files changed, 1067 insertions(+), 1340 deletions(-) create mode 100644 crates/webcodex-tool-contracts/src/tool_call/communication_inputs.rs diff --git a/crates/webcodex-tool-contracts/src/tests/tool_call_contracts.rs b/crates/webcodex-tool-contracts/src/tests/tool_call_contracts.rs index f089d6734..2eabddf92 100644 --- a/crates/webcodex-tool-contracts/src/tests/tool_call_contracts.rs +++ b/crates/webcodex-tool-contracts/src/tests/tool_call_contracts.rs @@ -1281,7 +1281,7 @@ fn continuation_endpoint_rotation_rejects_retired_name() { assert_eq!(canonical.tool_name(), "rotate_agent_continuation_endpoint"); assert!(matches!( canonical, - ToolCall::RotateAgentContinuationEndpoint { .. } + ToolCall::RotateAgentContinuationEndpoint(..) )); { diff --git a/crates/webcodex-tool-contracts/src/tool_call.rs b/crates/webcodex-tool-contracts/src/tool_call.rs index 48305163d..4d77f5efe 100644 --- a/crates/webcodex-tool-contracts/src/tool_call.rs +++ b/crates/webcodex-tool-contracts/src/tool_call.rs @@ -50,5 +50,6 @@ pub const TOOL_CALL_WRAPPER_FIELDS: &[&str] = &[TOOL_CALL_TOOL_FIELD, TOOL_CALL_ pub const AGENT_CONTINUATION_REF_PATTERN: &str = "^~ac[1-9][0-9]{0,18}$"; include!("tool_call/input_types.rs"); +include!("tool_call/communication_inputs.rs"); include!("tool_call/canonical_enum.rs"); include!("tool_call/parsing.rs"); diff --git a/crates/webcodex-tool-contracts/src/tool_call/canonical_enum.rs b/crates/webcodex-tool-contracts/src/tool_call/canonical_enum.rs index 35ad5b096..dcb6ece38 100644 --- a/crates/webcodex-tool-contracts/src/tool_call/canonical_enum.rs +++ b/crates/webcodex-tool-contracts/src/tool_call/canonical_enum.rs @@ -1556,7 +1556,8 @@ pub enum ToolCall { dependency_policy: Option, /// Rust-only Cargo dependency fetching (not an OS/network sandbox). #[serde(default)] - dependency_resolution_policy: Option, + dependency_resolution_policy: + Option, /// Total build execution budget, default 1800 seconds, clamped to 7 days. /// Host handoff timing never extends this budget or starts a second build. #[serde(default)] @@ -1594,7 +1595,8 @@ pub enum ToolCall { dependency_policy: Option, /// Cargo dependency fetching only (not an OS network sandbox). #[serde(default)] - dependency_resolution_policy: Option, + dependency_resolution_policy: + Option, /// Test-only selector and count postconditions. Rust uses a libtest substring, /// Go uses native -run regexp, and Python pytest uses -k. Node TAP supports /// count requirements but rejects test filters. Omission keeps unfiltered positive-test proof by default. @@ -1998,7 +2000,8 @@ pub enum ToolCall { summary: String, /// Exact keyed replay does not mutate; changed reuse conflicts. #[schemars(length(min = 1, max = 128))] - idempotency_key: String, }, + idempotency_key: String, + }, /// Open the readonly workbench. Empty arguments show a chooser; a Project is selected only /// when explicitly supplied. Session selection never creates a recorder or execution context. @@ -2463,95 +2466,16 @@ pub enum ToolCall { }, /// Create a durable Server-owned Agent identity and mutable self-description card. - CreateAgentIdentity { - /// Mutable non-unique Agent handle. It is self-description metadata, not canonical identity or - /// authority. - #[schemars(length(min = 1, max = 64))] - #[schemars(regex(pattern = "^[A-Za-z0-9._-]+$"))] - handle: String, - /// Mutable non-unique Agent display name. - #[schemars(length(min = 1, max = 128))] - display_name: String, - #[schemars(extend("default" = ""))] - /// Optional Agent Card description, bounded by 2048 UTF-8 bytes server-side. - #[schemars(length(max = 2048))] - #[serde(default)] - description: Option, - /// Bounded self-description labels. Labels never grant authority. - #[schemars(length(max = 16))] - #[schemars(inner(length(min = 1, max = 64)))] - #[serde(default)] - specialty_labels: Vec, - /// Caller-generated operation key. Exact replay under the same communication principal returns the - /// original durable resource; reuse with changed input is rejected. - #[schemars(length(min = 1, max = 128))] - idempotency_key: String, - }, + CreateAgentIdentity(CreateAgentIdentityInput), /// List Agent identities owned by the current communication principal. - ListAgentIdentities { - /// Optional exact canonical Agent id owned by the current communication principal. - #[schemars(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$"))] - #[serde(default)] - agent_id: Option, - #[schemars(extend("default" = 0))] - /// Zero-based bounded page offset. - #[schemars(range(min = 0))] - #[serde(default)] - offset: Option, - #[schemars(extend("default" = 50))] - /// Maximum records to return. Values above 100 are accepted and clamped to 100. - #[schemars(range(min = 1))] - #[serde(default)] - limit: Option, - }, + ListAgentIdentities(ListAgentIdentitiesInput), /// CAS-update mutable Agent Card metadata without changing canonical identity. - UpdateAgentIdentity { - /// Canonical durable Agent id to update. - #[schemars(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$"))] - agent_id: String, - /// Exact profile revision fence. A stale value is rejected without mutation. - #[schemars(range(min = 1))] - expected_profile_revision: i64, - #[schemars(length(min = 1, max = 64))] - #[schemars(regex(pattern = "^[A-Za-z0-9._-]+$"))] - #[serde(default)] - handle: Option, - /// Replacement display name. - #[schemars(length(min = 1, max = 128))] - #[serde(default)] - display_name: Option, - /// Replacement description, bounded by 2048 UTF-8 bytes server-side. - #[schemars(length(max = 2048))] - #[serde(default)] - description: Option, - /// Bounded self-description labels. Labels never grant authority. - #[schemars(length(max = 16))] - #[schemars(inner(length(min = 1, max = 64)))] - #[serde(default)] - specialty_labels: Option>, - }, + UpdateAgentIdentity(UpdateAgentIdentityInput), /// Rotate the server-local continuation Endpoint/controller generation for a durable Agent. - RotateAgentContinuationEndpoint { - /// Canonical durable Agent id owned by the current communication principal. - #[schemars(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$"))] - agent_id: String, - /// Server-local continuation adapter label, for example ChatGPT. This is recorded metadata only: it - /// does not initiate, configure, or authorize an external connection. - #[schemars(length(min = 1, max = 64))] - host: String, - /// Optional opaque host-local attachment label copied into server-local Endpoint metadata. It is - /// not durable Agent identity, remote-host authority, or a connection selector. - #[schemars(length(max = 128))] - #[serde(default)] - client_attachment_id: Option, - /// Caller-generated operation key. Exact replay under the same communication principal returns the - /// original durable resource; reuse with changed input is rejected. - #[schemars(length(min = 1, max = 128))] - idempotency_key: String, - }, + RotateAgentContinuationEndpoint(RotateAgentContinuationEndpointInput), /// Present one exact Agent/Endpoint continuation controller card. Never infers a target. /// Pass agent_continuation_ref, or the explicit tuple. Do not pass both. @@ -2679,173 +2603,25 @@ pub enum ToolCall { }, /// Detach an Endpoint while preserving the durable Agent. - DetachAgentEndpoint { - /// Canonical Agent Endpoint id attached by the current communication principal. - #[schemars(regex(pattern = "^wc_endpoint_[A-Za-z0-9_-]{16}$"))] - endpoint_id: String, - }, + DetachAgentEndpoint(DetachAgentEndpointInput), /// Create a durable Conversation with the current Human principal and Agents. - CreateConversation { - /// Optional mutable room title. - #[schemars(length(max = 200))] - #[serde(default)] - title: Option, - /// Owned Agent participants to add with the current Human principal. - #[schemars(length(min = 1, max = 16))] - #[schemars(inner(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$")))] - agent_ids: Vec, - /// Caller-generated operation key. Exact replay under the same communication principal returns the - /// original durable resource; reuse with changed input is rejected. - #[schemars(length(min = 1, max = 128))] - idempotency_key: String, - }, + CreateConversation(CreateConversationInput), /// List Conversations for a Human view or an explicitly attached Agent view. - ListConversations { - /// Optional Agent view. Must be paired with exact Endpoint fencing; omit all three fields for the - /// current Human principal. - #[schemars(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$"))] - #[serde(default)] - agent_id: Option, - /// Active Endpoint proving the optional Agent view. - #[schemars(regex(pattern = "^wc_endpoint_[A-Za-z0-9_-]{16}$"))] - #[serde(default)] - endpoint_id: Option, - /// Exact Server-assigned current Endpoint generation. Stale generations fail closed. - #[schemars(range(min = 1))] - #[serde(default)] - expected_controller_generation: Option, - #[schemars(extend("default" = 0))] - /// Zero-based bounded page offset. - #[schemars(range(min = 0))] - #[serde(default)] - offset: Option, - #[schemars(extend("default" = 50))] - /// Maximum records to return. Values above 100 are accepted and clamped to 100. - #[schemars(range(min = 1))] - #[serde(default)] - limit: Option, - }, + ListConversations(ListConversationsInput), /// Read an ordered append-only Conversation transcript page. - ReadConversation { - /// Canonical Conversation id. - #[schemars(regex(pattern = "^wc_conv_[A-Za-z0-9_-]{16}$"))] - conversation_id: String, - /// Optional Agent view. Must be paired with exact Endpoint fencing; omit all three fields for the - /// current Human principal. - #[schemars(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$"))] - #[serde(default)] - agent_id: Option, - /// Active Endpoint proving the optional Agent view. - #[schemars(regex(pattern = "^wc_endpoint_[A-Za-z0-9_-]{16}$"))] - #[serde(default)] - endpoint_id: Option, - /// Exact Server-assigned current Endpoint generation. Stale generations fail closed. - #[schemars(range(min = 1))] - #[serde(default)] - expected_controller_generation: Option, - #[schemars(extend("default" = 0))] - /// Return append-only transcript messages with seq greater than this cursor. - #[schemars(range(min = 0))] - #[serde(default)] - after_seq: Option, - #[schemars(extend("default" = 50))] - /// Maximum records to return. Values above 100 are accepted and clamped to 100. - #[schemars(range(min = 1))] - #[serde(default)] - limit: Option, - }, + ReadConversation(ReadConversationInput), /// Atomically append a Message and recipient-specific Agent deliveries. - PostConversationMessage { - /// Canonical Conversation id. - #[schemars(regex(pattern = "^wc_conv_[A-Za-z0-9_-]{16}$"))] - conversation_id: String, - /// Append-only message body, bounded by 4096 UTF-8 bytes server-side. - #[schemars(length(min = 1, max = 4096))] - body: String, - /// Agent author provenance. Omit for the current Human principal; Agent authors require - /// endpoint_id. - #[schemars(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$"))] - #[serde(default)] - author_agent_id: Option, - /// Active Endpoint proving an Agent-authored message. Omit for Human authors. - #[schemars(regex(pattern = "^wc_endpoint_[A-Za-z0-9_-]{16}$"))] - #[serde(default)] - endpoint_id: Option, - /// Exact Server-assigned current Endpoint generation. Stale generations fail closed. - #[schemars(range(min = 1))] - #[serde(default)] - expected_controller_generation: Option, - /// Optional explicit Agent Inbox recipients. Omit to deliver to every Agent participant except the - /// author; an explicit empty array posts to the transcript/room without Agent deliveries. - #[schemars(length(min = 0, max = 16))] - #[schemars(inner(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$")))] - #[serde(default)] - recipient_agent_ids: Option>, - /// Optional parent Message in the same Conversation. - #[schemars(regex(pattern = "^wc_cmsg_[A-Za-z0-9_-]{16}$"))] - #[serde(default)] - reply_to: Option, - /// Caller-generated operation key. Exact replay under the same communication principal returns the - /// original durable resource; reuse with changed input is rejected. - #[schemars(length(min = 1, max = 128))] - #[serde(default)] - idempotency_key: Option, - /// Exact durable Wake that already crossed a Host dispatch or explicit-activation fence and - /// provides stable resumed-turn reply replay identity. Use with reply_operation_index instead of - /// idempotency_key. - #[schemars(regex(pattern = "^wc_wake_[A-Za-z0-9_-]{16}$"))] - #[serde(default)] - wake_reply_id: Option, - /// Stable per-send index within one Wake. Reuse the same index only for an exact uncertain retry; - /// use a different index for each intentional additional Message. - #[schemars(range(min = 0, max = 31))] - #[serde(default)] - reply_operation_index: Option, - }, + PostConversationMessage(PostConversationMessageInput), /// List queued deliveries for an Agent proven by an active Endpoint. - ListAgentInbox { - /// Canonical recipient Agent id. - #[schemars(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$"))] - agent_id: String, - /// Active Endpoint proving access to this Agent Inbox. - #[schemars(regex(pattern = "^wc_endpoint_[A-Za-z0-9_-]{16}$"))] - endpoint_id: String, - /// Exact Server-assigned current Endpoint generation. Stale generations fail closed. - #[schemars(range(min = 1))] - expected_controller_generation: i64, - #[schemars(extend("default" = 0))] - /// Return queued deliveries with durable delivery_order greater than this cursor. - #[schemars(range(min = 0))] - #[serde(default)] - after_delivery_order: Option, - #[schemars(extend("default" = 50))] - /// Maximum records to return. Values above 100 are accepted and clamped to 100. - #[schemars(range(min = 1))] - #[serde(default)] - limit: Option, - }, + ListAgentInbox(ListAgentInboxInput), /// Mark exact recipient-specific Agent deliveries consumed. - ConsumeAgentDeliveries { - /// Canonical recipient Agent id. - #[schemars(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$"))] - agent_id: String, - /// Active Endpoint proving access to this Agent Inbox. - #[schemars(regex(pattern = "^wc_endpoint_[A-Za-z0-9_-]{16}$"))] - endpoint_id: String, - /// Exact Server-assigned current Endpoint generation. Stale generations fail closed. - #[schemars(range(min = 1))] - expected_controller_generation: i64, - /// Deliveries to mark consumed. Repeating already-consumed ids is a safe desired-state retry. - #[schemars(length(min = 1, max = 100))] - #[schemars(inner(regex(pattern = "^wc_delivery_[A-Za-z0-9_-]{16}$")))] - delivery_ids: Vec, - }, + ConsumeAgentDeliveries(ConsumeAgentDeliveriesInput), /// Verify one exact Agent/Endpoint activation and return bounded current /// Conversation, Inbox, Wake, Host-binding, and reply-replay context. @@ -4268,4 +4044,4 @@ pub enum ToolCall { #[serde(default = "default_true")] include_risk_summary: bool, }, -} \ No newline at end of file +} diff --git a/crates/webcodex-tool-contracts/src/tool_call/communication_inputs.rs b/crates/webcodex-tool-contracts/src/tool_call/communication_inputs.rs new file mode 100644 index 000000000..99fce2c91 --- /dev/null +++ b/crates/webcodex-tool-contracts/src/tool_call/communication_inputs.rs @@ -0,0 +1,273 @@ +// Shared communication payloads for tool calls and the Console. +// Authorization and transport context remain separate from these inputs. + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct CreateAgentIdentityInput { + /// Mutable non-unique Agent handle. It is self-description metadata, not canonical identity or + /// authority. + #[schemars(length(min = 1, max = 64))] + #[schemars(regex(pattern = "^[A-Za-z0-9._-]+$"))] + pub handle: String, + /// Mutable non-unique Agent display name. + #[schemars(length(min = 1, max = 128))] + pub display_name: String, + #[schemars(extend("default" = ""))] + /// Optional Agent Card description, bounded by 2048 UTF-8 bytes server-side. + #[schemars(length(max = 2048))] + #[serde(default)] + pub description: Option, + /// Bounded self-description labels. Labels never grant authority. + #[schemars(length(max = 16))] + #[schemars(inner(length(min = 1, max = 64)))] + #[serde(default)] + pub specialty_labels: Vec, + /// Caller-generated operation key. Exact replay under the same communication principal returns the + /// original durable resource; reuse with changed input is rejected. + #[schemars(length(min = 1, max = 128))] + pub idempotency_key: String, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ListAgentIdentitiesInput { + /// Optional exact canonical Agent id owned by the current communication principal. + #[schemars(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$"))] + #[serde(default)] + pub agent_id: Option, + #[schemars(extend("default" = 0))] + /// Zero-based bounded page offset. + #[schemars(range(min = 0))] + #[serde(default)] + pub offset: Option, + #[schemars(extend("default" = 50))] + /// Maximum records to return. Values above 100 are accepted and clamped to 100. + #[schemars(range(min = 1))] + #[serde(default)] + pub limit: Option, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct UpdateAgentIdentityInput { + /// Canonical durable Agent id to update. + #[schemars(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$"))] + pub agent_id: String, + /// Exact profile revision fence. A stale value is rejected without mutation. + #[schemars(range(min = 1))] + pub expected_profile_revision: i64, + #[schemars(length(min = 1, max = 64))] + #[schemars(regex(pattern = "^[A-Za-z0-9._-]+$"))] + #[serde(default)] + pub handle: Option, + /// Replacement display name. + #[schemars(length(min = 1, max = 128))] + #[serde(default)] + pub display_name: Option, + /// Replacement description, bounded by 2048 UTF-8 bytes server-side. + #[schemars(length(max = 2048))] + #[serde(default)] + pub description: Option, + /// Bounded self-description labels. Labels never grant authority. + #[schemars(length(max = 16))] + #[schemars(inner(length(min = 1, max = 64)))] + #[serde(default)] + pub specialty_labels: Option>, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct RotateAgentContinuationEndpointInput { + /// Canonical durable Agent id owned by the current communication principal. + #[schemars(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$"))] + pub agent_id: String, + /// Server-local continuation adapter label, for example ChatGPT. This is recorded metadata only: it + /// does not initiate, configure, or authorize an external connection. + #[schemars(length(min = 1, max = 64))] + pub host: String, + /// Optional opaque host-local attachment label copied into server-local Endpoint metadata. It is + /// not durable Agent identity, remote-host authority, or a connection selector. + #[schemars(length(max = 128))] + #[serde(default)] + pub client_attachment_id: Option, + /// Caller-generated operation key. Exact replay under the same communication principal returns the + /// original durable resource; reuse with changed input is rejected. + #[schemars(length(min = 1, max = 128))] + pub idempotency_key: String, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct DetachAgentEndpointInput { + /// Canonical Agent Endpoint id attached by the current communication principal. + #[schemars(regex(pattern = "^wc_endpoint_[A-Za-z0-9_-]{16}$"))] + pub endpoint_id: String, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct CreateConversationInput { + /// Optional mutable room title. + #[schemars(length(max = 200))] + #[serde(default)] + pub title: Option, + /// Owned Agent participants to add with the current Human principal. + #[schemars(length(min = 1, max = 16))] + #[schemars(inner(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$")))] + pub agent_ids: Vec, + /// Caller-generated operation key. Exact replay under the same communication principal returns the + /// original durable resource; reuse with changed input is rejected. + #[schemars(length(min = 1, max = 128))] + pub idempotency_key: String, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ListConversationsInput { + /// Optional Agent view. Must be paired with exact Endpoint fencing; omit all three fields for the + /// current Human principal. + #[schemars(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$"))] + #[serde(default)] + pub agent_id: Option, + /// Active Endpoint proving the optional Agent view. + #[schemars(regex(pattern = "^wc_endpoint_[A-Za-z0-9_-]{16}$"))] + #[serde(default)] + pub endpoint_id: Option, + /// Exact Server-assigned current Endpoint generation. Stale generations fail closed. + #[schemars(range(min = 1))] + #[serde(default)] + pub expected_controller_generation: Option, + #[schemars(extend("default" = 0))] + /// Zero-based bounded page offset. + #[schemars(range(min = 0))] + #[serde(default)] + pub offset: Option, + #[schemars(extend("default" = 50))] + /// Maximum records to return. Values above 100 are accepted and clamped to 100. + #[schemars(range(min = 1))] + #[serde(default)] + pub limit: Option, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ReadConversationInput { + /// Canonical Conversation id. + #[schemars(regex(pattern = "^wc_conv_[A-Za-z0-9_-]{16}$"))] + pub conversation_id: String, + /// Optional Agent view. Must be paired with exact Endpoint fencing; omit all three fields for the + /// current Human principal. + #[schemars(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$"))] + #[serde(default)] + pub agent_id: Option, + /// Active Endpoint proving the optional Agent view. + #[schemars(regex(pattern = "^wc_endpoint_[A-Za-z0-9_-]{16}$"))] + #[serde(default)] + pub endpoint_id: Option, + /// Exact Server-assigned current Endpoint generation. Stale generations fail closed. + #[schemars(range(min = 1))] + #[serde(default)] + pub expected_controller_generation: Option, + #[schemars(extend("default" = 0))] + /// Return append-only transcript messages with seq greater than this cursor. + #[schemars(range(min = 0))] + #[serde(default)] + pub after_seq: Option, + #[schemars(extend("default" = 50))] + /// Maximum records to return. Values above 100 are accepted and clamped to 100. + #[schemars(range(min = 1))] + #[serde(default)] + pub limit: Option, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct PostConversationMessageInput { + /// Canonical Conversation id. + #[schemars(regex(pattern = "^wc_conv_[A-Za-z0-9_-]{16}$"))] + pub conversation_id: String, + /// Append-only message body, bounded by 4096 UTF-8 bytes server-side. + #[schemars(length(min = 1, max = 4096))] + pub body: String, + /// Agent author provenance. Omit for the current Human principal; Agent authors require + /// endpoint_id. + #[schemars(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$"))] + #[serde(default)] + pub author_agent_id: Option, + /// Active Endpoint proving an Agent-authored message. Omit for Human authors. + #[schemars(regex(pattern = "^wc_endpoint_[A-Za-z0-9_-]{16}$"))] + #[serde(default)] + pub endpoint_id: Option, + /// Exact Server-assigned current Endpoint generation. Stale generations fail closed. + #[schemars(range(min = 1))] + #[serde(default)] + pub expected_controller_generation: Option, + /// Optional explicit Agent Inbox recipients. Omit to deliver to every Agent participant except the + /// author; an explicit empty array posts to the transcript/room without Agent deliveries. + #[schemars(length(min = 0, max = 16))] + #[schemars(inner(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$")))] + #[serde(default)] + pub recipient_agent_ids: Option>, + /// Optional parent Message in the same Conversation. + #[schemars(regex(pattern = "^wc_cmsg_[A-Za-z0-9_-]{16}$"))] + #[serde(default)] + pub reply_to: Option, + /// Caller-generated operation key. Exact replay under the same communication principal returns the + /// original durable resource; reuse with changed input is rejected. + #[schemars(length(min = 1, max = 128))] + #[serde(default)] + pub idempotency_key: Option, + /// Exact durable Wake that already crossed a Host dispatch or explicit-activation fence and + /// provides stable resumed-turn reply replay identity. Use with reply_operation_index instead of + /// idempotency_key. + #[schemars(regex(pattern = "^wc_wake_[A-Za-z0-9_-]{16}$"))] + #[serde(default)] + pub wake_reply_id: Option, + /// Stable per-send index within one Wake. Reuse the same index only for an exact uncertain retry; + /// use a different index for each intentional additional Message. + #[schemars(range(min = 0, max = 31))] + #[serde(default)] + pub reply_operation_index: Option, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ListAgentInboxInput { + /// Canonical recipient Agent id. + #[schemars(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$"))] + pub agent_id: String, + /// Active Endpoint proving access to this Agent Inbox. + #[schemars(regex(pattern = "^wc_endpoint_[A-Za-z0-9_-]{16}$"))] + pub endpoint_id: String, + /// Exact Server-assigned current Endpoint generation. Stale generations fail closed. + #[schemars(range(min = 1))] + pub expected_controller_generation: i64, + #[schemars(extend("default" = 0))] + /// Return queued deliveries with durable delivery_order greater than this cursor. + #[schemars(range(min = 0))] + #[serde(default)] + pub after_delivery_order: Option, + #[schemars(extend("default" = 50))] + /// Maximum records to return. Values above 100 are accepted and clamped to 100. + #[schemars(range(min = 1))] + #[serde(default)] + pub limit: Option, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ConsumeAgentDeliveriesInput { + /// Canonical recipient Agent id. + #[schemars(regex(pattern = "^wc_dagent_[A-Za-z0-9_-]{16}$"))] + pub agent_id: String, + /// Active Endpoint proving access to this Agent Inbox. + #[schemars(regex(pattern = "^wc_endpoint_[A-Za-z0-9_-]{16}$"))] + pub endpoint_id: String, + /// Exact Server-assigned current Endpoint generation. Stale generations fail closed. + #[schemars(range(min = 1))] + pub expected_controller_generation: i64, + /// Deliveries to mark consumed. Repeating already-consumed ids is a safe desired-state retry. + #[schemars(length(min = 1, max = 100))] + #[schemars(inner(regex(pattern = "^wc_delivery_[A-Za-z0-9_-]{16}$")))] + pub delivery_ids: Vec, +} diff --git a/crates/webcodex-tool-contracts/src/tool_call/parsing.rs b/crates/webcodex-tool-contracts/src/tool_call/parsing.rs index bc2efe45d..2ba9e0053 100644 --- a/crates/webcodex-tool-contracts/src/tool_call/parsing.rs +++ b/crates/webcodex-tool-contracts/src/tool_call/parsing.rs @@ -17,7 +17,10 @@ fn validate_coding_project_source_shape(tool_name: &str, arguments: &Value) -> R .and_then(Value::as_str) .is_some_and(|value| value.trim().is_empty()) { - return Err(coding_project_source_error(tool_name, &format!("{field} must not be empty"))); + return Err(coding_project_source_error( + tool_name, + &format!("{field} must not be empty"), + )); } } if let Some(path) = arguments.get("path").and_then(Value::as_str) { @@ -36,18 +39,27 @@ fn validate_coding_project_source_shape(tool_name: &str, arguments: &Value) -> R return if conflicts.is_empty() { Ok(()) } else { - Err(coding_project_source_error(tool_name, &format!("conflicting fields project and {}", conflicts.join(", ")))) + Err(coding_project_source_error( + tool_name, + &format!("conflicting fields project and {}", conflicts.join(", ")), + )) }; } if path { return if client_id { Ok(()) } else { - Err(coding_project_source_error(tool_name, "missing client_id required with path")) + Err(coding_project_source_error( + tool_name, + "missing client_id required with path", + )) }; } if client_id { - return Err(coding_project_source_error(tool_name, "missing path required with client_id")); + return Err(coding_project_source_error( + tool_name, + "missing path required with client_id", + )); } // A Session is an exact continuation selector, not a worktree source or // permission to infer a current Project. The runtime authorizes its binding. @@ -526,10 +538,10 @@ impl ToolCall { Self::ReconcileAgentTaskCodingRun { .. } => "reconcile_agent_task_coding_run", Self::HeartbeatAgentTaskAttempt { .. } => "heartbeat_agent_task_attempt", Self::CompleteAgentTaskAttempt { .. } => "complete_agent_task_attempt", - Self::CreateAgentIdentity { .. } => "create_agent_identity", - Self::ListAgentIdentities { .. } => "list_agent_identities", - Self::UpdateAgentIdentity { .. } => "update_agent_identity", - Self::RotateAgentContinuationEndpoint { .. } => "rotate_agent_continuation_endpoint", + Self::CreateAgentIdentity(_) => "create_agent_identity", + Self::ListAgentIdentities(_) => "list_agent_identities", + Self::UpdateAgentIdentity(_) => "update_agent_identity", + Self::RotateAgentContinuationEndpoint(_) => "rotate_agent_continuation_endpoint", Self::PresentAgentContinuation { .. } => "present_agent_continuation", Self::AgentContinuationBind { .. } => "bind_agent_continuation", @@ -539,13 +551,13 @@ impl ToolCall { Self::AgentContinuationWakePrepare { .. } => "prepare_agent_continuation_wake", Self::AgentContinuationWakeFinish { .. } => "finish_agent_continuation_wake", Self::AgentContinuationUnbind { .. } => "unbind_agent_continuation", - Self::DetachAgentEndpoint { .. } => "detach_agent_endpoint", - Self::CreateConversation { .. } => "create_conversation", - Self::ListConversations { .. } => "list_conversations", - Self::ReadConversation { .. } => "read_conversation", - Self::PostConversationMessage { .. } => "post_conversation_message", - Self::ListAgentInbox { .. } => "list_agent_inbox", - Self::ConsumeAgentDeliveries { .. } => "consume_agent_deliveries", + Self::DetachAgentEndpoint(_) => "detach_agent_endpoint", + Self::CreateConversation(_) => "create_conversation", + Self::ListConversations(_) => "list_conversations", + Self::ReadConversation(_) => "read_conversation", + Self::PostConversationMessage(_) => "post_conversation_message", + Self::ListAgentInbox(_) => "list_agent_inbox", + Self::ConsumeAgentDeliveries(_) => "consume_agent_deliveries", Self::BootstrapAgentConversation { .. } => "bootstrap_agent_conversation", Self::ConsumeAgentWake { .. } => "consume_agent_wake", Self::MemorySearch { .. } => "search_memory", diff --git a/crates/webcodex-tool-contracts/src/tool_definition/discovery.rs b/crates/webcodex-tool-contracts/src/tool_definition/discovery.rs index 2ce233ab3..2d83c1b49 100644 --- a/crates/webcodex-tool-contracts/src/tool_definition/discovery.rs +++ b/crates/webcodex-tool-contracts/src/tool_definition/discovery.rs @@ -1,35 +1,44 @@ use super::ToolVisibility::ModelVisible; use super::{ - adaptive_runtime_direct, def, model_spec, ToolDefinition, TOOL_CATEGORY_PROJECT, + adaptive_runtime_direct, control_def, model_spec, ToolDefinition, TOOL_CATEGORY_PROJECT, TOOL_CATEGORY_RUNTIME, }; use crate::metadata::{ - ToolPathHint::None as NoPath, ToolRisk::{ProjectWrite, Read}, - PROJECT_READ, PROJECT_WRITE, RUNTIME_READ, TOOL_PROVIDER_CONTROL, + PROJECT_READ, PROJECT_WRITE, RUNTIME_READ, }; pub(super) const DEFINITIONS: &[ToolDefinition] = &[ model_spec( - def("resolve_workspace", super::ToolAuditPolicy::TYPED_CANONICAL.session_input( + control_def( + "resolve_workspace", + super::ToolAuditPolicy::TYPED_CANONICAL.session_input( super::ToolAuditSessionInputPolicy::OmitTopLevel(&["path", "query", "client_id"])), - ModelVisible, TOOL_CATEGORY_PROJECT, None, TOOL_PROVIDER_CONTROL, + ModelVisible, + TOOL_CATEGORY_PROJECT, super::ToolSemanticContract { effect: super::ToolEffect::Observe, risk: Read, - approval: super::ToolApprovalPolicy::None, idempotency: super::ToolIdempotency::PureRead }, - Some(PROJECT_READ), false, NoPath, false, false, super::ToolSessionEvidencePolicy::NONE), + approval: super::ToolApprovalPolicy::None, idempotency: super::ToolIdempotency::PureRead }, + Some(PROJECT_READ), + false, + ), "Locate a registered workspace by exact Runner plus path or literal query; no Session or registration is created. Returns one resolved workspace or bounded ambiguous candidates, never the first fuzzy match. Git fields are cached Runner inventory, not current filesystem proof; incomplete inventory stays explicit. Starting or resuming work is a separate action. Gateway via call_runtime_tool.", ).with_discovery_keywords(&["locate", "resolve", "registered", "workspace", "path", "project", "定位", "已注册", "工作区", "路径", "项目"]), model_spec( - def("unregister_projects", super::ToolAuditPolicy::TYPED_CANONICAL, - ModelVisible, TOOL_CATEGORY_PROJECT, None, TOOL_PROVIDER_CONTROL, + control_def( + "unregister_projects", + super::ToolAuditPolicy::TYPED_CANONICAL, + ModelVisible, + TOOL_CATEGORY_PROJECT, super::ToolSemanticContract { effect: super::ToolEffect::Mutate, risk: ProjectWrite, - approval: super::ToolApprovalPolicy::Standard, idempotency: super::ToolIdempotency::NonIdempotent }, - Some(PROJECT_WRITE), false, NoPath, true, false, super::ToolSessionEvidencePolicy::NONE), + approval: super::ToolApprovalPolicy::Standard, idempotency: super::ToolIdempotency::NonIdempotent }, + Some(PROJECT_WRITE), + true, + ), "Preview or batch-remove (unregister) up to 16 explicit Project registrations with exact revisions; never delete files, directories, branches or Session history. Default dry_run=true inspects current registrations, not a lease. Execution requires dry_run=false and confirm=true; each item uses canonical owner/CAS/active-Job checks. Sequential, non-atomic, per-item outcomes; uncertainty stops remaining items. Never replay the batch blindly; reconcile exact failed or unknown items.", ).with_discovery_keywords(&["batch", "unregister", "remove", "registrations", "projects", "批量", "注销", "移除", "注册", "项目"]), model_spec( - def( + control_def( "list_projects", super::ToolAuditPolicy::TYPED_CANONICAL.session_input( super::ToolAuditSessionInputPolicy::OmitTopLevel(&[ @@ -40,8 +49,6 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ ), ModelVisible, TOOL_CATEGORY_PROJECT, - None, - TOOL_PROVIDER_CONTROL, super::ToolSemanticContract { effect: super::ToolEffect::Observe, risk: Read, @@ -50,10 +57,6 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ }, Some(PROJECT_READ), false, - NoPath, - false, - false, - super::ToolSessionEvidencePolicy::NONE, ) .with_activity( super::ToolActivityPresentation::Support, @@ -62,13 +65,11 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ "List registered Projects on an exact Runner; use resolve_workspace for unique path/name selection, or include_git_summary for cached maintenance facts. Results keep canonical Runtime Project ids and, when stable root identity is available, also issue a short principal-scoped project_ref for later model calls. project_ref is convenience only and grants no authority. When Runner/Project identity is known, pass exact client_id/project filters; use bounded query and summary_only instead of reading the full registry.", ), model_spec( - def( + control_def( "register_project", super::ToolAuditPolicy::TYPED_CANONICAL, ModelVisible, TOOL_CATEGORY_PROJECT, - None, - TOOL_PROVIDER_CONTROL, super::ToolSemanticContract { effect: super::ToolEffect::Mutate, risk: ProjectWrite, @@ -76,22 +77,16 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ idempotency: super::ToolIdempotency::NonIdempotent, }, Some(PROJECT_WRITE), - false, - NoPath, true, - false, - super::ToolSessionEvidencePolicy::NONE, ), "Register an existing directory, including a non-Git or ad-hoc workspace, as a Project on one Runner. Successful onboarding returns the canonical Runtime Project id and a Server-issued project_ref when a stable root identity is available. Use this when the directory already exists; policy still bounds allowed paths.", ).with_discovery_keywords(&["register", "existing", "project", "directory", "注册", "现有", "项目", "目录"]), model_spec( - def( + control_def( "unregister_project", super::ToolAuditPolicy::TYPED_CANONICAL, ModelVisible, TOOL_CATEGORY_PROJECT, - None, - TOOL_PROVIDER_CONTROL, super::ToolSemanticContract { effect: super::ToolEffect::Mutate, risk: ProjectWrite, @@ -99,22 +94,16 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ idempotency: super::ToolIdempotency::NonIdempotent, }, Some(PROJECT_WRITE), - false, - NoPath, true, - false, - super::ToolSessionEvidencePolicy::NONE, ), "Unregister one exact Runner project using the revision from list_projects. Removes registration only; never deletes source, worktree, or branch. Re-list after an indeterminate outcome.", ), model_spec( - def( + control_def( "create_project", super::ToolAuditPolicy::TYPED_CANONICAL, ModelVisible, TOOL_CATEGORY_PROJECT, - None, - TOOL_PROVIDER_CONTROL, super::ToolSemanticContract { effect: super::ToolEffect::Mutate, risk: ProjectWrite, @@ -122,24 +111,18 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ idempotency: super::ToolIdempotency::NonIdempotent, }, Some(PROJECT_WRITE), - false, - NoPath, true, - false, - super::ToolSessionEvidencePolicy::NONE, ), "Create a directory on one Runner and register it as a Project. Successful onboarding returns the canonical Runtime Project id and a Server-issued project_ref when a stable root identity is available. Use this for a new workspace; existing directories belong on the registration path.", ).with_discovery_keywords(&["create", "new", "project", "directory", "scaffold", "创建", "新建", "项目", "目录"]), model_spec( - def( + control_def( "list_runners", super::ToolAuditPolicy::TYPED_CANONICAL.session_input( super::ToolAuditSessionInputPolicy::OmitTopLevel(&["client_id", "client_ids"]), ), ModelVisible, TOOL_CATEGORY_RUNTIME, - None, - TOOL_PROVIDER_CONTROL, super::ToolSemanticContract { effect: super::ToolEffect::Observe, risk: Read, @@ -148,10 +131,6 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ }, Some(RUNTIME_READ), false, - NoPath, - false, - false, - super::ToolSessionEvidencePolicy::NONE, ) .with_activity( super::ToolActivityPresentation::Support, @@ -161,15 +140,13 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ ), adaptive_runtime_direct( model_spec( - def( + control_def( "get_runtime_status", super::ToolAuditPolicy::TYPED_CANONICAL.session_input( super::ToolAuditSessionInputPolicy::OmitTopLevel(&["client_id"]), ), ModelVisible, TOOL_CATEGORY_RUNTIME, - None, - TOOL_PROVIDER_CONTROL, super::ToolSemanticContract { effect: super::ToolEffect::Observe, risk: Read, @@ -178,10 +155,6 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ }, Some(RUNTIME_READ), false, - NoPath, - false, - false, - super::ToolSessionEvidencePolicy::NONE, ) .with_activity( super::ToolActivityPresentation::Support, @@ -196,13 +169,11 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ super::ToolDirectReason::CoreWorkflow, ), model_spec( - def( + control_def( "read_current_window_activity", super::ToolAuditPolicy::TYPED_CANONICAL, ModelVisible, TOOL_CATEGORY_RUNTIME, - None, - TOOL_PROVIDER_CONTROL, super::ToolSemanticContract { effect: super::ToolEffect::Observe, risk: Read, @@ -211,10 +182,6 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ }, Some(RUNTIME_READ), false, - NoPath, - false, - false, - super::ToolSessionEvidencePolicy::NONE, ) .with_activity( super::ToolActivityPresentation::Support, @@ -224,13 +191,11 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ ), adaptive_runtime_direct( model_spec( - def( + control_def( "read_tool_manifest", super::ToolAuditPolicy::TYPED_CANONICAL, ModelVisible, TOOL_CATEGORY_RUNTIME, - None, - TOOL_PROVIDER_CONTROL, super::ToolSemanticContract { effect: super::ToolEffect::Observe, risk: Read, @@ -239,10 +204,6 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ }, Some(RUNTIME_READ), false, - NoPath, - false, - false, - super::ToolSessionEvidencePolicy::NONE, ) .with_activity( super::ToolActivityPresentation::Support, diff --git a/crates/webcodex-tool-contracts/src/tool_definition/sessions.rs b/crates/webcodex-tool-contracts/src/tool_definition/sessions.rs index 51b12b556..0dd167e49 100644 --- a/crates/webcodex-tool-contracts/src/tool_definition/sessions.rs +++ b/crates/webcodex-tool-contracts/src/tool_definition/sessions.rs @@ -1,7 +1,7 @@ use super::RunnerCapabilityRequirement::{GitOrShell, InternalPosixScript, OwnerOnly}; use super::ToolVisibility::{ModelHidden, ModelVisible}; use super::{ - adaptive_runtime_direct, def, model_spec, permission_risk, require_all_scopes, + adaptive_runtime_direct, control_def, def, model_spec, permission_risk, require_all_scopes, requires_explicit_business_session, ToolDefinition, PERMISSION_RISK_WRITE, TOOL_CATEGORY_SESSION, TOOL_CATEGORY_VALIDATION, }; @@ -88,13 +88,11 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ "Read all retained external reports for an exact Session/Project (at most 256). Reports are untrusted adapter claims, separate from native Job and validation evidence. They never establish task completion or authorize replaying work. coverage remains incomplete until a durable source sequence can prove gaps/order.", )), - def( + control_def( "start_session", super::ToolAuditPolicy::TYPED_CANONICAL, ModelHidden, TOOL_CATEGORY_SESSION, - None, - TOOL_PROVIDER_CONTROL, super::ToolSemanticContract { effect: super::ToolEffect::Mutate, risk: super::ToolRisk::WorkflowManage, @@ -103,10 +101,6 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ }, Some(RUNTIME_READ), false, - NoPath, - false, - false, - super::ToolSessionEvidencePolicy::NONE, ), adaptive_runtime_direct( model_spec( @@ -322,13 +316,11 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ super::ToolActivityInteraction::NonMeaningful, ), requires_explicit_business_session(model_spec( - def( + control_def( "read_session_summary", super::ToolAuditPolicy::TYPED_CANONICAL, ModelVisible, TOOL_CATEGORY_SESSION, - None, - TOOL_PROVIDER_CONTROL, super::ToolSemanticContract { effect: super::ToolEffect::Observe, risk: Read, @@ -337,10 +329,6 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ }, Some(RUNTIME_READ), false, - NoPath, - false, - false, - super::ToolSessionEvidencePolicy::NONE, ), "Return a bounded structured summary from the session ledger for an explicit session_id: recorded events, message-board summary, task mode, guards, and lifecycle. Uses durable ledger data where session persistence is configured.", )), @@ -456,7 +444,7 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ )), requires_explicit_business_session(model_spec( - def( + control_def( "list_session_messages", super::ToolAuditPolicy::typed_fields(&[ super::ToolAuditResultField::value("success"), @@ -465,8 +453,6 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ ]), ModelVisible, TOOL_CATEGORY_SESSION, - None, - TOOL_PROVIDER_CONTROL, super::ToolSemanticContract { effect: super::ToolEffect::Observe, risk: Read, @@ -475,15 +461,11 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ }, Some(RUNTIME_READ), false, - NoPath, - false, - false, - super::ToolSessionEvidencePolicy::NONE, ), "Read bounded session-local messages with exact filters. For an executable todo, prefer get_session_assignment so the todo, all retained direct replies, and its completion fence come from one store snapshot.", )), requires_explicit_business_session(model_spec( - def( + control_def( "get_session_assignment", super::ToolAuditPolicy::typed_fields(&[ super::ToolAuditResultField::value("success"), @@ -495,8 +477,6 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ ]), ModelVisible, TOOL_CATEGORY_SESSION, - None, - TOOL_PROVIDER_CONTROL, super::ToolSemanticContract { effect: super::ToolEffect::Observe, risk: Read, @@ -505,15 +485,11 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ }, Some(RUNTIME_READ), false, - NoPath, - false, - false, - super::ToolSessionEvidencePolicy::NONE, ), "Atomically read one exact open todo plus all retained direct replies and return the opaque assignment_fence required by complete_session_message. Unrelated Session traffic and ACK bookkeeping do not stale it; incomplete retained assignment history fails closed.", )), requires_explicit_business_session(model_spec( - def( + control_def( "observe_session_messages", super::ToolAuditPolicy::typed_fields(&[ super::ToolAuditResultField::value("success"), @@ -526,8 +502,6 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ ]), ModelVisible, TOOL_CATEGORY_SESSION, - None, - TOOL_PROVIDER_CONTROL, super::ToolSemanticContract { effect: super::ToolEffect::Observe, risk: Read, @@ -536,10 +510,6 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ }, Some(RUNTIME_READ), false, - NoPath, - false, - false, - super::ToolSessionEvidencePolicy::NONE, ), "Observe Session message-state delta after an opaque durable cursor. No token establishes the current baseline and returns no history; token calls may wait once for change. Reports retention loss; never a subscription, delivery receipt, or model-context proof.", )), @@ -611,7 +581,7 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ )), adaptive_runtime_direct( requires_explicit_business_session(model_spec( - def( + control_def( "read_session_discussion_summary", super::ToolAuditPolicy::typed_fields(&[ super::ToolAuditResultField::value("success"), @@ -621,59 +591,47 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ super::ToolAuditResultField::array_len("recent_answer_count", "recent_answers"), super::ToolAuditResultField::array_len("recent_completion_count", "recent_completions"), ]), - ModelVisible, - TOOL_CATEGORY_SESSION, - None, - TOOL_PROVIDER_CONTROL, - super::ToolSemanticContract { - effect: super::ToolEffect::Observe, - risk: Read, - approval: super::ToolApprovalPolicy::None, - idempotency: super::ToolIdempotency::PureRead, - }, - Some(RUNTIME_READ), - false, - NoPath, - false, - false, - super::ToolSessionEvidencePolicy::NONE, - ), + ModelVisible, + TOOL_CATEGORY_SESSION, + super::ToolSemanticContract { + effect: super::ToolEffect::Observe, + risk: Read, + approval: super::ToolApprovalPolicy::None, + idempotency: super::ToolIdempotency::PureRead, + }, + Some(RUNTIME_READ), + false, + ), "Return a bounded structured aggregate of session-local discussion from the recorded session ledger. Does not call an LLM or generate natural-language summaries.", )), 15, super::ToolDirectReason::CoreWorkflow, ), requires_explicit_business_session(model_spec( - def( + control_def( "read_session_handoff", - super::ToolAuditPolicy::typed_fields(&[ - super::ToolAuditResultField::value("session_id"), - super::ToolAuditResultField::value("project"), - super::ToolAuditResultField::value("lifecycle"), - super::ToolAuditResultField::value("counts"), - super::ToolAuditResultField::array_len("open_todo_count", "open_todos"), - super::ToolAuditResultField::array_len("recent_answer_count", "recent_answers"), - super::ToolAuditResultField::array_len("recent_completion_count", "recent_completions"), - super::ToolAuditResultField::value("diagnostic"), - super::ToolAuditResultField::value("error_kind"), - ]), - ModelVisible, - TOOL_CATEGORY_SESSION, - None, - TOOL_PROVIDER_CONTROL, - super::ToolSemanticContract { - effect: super::ToolEffect::Observe, - risk: Read, - approval: super::ToolApprovalPolicy::None, - idempotency: super::ToolIdempotency::PureRead, - }, - Some(RUNTIME_READ), - false, - NoPath, - false, - false, - super::ToolSessionEvidencePolicy::NONE, - ) + super::ToolAuditPolicy::typed_fields(&[ + super::ToolAuditResultField::value("session_id"), + super::ToolAuditResultField::value("project"), + super::ToolAuditResultField::value("lifecycle"), + super::ToolAuditResultField::value("counts"), + super::ToolAuditResultField::array_len("open_todo_count", "open_todos"), + super::ToolAuditResultField::array_len("recent_answer_count", "recent_answers"), + super::ToolAuditResultField::array_len("recent_completion_count", "recent_completions"), + super::ToolAuditResultField::value("diagnostic"), + super::ToolAuditResultField::value("error_kind"), + ]), + ModelVisible, + TOOL_CATEGORY_SESSION, + super::ToolSemanticContract { + effect: super::ToolEffect::Observe, + risk: Read, + approval: super::ToolApprovalPolicy::None, + idempotency: super::ToolIdempotency::PureRead, + }, + Some(RUNTIME_READ), + false, + ) .with_activity( super::ToolActivityPresentation::Support, super::ToolActivityInteraction::Meaningful, diff --git a/crates/webcodex-tool-contracts/src/tool_definition/skills.rs b/crates/webcodex-tool-contracts/src/tool_definition/skills.rs index 509ce1b37..5e9c06f10 100644 --- a/crates/webcodex-tool-contracts/src/tool_definition/skills.rs +++ b/crates/webcodex-tool-contracts/src/tool_definition/skills.rs @@ -1,8 +1,8 @@ use super::RunnerCapabilityRequirement::{FileRead, SkillManagement}; use super::ToolVisibility::{ModelHidden, ModelVisible}; use super::{ - adaptive_runtime_direct, def, model_spec, require_all_scopes, ToolDefinition, - ToolOperatorExtensionFamily, TOOL_CATEGORY_SKILL, + adaptive_runtime_direct, def, model_spec, require_all_scopes, runner_project_read_def, + ToolDefinition, ToolOperatorExtensionFamily, TOOL_CATEGORY_SKILL, }; use crate::metadata::{ ToolPathHint::None as NoPath, @@ -16,7 +16,7 @@ use crate::metadata::{ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ adaptive_runtime_direct( model_spec( - def( + runner_project_read_def( "load_skill", super::ToolAuditPolicy::typed_fields(&[ super::ToolAuditResultField::value("project"), @@ -54,21 +54,9 @@ pub(super) const DEFINITIONS: &[ToolDefinition] = &[ super::ToolAuditResultField::value("error_kind"), super::ToolAuditResultField::value("state_changed"), ])), - ModelVisible, TOOL_CATEGORY_SKILL, - Some(FileRead), - TOOL_PROVIDER_RUNNER, - super::ToolSemanticContract { - effect: super::ToolEffect::Observe, - risk: Read, - approval: super::ToolApprovalPolicy::None, - idempotency: super::ToolIdempotency::PureRead, - }, - Some(PROJECT_READ), - true, + FileRead, NoPath, - false, - false, super::ToolSessionEvidencePolicy::NONE, ), "Load one uniquely named Skill by exact Unicode case folding. Returns descriptor, bounded SKILL.md, and revisions read-only. Reuse a complete Skill body and revisions retained for the same Project; a new task/Session alone does not require reloading. Refresh for changed revisions, missing/truncated body, context loss/uncertainty, or user request. Catalog metadata is not the Skill body. Cite project_path with source lines for Project Skills; skill_path is package-relative for further Skill reads. Runner Skills have no project_path; never invent one. Guidance grants no authority. Missing, ambiguous, or truncated discovery fails closed; Skill resources are never executed.", diff --git a/crates/webcodex-tool-runtime-contracts/src/tool_audit.rs b/crates/webcodex-tool-runtime-contracts/src/tool_audit.rs index 6eeee14e8..e234ba95e 100644 --- a/crates/webcodex-tool-runtime-contracts/src/tool_audit.rs +++ b/crates/webcodex-tool-runtime-contracts/src/tool_audit.rs @@ -1038,60 +1038,38 @@ impl ToolCallAuditProjection for ToolCall { "completion_key": completion_key, }), ), - Self::CreateAgentIdentity { - handle, - display_name, - description, - specialty_labels, - idempotency_key, - } => typed_communication_request_audit( + Self::CreateAgentIdentity(input) => typed_communication_request_audit( CommunicationRequestAudit::CreateIdentity, &serde_json::json!({ - "handle": handle, - "display_name": display_name, - "description": description, - "specialty_labels": specialty_labels, - "idempotency_key": idempotency_key, + "handle": input.handle, + "display_name": input.display_name, + "description": input.description, + "specialty_labels": input.specialty_labels, + "idempotency_key": input.idempotency_key, }), ), - Self::ListAgentIdentities { - agent_id, - offset, - limit, - } => typed_communication_request_audit( + Self::ListAgentIdentities(input) => typed_communication_request_audit( CommunicationRequestAudit::ListIdentities, - &serde_json::json!({"agent_id": agent_id, "offset": offset, "limit": limit}), + &serde_json::json!({"agent_id": input.agent_id, "offset": input.offset, "limit": input.limit}), ), - Self::UpdateAgentIdentity { - agent_id, - expected_profile_revision, - handle, - display_name, - description, - specialty_labels, - } => typed_communication_request_audit( + Self::UpdateAgentIdentity(input) => typed_communication_request_audit( CommunicationRequestAudit::UpdateIdentity, &serde_json::json!({ - "agent_id": agent_id, - "expected_profile_revision": expected_profile_revision, - "handle": handle, - "display_name": display_name, - "description": description, - "specialty_labels": specialty_labels, + "agent_id": input.agent_id, + "expected_profile_revision": input.expected_profile_revision, + "handle": input.handle, + "display_name": input.display_name, + "description": input.description, + "specialty_labels": input.specialty_labels, }), ), - Self::RotateAgentContinuationEndpoint { - agent_id, - host, - client_attachment_id, - idempotency_key, - } => typed_communication_request_audit( + Self::RotateAgentContinuationEndpoint(input) => typed_communication_request_audit( CommunicationRequestAudit::AttachEndpoint, &serde_json::json!({ - "agent_id": agent_id, - "host": host, - "client_attachment_id": client_attachment_id, - "idempotency_key": idempotency_key, + "agent_id": input.agent_id, + "host": input.host, + "client_attachment_id": input.client_attachment_id, + "idempotency_key": input.idempotency_key, }), ), @@ -1170,110 +1148,71 @@ impl ToolCallAuditProjection for ToolCall { "attempt_id": attempt_id, "outcome": outcome, }), - Self::DetachAgentEndpoint { endpoint_id } => typed_communication_request_audit( + Self::DetachAgentEndpoint(input) => typed_communication_request_audit( CommunicationRequestAudit::DetachEndpoint, - &serde_json::json!({"endpoint_id": endpoint_id}), + &serde_json::json!({"endpoint_id": input.endpoint_id}), ), - Self::CreateConversation { - title, - agent_ids, - idempotency_key, - } => typed_communication_request_audit( + Self::CreateConversation(input) => typed_communication_request_audit( CommunicationRequestAudit::CreateConversation, &serde_json::json!({ - "title": title, - "agent_ids": agent_ids, - "idempotency_key": idempotency_key, + "title": input.title, + "agent_ids": input.agent_ids, + "idempotency_key": input.idempotency_key, }), ), - Self::ListConversations { - agent_id, - endpoint_id, - expected_controller_generation, - offset, - limit, - } => typed_communication_request_audit( + Self::ListConversations(input) => typed_communication_request_audit( CommunicationRequestAudit::ListConversations, &serde_json::json!({ - "agent_id": agent_id, - "endpoint_id": endpoint_id, - "expected_controller_generation": expected_controller_generation, - "offset": offset, - "limit": limit, + "agent_id": input.agent_id, + "endpoint_id": input.endpoint_id, + "expected_controller_generation": input.expected_controller_generation, + "offset": input.offset, + "limit": input.limit, }), ), - Self::ReadConversation { - conversation_id, - agent_id, - endpoint_id, - expected_controller_generation, - after_seq, - limit, - } => typed_communication_request_audit( + Self::ReadConversation(input) => typed_communication_request_audit( CommunicationRequestAudit::ReadConversation, &serde_json::json!({ - "conversation_id": conversation_id, - "agent_id": agent_id, - "endpoint_id": endpoint_id, - "expected_controller_generation": expected_controller_generation, - "after_seq": after_seq, - "limit": limit, + "conversation_id": input.conversation_id, + "agent_id": input.agent_id, + "endpoint_id": input.endpoint_id, + "expected_controller_generation": input.expected_controller_generation, + "after_seq": input.after_seq, + "limit": input.limit, }), ), - Self::PostConversationMessage { - conversation_id, - body, - author_agent_id, - endpoint_id, - expected_controller_generation, - recipient_agent_ids, - reply_to, - idempotency_key, - wake_reply_id, - reply_operation_index, - } => typed_communication_request_audit( + Self::PostConversationMessage(input) => typed_communication_request_audit( CommunicationRequestAudit::PostMessage, &serde_json::json!({ - "conversation_id": conversation_id, - "body": body, - "author_agent_id": author_agent_id, - "endpoint_id": endpoint_id, - "expected_controller_generation": expected_controller_generation, - "recipient_agent_ids": recipient_agent_ids, - "reply_to": reply_to, - "idempotency_key": idempotency_key, - "wake_reply_id": wake_reply_id, - "reply_operation_index": reply_operation_index, + "conversation_id": input.conversation_id, + "body": input.body, + "author_agent_id": input.author_agent_id, + "endpoint_id": input.endpoint_id, + "expected_controller_generation": input.expected_controller_generation, + "recipient_agent_ids": input.recipient_agent_ids, + "reply_to": input.reply_to, + "idempotency_key": input.idempotency_key, + "wake_reply_id": input.wake_reply_id, + "reply_operation_index": input.reply_operation_index, }), ), - Self::ListAgentInbox { - agent_id, - endpoint_id, - expected_controller_generation, - after_delivery_order, - limit, - } => typed_communication_request_audit( + Self::ListAgentInbox(input) => typed_communication_request_audit( CommunicationRequestAudit::ListInbox, &serde_json::json!({ - "agent_id": agent_id, - "endpoint_id": endpoint_id, - "expected_controller_generation": expected_controller_generation, - "after_delivery_order": after_delivery_order, - "limit": limit, + "agent_id": input.agent_id, + "endpoint_id": input.endpoint_id, + "expected_controller_generation": input.expected_controller_generation, + "after_delivery_order": input.after_delivery_order, + "limit": input.limit, }), ), - Self::ConsumeAgentDeliveries { - agent_id, - endpoint_id, - expected_controller_generation, - delivery_ids, - } => typed_communication_request_audit( + Self::ConsumeAgentDeliveries(input) => typed_communication_request_audit( CommunicationRequestAudit::ConsumeDeliveries, &serde_json::json!({ - "agent_id": agent_id, - "endpoint_id": endpoint_id, - "expected_controller_generation": expected_controller_generation, - "delivery_ids": delivery_ids, + "agent_id": input.agent_id, + "endpoint_id": input.endpoint_id, + "expected_controller_generation": input.expected_controller_generation, + "delivery_ids": input.delivery_ids, }), ), Self::BootstrapAgentConversation { diff --git a/crates/webcodex-tool-runtime-contracts/src/tool_audit/tests/computer_privacy.rs b/crates/webcodex-tool-runtime-contracts/src/tool_audit/tests/computer_privacy.rs index e4b423b05..aba4210d6 100644 --- a/crates/webcodex-tool-runtime-contracts/src/tool_audit/tests/computer_privacy.rs +++ b/crates/webcodex-tool-runtime-contracts/src/tool_audit/tests/computer_privacy.rs @@ -510,13 +510,13 @@ fn communication_audit_omits_profile_and_message_bodies() { const PRIVATE_BODY: &str = "PRIVATE CONVERSATION BODY"; const PRIVATE_KEY: &str = "PRIVATE_IDEMPOTENCY_KEY"; - let create = ToolCall::CreateAgentIdentity { + let create = ToolCall::CreateAgentIdentity(webcodex_tool_contracts::CreateAgentIdentityInput { handle: PRIVATE_HANDLE.to_string(), display_name: PRIVATE_DISPLAY.to_string(), description: Some(PRIVATE_DESCRIPTION.to_string()), specialty_labels: vec![PRIVATE_LABEL.to_string()], idempotency_key: PRIVATE_KEY.to_string(), - } + }) .session_log_arguments(); let create_text = create.to_string(); for private in [ @@ -535,19 +535,20 @@ fn communication_audit_omits_profile_and_message_bodies() { assert_eq!(create["specialty_label_count"], 1); assert_eq!(create["idempotency_key_present"], true); - let post = ToolCall::PostConversationMessage { - conversation_id: "wc_conv_iavN7wEjRWeJq83v".to_string(), - body: PRIVATE_BODY.to_string(), - author_agent_id: None, - endpoint_id: None, - expected_controller_generation: None, - recipient_agent_ids: Some(vec!["wc_dagent_iavN7wEjRWeJq83v".to_string()]), - reply_to: None, - idempotency_key: Some(PRIVATE_KEY.to_string()), - wake_reply_id: None, - reply_operation_index: None, - } - .session_log_arguments(); + let post = + ToolCall::PostConversationMessage(webcodex_tool_contracts::PostConversationMessageInput { + conversation_id: "wc_conv_iavN7wEjRWeJq83v".to_string(), + body: PRIVATE_BODY.to_string(), + author_agent_id: None, + endpoint_id: None, + expected_controller_generation: None, + recipient_agent_ids: Some(vec!["wc_dagent_iavN7wEjRWeJq83v".to_string()]), + reply_to: None, + idempotency_key: Some(PRIVATE_KEY.to_string()), + wake_reply_id: None, + reply_operation_index: None, + }) + .session_log_arguments(); let post_text = post.to_string(); assert!(!post_text.contains(PRIVATE_BODY)); assert!(!post_text.contains(PRIVATE_KEY)); diff --git a/src/agent_continuation_tests.rs b/src/agent_continuation_tests.rs index 048bbf185..b26cb1a39 100644 --- a/src/agent_continuation_tests.rs +++ b/src/agent_continuation_tests.rs @@ -146,11 +146,13 @@ fn create_agent( ) -> String { let result = runtime.create_agent_identity( None, - handle.to_string(), - display_name.to_string(), - Some(description.to_string()), - vec![label.to_string()], - key.to_string(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: handle.to_string(), + display_name: display_name.to_string(), + description: Some(description.to_string()), + specialty_labels: vec![label.to_string()], + idempotency_key: key.to_string(), + }, ); assert!(result.success, "{:?}", result.output); result.output["agent"]["agent_id"] @@ -162,10 +164,12 @@ fn create_agent( fn attach(runtime: &ToolRuntime, agent_id: &str, key: &str) -> (String, i64) { let result = runtime.attach_agent_endpoint( None, - agent_id.to_string(), - "Deterministic Host".to_string(), - Some(format!("attachment-{key}")), - key.to_string(), + webcodex_tool_contracts::RotateAgentContinuationEndpointInput { + agent_id: agent_id.to_string(), + host: "Deterministic Host".to_string(), + client_attachment_id: Some(format!("attachment-{key}")), + idempotency_key: key.to_string(), + }, ); assert!(result.success, "{:?}", result.output); assert_eq!(result.output["endpoint"]["wake_capable"], false); @@ -183,9 +187,11 @@ fn attach(runtime: &ToolRuntime, agent_id: &str, key: &str) -> (String, i64) { fn create_conversation(runtime: &ToolRuntime, agent_a: &str, agent_b: &str, key: &str) -> String { let result = runtime.create_conversation( None, - Some("Natural durable conversation".to_string()), - vec![agent_a.to_string(), agent_b.to_string()], - key.to_string(), + webcodex_tool_contracts::CreateConversationInput { + title: Some("Natural durable conversation".to_string()), + agent_ids: vec![agent_a.to_string(), agent_b.to_string()], + idempotency_key: key.to_string(), + }, ); assert!(result.success, "{:?}", result.output); result.output["conversation"]["conversation"]["conversation_id"] @@ -209,16 +215,18 @@ fn post_as_agent( ) -> Value { let result = runtime.post_conversation_message( None, - conversation_id.to_string(), - body.to_string(), - Some(author_agent_id.to_string()), - Some(endpoint_id.to_string()), - Some(controller_generation), - Some(vec![recipient_agent_id.to_string()]), - None, - idempotency_key.map(ToOwned::to_owned), - wake_reply_id.map(ToOwned::to_owned), - reply_operation_index, + webcodex_tool_contracts::PostConversationMessageInput { + conversation_id: conversation_id.to_string(), + body: body.to_string(), + author_agent_id: Some(author_agent_id.to_string()), + endpoint_id: Some(endpoint_id.to_string()), + expected_controller_generation: Some(controller_generation), + recipient_agent_ids: Some(vec![recipient_agent_id.to_string()]), + reply_to: None, + idempotency_key: idempotency_key.map(ToOwned::to_owned), + wake_reply_id: wake_reply_id.map(ToOwned::to_owned), + reply_operation_index: reply_operation_index, + }, ); assert!(result.success, "{:?}", result.output); result.output @@ -300,7 +308,14 @@ fn bind_mcp_app( } fn listed_agent(runtime: &ToolRuntime, agent_id: &str) -> Value { - let result = runtime.list_agent_identities(None, Some(agent_id.to_string()), None, None); + let result = runtime.list_agent_identities( + None, + webcodex_tool_contracts::ListAgentIdentitiesInput { + agent_id: Some(agent_id.to_string()), + offset: None, + limit: None, + }, + ); assert!(result.success, "{:?}", result.output); let agents = result.output["agents"].as_array().unwrap(); assert_eq!(agents.len(), 1); @@ -713,7 +728,12 @@ fn agent_listing_projects_only_current_production_continuation_readiness() { listed_agent(&runtime, &detached)["production_auto_resume_available"], true ); - let detached_result = runtime.detach_agent_endpoint(None, detached_endpoint.clone()); + let detached_result = runtime.detach_agent_endpoint( + None, + webcodex_tool_contracts::DetachAgentEndpointInput { + endpoint_id: detached_endpoint.clone(), + }, + ); assert!(detached_result.success, "{:?}", detached_result.output); let detached_listing = listed_agent(&runtime, &detached); assert_eq!(detached_listing["active_endpoint_count"], 0); @@ -998,10 +1018,12 @@ fn offline_restart_and_replacement_dispatch_the_same_logical_wake() { ); let replayed_old = runtime.attach_agent_endpoint( None, - agent_b.clone(), - "Deterministic Host".to_string(), - Some("attachment-restart-endpoint-b".to_string()), - "restart-endpoint-b".to_string(), + webcodex_tool_contracts::RotateAgentContinuationEndpointInput { + agent_id: agent_b.clone(), + host: "Deterministic Host".to_string(), + client_attachment_id: Some("attachment-restart-endpoint-b".to_string()), + idempotency_key: "restart-endpoint-b".to_string(), + }, ); assert!(replayed_old.success, "{:?}", replayed_old.output); assert_eq!(replayed_old.output["replayed"], true); @@ -1964,9 +1986,12 @@ fn mcp_app_restart_recovery_never_bypasses_endpoint_lifecycle() { &detached.receiver_endpoint, detached.receiver_generation, ); - let detached_result = detached - .runtime - .detach_agent_endpoint(None, detached.receiver_endpoint.clone()); + let detached_result = detached.runtime.detach_agent_endpoint( + None, + webcodex_tool_contracts::DetachAgentEndpointInput { + endpoint_id: detached.receiver_endpoint.clone(), + }, + ); assert!(detached_result.success, "{:?}", detached_result.output); assert!(endpoint_recovery_fingerprint(&detached.db, &detached.receiver_endpoint).is_none()); let detached_state = detached.runtime.agent_continuation_state( @@ -2173,16 +2198,18 @@ fn wake_derived_reply_identity_closes_response_loss_without_merging_consumption( assert!(pending_bootstrap.output["reply_replay"].is_null()); let pending_reply = runtime.post_conversation_message( None, - conversation_id.clone(), - "must activate before using Wake reply identity".to_string(), - Some(agent_b.clone()), - Some(endpoint_b.clone()), - Some(generation_b), - Some(vec![agent_a.clone()]), - None, - None, - Some(wake_id.clone()), - Some(0), + webcodex_tool_contracts::PostConversationMessageInput { + conversation_id: conversation_id.clone(), + body: "must activate before using Wake reply identity".to_string(), + author_agent_id: Some(agent_b.clone()), + endpoint_id: Some(endpoint_b.clone()), + expected_controller_generation: Some(generation_b), + recipient_agent_ids: Some(vec![agent_a.clone()]), + reply_to: None, + idempotency_key: None, + wake_reply_id: Some(wake_id.clone()), + reply_operation_index: Some(0), + }, ); assert!(!pending_reply.success); assert_eq!(pending_reply.output["error_kind"], "wake_not_dispatched"); @@ -2248,16 +2275,18 @@ fn wake_derived_reply_identity_closes_response_loss_without_merging_consumption( let changed = runtime.post_conversation_message( None, - conversation_id.clone(), - "changed replay must conflict".to_string(), - Some(agent_b.clone()), - Some(replacement_endpoint_b.clone()), - Some(replacement_generation_b), - Some(vec![agent_a.clone()]), - None, - None, - Some(wake_id.clone()), - Some(0), + webcodex_tool_contracts::PostConversationMessageInput { + conversation_id: conversation_id.clone(), + body: "changed replay must conflict".to_string(), + author_agent_id: Some(agent_b.clone()), + endpoint_id: Some(replacement_endpoint_b.clone()), + expected_controller_generation: Some(replacement_generation_b), + recipient_agent_ids: Some(vec![agent_a.clone()]), + reply_to: None, + idempotency_key: None, + wake_reply_id: Some(wake_id.clone()), + reply_operation_index: Some(0), + }, ); assert!(!changed.success); assert_eq!( @@ -2278,10 +2307,12 @@ fn wake_derived_reply_identity_closes_response_loss_without_merging_consumption( .unwrap(); let consumed = runtime.consume_agent_deliveries( None, - agent_b.clone(), - replacement_endpoint_b, - replacement_generation_b, - vec![delivery_id], + webcodex_tool_contracts::ConsumeAgentDeliveriesInput { + agent_id: agent_b.clone(), + endpoint_id: replacement_endpoint_b, + expected_controller_generation: replacement_generation_b, + delivery_ids: vec![delivery_id], + }, ); assert!(consumed.success); assert_eq!( @@ -3234,11 +3265,13 @@ fn explicit_activation_bootstrap_is_replayable_and_consumes_wake_separately() { assert!(consumed.success, "{:?}", consumed.output); let inbox = runtime.list_agent_inbox( None, - agent_b.clone(), - endpoint_b, - generation_b, - Some(0), - Some(10), + webcodex_tool_contracts::ListAgentInboxInput { + agent_id: agent_b.clone(), + endpoint_id: endpoint_b, + expected_controller_generation: generation_b, + after_delivery_order: Some(0), + limit: Some(10), + }, ); assert!(inbox.success, "{:?}", inbox.output); assert_eq!( diff --git a/src/mcp_tests/agent_continuation_app.rs b/src/mcp_tests/agent_continuation_app.rs index 4a06aa65d..a596f7bcf 100644 --- a/src/mcp_tests/agent_continuation_app.rs +++ b/src/mcp_tests/agent_continuation_app.rs @@ -146,11 +146,13 @@ fn create_agent( ) -> String { let result = runtime.create_agent_identity( Some(auth), - handle.to_string(), - display_name.to_string(), - Some("PRIVATE Agent description".to_string()), - vec!["PRIVATE-specialty-label".to_string()], - key.to_string(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: handle.to_string(), + display_name: display_name.to_string(), + description: Some("PRIVATE Agent description".to_string()), + specialty_labels: vec!["PRIVATE-specialty-label".to_string()], + idempotency_key: key.to_string(), + }, ); assert!(result.success, "{:?}", result.output); result.output["agent"]["agent_id"] @@ -167,10 +169,12 @@ fn attach( ) -> (String, i64) { let result = runtime.attach_agent_endpoint( Some(auth), - agent_id.to_string(), - "ChatGPT".to_string(), - Some(format!("attachment-{key}")), - key.to_string(), + webcodex_tool_contracts::RotateAgentContinuationEndpointInput { + agent_id: agent_id.to_string(), + host: "ChatGPT".to_string(), + client_attachment_id: Some(format!("attachment-{key}")), + idempotency_key: key.to_string(), + }, ); assert!(result.success, "{:?}", result.output); ( @@ -193,9 +197,11 @@ fn create_conversation( ) -> String { let result = runtime.create_conversation( Some(auth), - Some("PRIVATE conversation title".to_string()), - vec![a.to_string(), b.to_string()], - key.to_string(), + webcodex_tool_contracts::CreateConversationInput { + title: Some("PRIVATE conversation title".to_string()), + agent_ids: vec![a.to_string(), b.to_string()], + idempotency_key: key.to_string(), + }, ); assert!(result.success, "{:?}", result.output); result.output["conversation"]["conversation"]["conversation_id"] @@ -218,16 +224,18 @@ fn post_message( ) { let result = runtime.post_conversation_message( Some(auth), - conversation_id.to_string(), - body.to_string(), - Some(sender.to_string()), - Some(sender_endpoint.to_string()), - Some(sender_generation), - Some(vec![receiver.to_string()]), - None, - Some(key.to_string()), - None, - None, + webcodex_tool_contracts::PostConversationMessageInput { + conversation_id: conversation_id.to_string(), + body: body.to_string(), + author_agent_id: Some(sender.to_string()), + endpoint_id: Some(sender_endpoint.to_string()), + expected_controller_generation: Some(sender_generation), + recipient_agent_ids: Some(vec![receiver.to_string()]), + reply_to: None, + idempotency_key: Some(key.to_string()), + wake_reply_id: None, + reply_operation_index: None, + }, ); assert!(result.success, "{:?}", result.output); } diff --git a/src/runtime_console_http/communication.rs b/src/runtime_console_http/communication.rs index 62b123f8e..24fac37d5 100644 --- a/src/runtime_console_http/communication.rs +++ b/src/runtime_console_http/communication.rs @@ -3,6 +3,12 @@ use serde::Deserialize; use salvo::writing::Scribe; use serde_json::Value; +use webcodex_tool_contracts::tool_call::{ + ConsumeAgentDeliveriesInput, CreateAgentIdentityInput, CreateConversationInput, + DetachAgentEndpointInput, ListAgentIdentitiesInput, ListAgentInboxInput, + ListConversationsInput, PostConversationMessageInput, ReadConversationInput, + RotateAgentContinuationEndpointInput, UpdateAgentIdentityInput, +}; use super::{ parse_console_json, prepared, require_communication_manage, require_communication_read, @@ -44,60 +50,6 @@ impl Scribe for CommunicationResponse { type CommunicationResult = Result; -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CommunicationAgentsInput { - #[serde(default)] - agent_id: Option, - #[serde(default)] - offset: Option, - #[serde(default)] - limit: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CommunicationAgentCreateInput { - handle: String, - display_name: String, - #[serde(default)] - description: Option, - #[serde(default)] - specialty_labels: Vec, - idempotency_key: String, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CommunicationAgentUpdateInput { - agent_id: String, - expected_profile_revision: i64, - #[serde(default)] - handle: Option, - #[serde(default)] - display_name: Option, - #[serde(default)] - description: Option, - #[serde(default)] - specialty_labels: Option>, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CommunicationEndpointAttachInput { - agent_id: String, - host: String, - #[serde(default)] - client_attachment_id: Option, - idempotency_key: String, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CommunicationEndpointDetachInput { - endpoint_id: String, -} - #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] struct CommunicationEndpointRenewInput { @@ -105,90 +57,6 @@ struct CommunicationEndpointRenewInput { expected_controller_generation: i64, } -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CommunicationConversationsInput { - #[serde(default)] - agent_id: Option, - #[serde(default)] - endpoint_id: Option, - #[serde(default)] - expected_controller_generation: Option, - #[serde(default)] - offset: Option, - #[serde(default)] - limit: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CommunicationConversationCreateInput { - #[serde(default)] - title: Option, - agent_ids: Vec, - idempotency_key: String, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CommunicationConversationInput { - conversation_id: String, - #[serde(default)] - agent_id: Option, - #[serde(default)] - endpoint_id: Option, - #[serde(default)] - expected_controller_generation: Option, - #[serde(default)] - after_seq: Option, - #[serde(default)] - limit: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CommunicationMessagePostInput { - conversation_id: String, - body: String, - #[serde(default)] - author_agent_id: Option, - #[serde(default)] - endpoint_id: Option, - #[serde(default)] - expected_controller_generation: Option, - #[serde(default)] - recipient_agent_ids: Option>, - #[serde(default)] - reply_to: Option, - #[serde(default)] - idempotency_key: Option, - #[serde(default)] - wake_reply_id: Option, - #[serde(default)] - reply_operation_index: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CommunicationInboxInput { - agent_id: String, - endpoint_id: String, - expected_controller_generation: i64, - #[serde(default)] - after_delivery_order: Option, - #[serde(default)] - limit: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct CommunicationInboxConsumeInput { - agent_id: String, - endpoint_id: String, - expected_controller_generation: i64, - delivery_ids: Vec, -} - #[handler] pub(super) async fn communication_agents( req: &mut Request, @@ -196,13 +64,10 @@ pub(super) async fn communication_agents( ) -> CommunicationResult { let (runtime, auth) = prepared(req, depot).await?; require_communication_read(&auth)?; - let input: CommunicationAgentsInput = parse_console_json(req).await?; - Ok(CommunicationResponse(runtime.list_agent_identities( - Some(&auth), - input.agent_id, - input.offset, - input.limit, - ))) + let input: ListAgentIdentitiesInput = parse_console_json(req).await?; + Ok(CommunicationResponse( + runtime.list_agent_identities(Some(&auth), input), + )) } #[handler] @@ -212,15 +77,10 @@ pub(super) async fn communication_agent_create( ) -> CommunicationResult { let (runtime, auth) = prepared(req, depot).await?; require_communication_manage(&auth)?; - let input: CommunicationAgentCreateInput = parse_console_json(req).await?; - Ok(CommunicationResponse(runtime.create_agent_identity( - Some(&auth), - input.handle, - input.display_name, - input.description, - input.specialty_labels, - input.idempotency_key, - ))) + let input: CreateAgentIdentityInput = parse_console_json(req).await?; + Ok(CommunicationResponse( + runtime.create_agent_identity(Some(&auth), input), + )) } #[handler] @@ -230,16 +90,10 @@ pub(super) async fn communication_agent_update( ) -> CommunicationResult { let (runtime, auth) = prepared(req, depot).await?; require_communication_manage(&auth)?; - let input: CommunicationAgentUpdateInput = parse_console_json(req).await?; - Ok(CommunicationResponse(runtime.update_agent_identity( - Some(&auth), - input.agent_id, - input.expected_profile_revision, - input.handle, - input.display_name, - input.description, - input.specialty_labels, - ))) + let input: UpdateAgentIdentityInput = parse_console_json(req).await?; + Ok(CommunicationResponse( + runtime.update_agent_identity(Some(&auth), input), + )) } #[handler] @@ -249,14 +103,10 @@ pub(super) async fn communication_endpoint_attach( ) -> CommunicationResult { let (runtime, auth) = prepared(req, depot).await?; require_communication_manage(&auth)?; - let input: CommunicationEndpointAttachInput = parse_console_json(req).await?; - Ok(CommunicationResponse(runtime.attach_agent_endpoint( - Some(&auth), - input.agent_id, - input.host, - input.client_attachment_id, - input.idempotency_key, - ))) + let input: RotateAgentContinuationEndpointInput = parse_console_json(req).await?; + Ok(CommunicationResponse( + runtime.attach_agent_endpoint(Some(&auth), input), + )) } #[handler] @@ -281,9 +131,9 @@ pub(super) async fn communication_endpoint_detach( ) -> CommunicationResult { let (runtime, auth) = prepared(req, depot).await?; require_communication_manage(&auth)?; - let input: CommunicationEndpointDetachInput = parse_console_json(req).await?; + let input: DetachAgentEndpointInput = parse_console_json(req).await?; Ok(CommunicationResponse( - runtime.detach_agent_endpoint(Some(&auth), input.endpoint_id), + runtime.detach_agent_endpoint(Some(&auth), input), )) } @@ -294,15 +144,10 @@ pub(super) async fn communication_conversations( ) -> CommunicationResult { let (runtime, auth) = prepared(req, depot).await?; require_communication_read(&auth)?; - let input: CommunicationConversationsInput = parse_console_json(req).await?; - Ok(CommunicationResponse(runtime.list_conversations( - Some(&auth), - input.agent_id, - input.endpoint_id, - input.expected_controller_generation, - input.offset, - input.limit, - ))) + let input: ListConversationsInput = parse_console_json(req).await?; + Ok(CommunicationResponse( + runtime.list_conversations(Some(&auth), input), + )) } #[handler] @@ -312,13 +157,10 @@ pub(super) async fn communication_conversation_create( ) -> CommunicationResult { let (runtime, auth) = prepared(req, depot).await?; require_communication_manage(&auth)?; - let input: CommunicationConversationCreateInput = parse_console_json(req).await?; - Ok(CommunicationResponse(runtime.create_conversation( - Some(&auth), - input.title, - input.agent_ids, - input.idempotency_key, - ))) + let input: CreateConversationInput = parse_console_json(req).await?; + Ok(CommunicationResponse( + runtime.create_conversation(Some(&auth), input), + )) } #[handler] @@ -328,16 +170,10 @@ pub(super) async fn communication_conversation( ) -> CommunicationResult { let (runtime, auth) = prepared(req, depot).await?; require_communication_read(&auth)?; - let input: CommunicationConversationInput = parse_console_json(req).await?; - Ok(CommunicationResponse(runtime.read_conversation( - Some(&auth), - input.conversation_id, - input.agent_id, - input.endpoint_id, - input.expected_controller_generation, - input.after_seq, - input.limit, - ))) + let input: ReadConversationInput = parse_console_json(req).await?; + Ok(CommunicationResponse( + runtime.read_conversation(Some(&auth), input), + )) } #[handler] @@ -347,20 +183,10 @@ pub(super) async fn communication_message_post( ) -> CommunicationResult { let (runtime, auth) = prepared(req, depot).await?; require_communication_manage(&auth)?; - let input: CommunicationMessagePostInput = parse_console_json(req).await?; - Ok(CommunicationResponse(runtime.post_conversation_message( - Some(&auth), - input.conversation_id, - input.body, - input.author_agent_id, - input.endpoint_id, - input.expected_controller_generation, - input.recipient_agent_ids, - input.reply_to, - input.idempotency_key, - input.wake_reply_id, - input.reply_operation_index, - ))) + let input: PostConversationMessageInput = parse_console_json(req).await?; + Ok(CommunicationResponse( + runtime.post_conversation_message(Some(&auth), input), + )) } #[handler] @@ -370,15 +196,10 @@ pub(super) async fn communication_inbox( ) -> CommunicationResult { let (runtime, auth) = prepared(req, depot).await?; require_communication_read(&auth)?; - let input: CommunicationInboxInput = parse_console_json(req).await?; - Ok(CommunicationResponse(runtime.list_agent_inbox( - Some(&auth), - input.agent_id, - input.endpoint_id, - input.expected_controller_generation, - input.after_delivery_order, - input.limit, - ))) + let input: ListAgentInboxInput = parse_console_json(req).await?; + Ok(CommunicationResponse( + runtime.list_agent_inbox(Some(&auth), input), + )) } #[handler] @@ -388,12 +209,8 @@ pub(super) async fn communication_inbox_consume( ) -> CommunicationResult { let (runtime, auth) = prepared(req, depot).await?; require_communication_manage(&auth)?; - let input: CommunicationInboxConsumeInput = parse_console_json(req).await?; - Ok(CommunicationResponse(runtime.consume_agent_deliveries( - Some(&auth), - input.agent_id, - input.endpoint_id, - input.expected_controller_generation, - input.delivery_ids, - ))) + let input: ConsumeAgentDeliveriesInput = parse_console_json(req).await?; + Ok(CommunicationResponse( + runtime.consume_agent_deliveries(Some(&auth), input), + )) } diff --git a/src/runtime_console_http/goals.rs b/src/runtime_console_http/goals.rs index dba2c64fc..5b3d16c80 100644 --- a/src/runtime_console_http/goals.rs +++ b/src/runtime_console_http/goals.rs @@ -213,8 +213,14 @@ async fn exact_agent( auth: &crate::auth::AuthContext, agent_id: &str, ) -> Option { - let result = - runtime.list_agent_identities(Some(auth), Some(agent_id.to_string()), Some(0), Some(1)); + let result = runtime.list_agent_identities( + Some(auth), + webcodex_tool_contracts::ListAgentIdentitiesInput { + agent_id: Some(agent_id.to_string()), + offset: Some(0), + limit: Some(1), + }, + ); if !result.success { return None; } diff --git a/src/runtime_console_http/tests/goals.rs b/src/runtime_console_http/tests/goals.rs index b53bf426d..34451d58c 100644 --- a/src/runtime_console_http/tests/goals.rs +++ b/src/runtime_console_http/tests/goals.rs @@ -20,11 +20,13 @@ async fn goal_workbench_projects_durable_goal_truth_without_new_authority() { ); let agent = runtime.create_agent_identity( Some(&auth), - "goal-controller".into(), - "Goal Controller".into(), - None, - vec!["runtime-v2".into()], - "goal-workbench-controller".into(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "goal-controller".into(), + display_name: "Goal Controller".into(), + description: None, + specialty_labels: vec!["runtime-v2".into()], + idempotency_key: "goal-workbench-controller".into(), + }, ); assert!(agent.success, "{:?}", agent.output); let agent_id = agent.output["agent"]["agent_id"] @@ -99,10 +101,12 @@ async fn goal_workbench_projects_durable_goal_truth_without_new_authority() { assert!(task_link.success, "{:?}", task_link.output); let endpoint = runtime.attach_agent_endpoint( Some(&auth), - agent_id.clone(), - "ChatGPT".into(), - Some("goal-workbench-test".into()), - "goal-workbench-endpoint".into(), + webcodex_tool_contracts::RotateAgentContinuationEndpointInput { + agent_id: agent_id.clone(), + host: "ChatGPT".into(), + client_attachment_id: Some("goal-workbench-test".into()), + idempotency_key: "goal-workbench-endpoint".into(), + }, ); assert!(endpoint.success, "{:?}", endpoint.output); let endpoint_id = endpoint.output["endpoint"]["endpoint_id"] diff --git a/src/tool_runtime/communication.rs b/src/tool_runtime/communication.rs index 278ec3bf4..4e4d11927 100644 --- a/src/tool_runtime/communication.rs +++ b/src/tool_runtime/communication.rs @@ -9,6 +9,12 @@ use crate::db::{ use serde::Serialize; use serde_json::{json, to_value}; use sha2::{Digest, Sha256}; +use webcodex_tool_contracts::tool_call::{ + ConsumeAgentDeliveriesInput, CreateAgentIdentityInput, CreateConversationInput, + DetachAgentEndpointInput, ListAgentIdentitiesInput, ListAgentInboxInput, + ListConversationsInput, PostConversationMessageInput, ReadConversationInput, + RotateAgentContinuationEndpointInput, UpdateAgentIdentityInput, +}; const DEFAULT_COMMUNICATION_LIST_LIMIT: usize = 50; @@ -289,11 +295,7 @@ impl ToolRuntime { pub(crate) fn create_agent_identity( &self, auth: Option<&AuthContext>, - handle: String, - display_name: String, - description: Option, - specialty_labels: Vec, - idempotency_key: String, + input: CreateAgentIdentityInput, ) -> ToolResult { let principal = match communication_principal(auth) { Ok(principal) => principal, @@ -305,11 +307,11 @@ impl ToolRuntime { match db.create_agent_identity( &principal, NewAgentIdentity { - handle, - display_name, - description: description.unwrap_or_default(), - specialty_labels, - idempotency_key, + handle: input.handle, + display_name: input.display_name, + description: input.description.unwrap_or_default(), + specialty_labels: input.specialty_labels, + idempotency_key: input.idempotency_key, }, ) { Ok(result) => serialized_success(result), @@ -320,9 +322,7 @@ impl ToolRuntime { pub(crate) fn list_agent_identities( &self, auth: Option<&AuthContext>, - agent_id: Option, - offset: Option, - limit: Option, + input: ListAgentIdentitiesInput, ) -> ToolResult { let principal = match communication_principal(auth) { Ok(principal) => principal, @@ -333,9 +333,9 @@ impl ToolRuntime { }; match db.list_agent_identities( &principal, - agent_id.as_deref(), - offset.unwrap_or(0), - communication_list_limit(limit), + input.agent_id.as_deref(), + input.offset.unwrap_or(0), + communication_list_limit(input.limit), ) { Ok(result) => { let agents = result @@ -371,16 +371,10 @@ impl ToolRuntime { } } - #[allow(clippy::too_many_arguments)] pub(crate) fn update_agent_identity( &self, auth: Option<&AuthContext>, - agent_id: String, - expected_profile_revision: i64, - handle: Option, - display_name: Option, - description: Option, - specialty_labels: Option>, + input: UpdateAgentIdentityInput, ) -> ToolResult { let principal = match communication_principal(auth) { Ok(principal) => principal, @@ -391,13 +385,13 @@ impl ToolRuntime { }; match db.update_agent_identity( &principal, - &agent_id, - expected_profile_revision, + &input.agent_id, + input.expected_profile_revision, AgentProfilePatch { - handle, - display_name, - description, - specialty_labels, + handle: input.handle, + display_name: input.display_name, + description: input.description, + specialty_labels: input.specialty_labels, }, ) { Ok(result) => serialized_success(result), @@ -405,14 +399,10 @@ impl ToolRuntime { } } - #[allow(clippy::too_many_arguments)] pub(crate) fn attach_agent_endpoint( &self, auth: Option<&AuthContext>, - agent_id: String, - host: String, - client_attachment_id: Option, - idempotency_key: String, + input: RotateAgentContinuationEndpointInput, ) -> ToolResult { let principal = match communication_principal(auth) { Ok(principal) => principal, @@ -424,14 +414,14 @@ impl ToolRuntime { match db.attach_agent_endpoint( &principal, NewAgentEndpoint { - agent_id, - host, - client_attachment_id, + agent_id: input.agent_id, + host: input.host, + client_attachment_id: input.client_attachment_id, // Public/model/Console attachment cannot self-assert Host // continuation capability. Only process-local adapter // registration may transition this field to true. wake_capable: false, - idempotency_key, + idempotency_key: input.idempotency_key, }, ) { Ok(result) => { @@ -1280,7 +1270,7 @@ impl ToolRuntime { pub(crate) fn detach_agent_endpoint( &self, auth: Option<&AuthContext>, - endpoint_id: String, + input: DetachAgentEndpointInput, ) -> ToolResult { let principal = match communication_principal(auth) { Ok(principal) => principal, @@ -1289,7 +1279,7 @@ impl ToolRuntime { let Some(db) = self.communication_db.as_ref() else { return communication_store_unavailable(); }; - match db.detach_agent_endpoint(&principal, &endpoint_id) { + match db.detach_agent_endpoint(&principal, &input.endpoint_id) { Ok(result) => { if let Some(controller) = self.agent_continuations.as_ref() { controller.endpoint_detached( @@ -1310,9 +1300,7 @@ impl ToolRuntime { pub(crate) fn create_conversation( &self, auth: Option<&AuthContext>, - title: Option, - agent_ids: Vec, - idempotency_key: String, + input: CreateConversationInput, ) -> ToolResult { let principal = match communication_principal(auth) { Ok(principal) => principal, @@ -1324,9 +1312,9 @@ impl ToolRuntime { match db.create_conversation( &principal, NewConversation { - title, - agent_ids, - idempotency_key, + title: input.title, + agent_ids: input.agent_ids, + idempotency_key: input.idempotency_key, }, ) { Ok(result) => serialized_success(result), @@ -1334,88 +1322,73 @@ impl ToolRuntime { } } - #[allow(clippy::too_many_arguments)] pub(crate) fn list_conversations( &self, auth: Option<&AuthContext>, - agent_id: Option, - endpoint_id: Option, - expected_controller_generation: Option, - offset: Option, - limit: Option, + input: ListConversationsInput, ) -> ToolResult { let principal = match communication_principal(auth) { Ok(principal) => principal, Err(result) => return result, }; - let access = - match access_from_endpoint(agent_id, endpoint_id, expected_controller_generation) { - Ok(access) => access, - Err(result) => return result, - }; + let access = match access_from_endpoint( + input.agent_id, + input.endpoint_id, + input.expected_controller_generation, + ) { + Ok(access) => access, + Err(result) => return result, + }; let Some(db) = self.communication_db.as_ref() else { return communication_store_unavailable(); }; match db.list_conversations( &principal, &access, - offset.unwrap_or(0), - communication_list_limit(limit), + input.offset.unwrap_or(0), + communication_list_limit(input.limit), ) { Ok(result) => serialized_success(result), Err(error) => communication_error(error, RecoveryKind::RetrySame), } } - #[allow(clippy::too_many_arguments)] pub(crate) fn read_conversation( &self, auth: Option<&AuthContext>, - conversation_id: String, - agent_id: Option, - endpoint_id: Option, - expected_controller_generation: Option, - after_seq: Option, - limit: Option, + input: ReadConversationInput, ) -> ToolResult { let principal = match communication_principal(auth) { Ok(principal) => principal, Err(result) => return result, }; - let access = - match access_from_endpoint(agent_id, endpoint_id, expected_controller_generation) { - Ok(access) => access, - Err(result) => return result, - }; + let access = match access_from_endpoint( + input.agent_id, + input.endpoint_id, + input.expected_controller_generation, + ) { + Ok(access) => access, + Err(result) => return result, + }; let Some(db) = self.communication_db.as_ref() else { return communication_store_unavailable(); }; match db.read_conversation( &principal, &access, - &conversation_id, - after_seq.unwrap_or(0), - communication_list_limit(limit), + &input.conversation_id, + input.after_seq.unwrap_or(0), + communication_list_limit(input.limit), ) { Ok(result) => serialized_success(result), Err(error) => communication_error(error, RecoveryKind::RetrySame), } } - #[allow(clippy::too_many_arguments)] pub(crate) fn post_conversation_message( &self, auth: Option<&AuthContext>, - conversation_id: String, - body: String, - author_agent_id: Option, - endpoint_id: Option, - expected_controller_generation: Option, - recipient_agent_ids: Option>, - reply_to: Option, - idempotency_key: Option, - wake_reply_id: Option, - reply_operation_index: Option, + input: PostConversationMessageInput, ) -> ToolResult { let principal = match communication_principal(auth) { Ok(principal) => principal, @@ -1427,16 +1400,16 @@ impl ToolRuntime { match db.post_conversation_message( &principal, NewConversationMessage { - conversation_id, - body, - author_agent_id, - endpoint_id, - expected_controller_generation, - recipient_agent_ids, - reply_to, - idempotency_key, - wake_reply_id, - reply_operation_index, + conversation_id: input.conversation_id, + body: input.body, + author_agent_id: input.author_agent_id, + endpoint_id: input.endpoint_id, + expected_controller_generation: input.expected_controller_generation, + recipient_agent_ids: input.recipient_agent_ids, + reply_to: input.reply_to, + idempotency_key: input.idempotency_key, + wake_reply_id: input.wake_reply_id, + reply_operation_index: input.reply_operation_index, }, ) { Ok(result) => { @@ -1464,11 +1437,7 @@ impl ToolRuntime { pub(crate) fn list_agent_inbox( &self, auth: Option<&AuthContext>, - agent_id: String, - endpoint_id: String, - expected_controller_generation: i64, - after_delivery_order: Option, - limit: Option, + input: ListAgentInboxInput, ) -> ToolResult { let principal = match communication_principal(auth) { Ok(principal) => principal, @@ -1479,11 +1448,11 @@ impl ToolRuntime { }; match db.list_agent_inbox( &principal, - &agent_id, - &endpoint_id, - expected_controller_generation, - after_delivery_order.unwrap_or(0), - communication_list_limit(limit), + &input.agent_id, + &input.endpoint_id, + input.expected_controller_generation, + input.after_delivery_order.unwrap_or(0), + communication_list_limit(input.limit), ) { Ok(result) => serialized_success(result), Err(error) => communication_error(error, RecoveryKind::RetrySame), @@ -1493,10 +1462,7 @@ impl ToolRuntime { pub(crate) fn consume_agent_deliveries( &self, auth: Option<&AuthContext>, - agent_id: String, - endpoint_id: String, - expected_controller_generation: i64, - delivery_ids: Vec, + input: ConsumeAgentDeliveriesInput, ) -> ToolResult { let principal = match communication_principal(auth) { Ok(principal) => principal, @@ -1507,10 +1473,10 @@ impl ToolRuntime { }; match db.consume_agent_deliveries( &principal, - &agent_id, - &endpoint_id, - expected_controller_generation, - delivery_ids, + &input.agent_id, + &input.endpoint_id, + input.expected_controller_generation, + input.delivery_ids, ) { Ok(result) => serialized_success(result), Err(error) => communication_error(error, RecoveryKind::RetrySame), @@ -1762,11 +1728,13 @@ mod tests { let created_agent = runtime.create_agent_identity( Some(&bob), - "bob-agent".to_string(), - "Bob Agent".to_string(), - Some("private profile".to_string()), - vec!["privacy".to_string()], - "bob-agent-create".to_string(), + CreateAgentIdentityInput { + handle: "bob-agent".to_string(), + display_name: "Bob Agent".to_string(), + description: Some("private profile".to_string()), + specialty_labels: vec!["privacy".to_string()], + idempotency_key: "bob-agent-create".to_string(), + }, ); assert!(created_agent.success, "{:?}", created_agent.error); let bob_agent_id = created_agent.output["agent"]["agent_id"] @@ -1777,29 +1745,35 @@ mod tests { let foreign_agent = runtime.update_agent_identity( Some(&alice), - bob_agent_id.clone(), - 1, - None, - None, - Some("probe".to_string()), - None, + UpdateAgentIdentityInput { + agent_id: bob_agent_id.clone(), + expected_profile_revision: 1, + handle: None, + display_name: None, + description: Some("probe".to_string()), + specialty_labels: None, + }, ); let missing_agent_result = runtime.update_agent_identity( Some(&alice), - missing_agent, - 1, - None, - None, - Some("probe".to_string()), - None, + UpdateAgentIdentityInput { + agent_id: missing_agent, + expected_profile_revision: 1, + handle: None, + display_name: None, + description: Some("probe".to_string()), + specialty_labels: None, + }, ); assert_same_public_failure(foreign_agent, missing_agent_result, "agent_not_found"); let created_conversation = runtime.create_conversation( Some(&bob), - Some("Bob private room".to_string()), - vec![bob_agent_id], - "bob-room-create".to_string(), + CreateConversationInput { + title: Some("Bob private room".to_string()), + agent_ids: vec![bob_agent_id], + idempotency_key: "bob-room-create".to_string(), + }, ); assert!( created_conversation.success, @@ -1814,21 +1788,25 @@ mod tests { let missing_conversation = "wc_conv_iIiIiIiIiIiIiIiI".to_string(); let foreign_conversation = runtime.read_conversation( Some(&alice), - bob_conversation_id, - None, - None, - None, - Some(0), - Some(10), + ReadConversationInput { + conversation_id: bob_conversation_id, + agent_id: None, + endpoint_id: None, + expected_controller_generation: None, + after_seq: Some(0), + limit: Some(10), + }, ); let missing_conversation_result = runtime.read_conversation( Some(&alice), - missing_conversation, - None, - None, - None, - Some(0), - Some(10), + ReadConversationInput { + conversation_id: missing_conversation, + agent_id: None, + endpoint_id: None, + expected_controller_generation: None, + after_seq: Some(0), + limit: Some(10), + }, ); assert_same_public_failure( foreign_conversation, diff --git a/src/tool_runtime/dispatch/agents.rs b/src/tool_runtime/dispatch/agents.rs index 4ca74e323..e28e640fb 100644 --- a/src/tool_runtime/dispatch/agents.rs +++ b/src/tool_runtime/dispatch/agents.rs @@ -10,56 +10,15 @@ impl ToolRuntime { window: Option<&crate::client_window::ClientWindow>, ) -> ToolResult { match call { - ToolCall::CreateAgentIdentity { - handle, - display_name, - description, - specialty_labels, - idempotency_key, - } => self.create_agent_identity( - auth, - handle, - display_name, - description, - specialty_labels, - idempotency_key, - ), + ToolCall::CreateAgentIdentity(input) => self.create_agent_identity(auth, input), - ToolCall::ListAgentIdentities { - agent_id, - offset, - limit, - } => self.list_agent_identities(auth, agent_id, offset, limit), + ToolCall::ListAgentIdentities(input) => self.list_agent_identities(auth, input), - ToolCall::UpdateAgentIdentity { - agent_id, - expected_profile_revision, - handle, - display_name, - description, - specialty_labels, - } => self.update_agent_identity( - auth, - agent_id, - expected_profile_revision, - handle, - display_name, - description, - specialty_labels, - ), + ToolCall::UpdateAgentIdentity(input) => self.update_agent_identity(auth, input), - ToolCall::RotateAgentContinuationEndpoint { - agent_id, - host, - client_attachment_id, - idempotency_key, - } => self.attach_agent_endpoint( - auth, - agent_id, - host, - client_attachment_id, - idempotency_key, - ), + ToolCall::RotateAgentContinuationEndpoint(input) => { + self.attach_agent_endpoint(auth, input) + } ToolCall::PresentAgentContinuation { agent_continuation_ref, @@ -182,100 +141,19 @@ impl ToolRuntime { binding_id, ), - ToolCall::DetachAgentEndpoint { endpoint_id } => { - self.detach_agent_endpoint(auth, endpoint_id) - } + ToolCall::DetachAgentEndpoint(input) => self.detach_agent_endpoint(auth, input), - ToolCall::CreateConversation { - title, - agent_ids, - idempotency_key, - } => self.create_conversation(auth, title, agent_ids, idempotency_key), + ToolCall::CreateConversation(input) => self.create_conversation(auth, input), - ToolCall::ListConversations { - agent_id, - endpoint_id, - expected_controller_generation, - offset, - limit, - } => self.list_conversations( - auth, - agent_id, - endpoint_id, - expected_controller_generation, - offset, - limit, - ), + ToolCall::ListConversations(input) => self.list_conversations(auth, input), - ToolCall::ReadConversation { - conversation_id, - agent_id, - endpoint_id, - expected_controller_generation, - after_seq, - limit, - } => self.read_conversation( - auth, - conversation_id, - agent_id, - endpoint_id, - expected_controller_generation, - after_seq, - limit, - ), + ToolCall::ReadConversation(input) => self.read_conversation(auth, input), - ToolCall::PostConversationMessage { - conversation_id, - body, - author_agent_id, - endpoint_id, - expected_controller_generation, - recipient_agent_ids, - reply_to, - idempotency_key, - wake_reply_id, - reply_operation_index, - } => self.post_conversation_message( - auth, - conversation_id, - body, - author_agent_id, - endpoint_id, - expected_controller_generation, - recipient_agent_ids, - reply_to, - idempotency_key, - wake_reply_id, - reply_operation_index, - ), + ToolCall::PostConversationMessage(input) => self.post_conversation_message(auth, input), - ToolCall::ListAgentInbox { - agent_id, - endpoint_id, - expected_controller_generation, - after_delivery_order, - limit, - } => self.list_agent_inbox( - auth, - agent_id, - endpoint_id, - expected_controller_generation, - after_delivery_order, - limit, - ), + ToolCall::ListAgentInbox(input) => self.list_agent_inbox(auth, input), - ToolCall::ConsumeAgentDeliveries { - agent_id, - endpoint_id, - expected_controller_generation, - delivery_ids, - } => self.consume_agent_deliveries( - auth, - agent_id, - endpoint_id, - expected_controller_generation, - delivery_ids, - ), + ToolCall::ConsumeAgentDeliveries(input) => self.consume_agent_deliveries(auth, input), ToolCall::BootstrapAgentConversation { agent_id, diff --git a/src/tool_runtime/dispatch/routing.rs b/src/tool_runtime/dispatch/routing.rs index 32528b451..9de4c2d1a 100644 --- a/src/tool_runtime/dispatch/routing.rs +++ b/src/tool_runtime/dispatch/routing.rs @@ -248,10 +248,10 @@ impl ToolRuntime { self.dispatch_agent_work_authorized(call, auth).await } - call @ (ToolCall::CreateAgentIdentity { .. } - | ToolCall::ListAgentIdentities { .. } - | ToolCall::UpdateAgentIdentity { .. } - | ToolCall::RotateAgentContinuationEndpoint { .. } + call @ (ToolCall::CreateAgentIdentity(_) + | ToolCall::ListAgentIdentities(_) + | ToolCall::UpdateAgentIdentity(_) + | ToolCall::RotateAgentContinuationEndpoint(_) | ToolCall::PresentAgentContinuation { .. } | ToolCall::AgentContinuationBind { .. } | ToolCall::AgentContinuationRecoverEndpoint { .. } @@ -260,13 +260,13 @@ impl ToolRuntime { | ToolCall::AgentContinuationWakePrepare { .. } | ToolCall::AgentContinuationWakeFinish { .. } | ToolCall::AgentContinuationUnbind { .. } - | ToolCall::DetachAgentEndpoint { .. } - | ToolCall::CreateConversation { .. } - | ToolCall::ListConversations { .. } - | ToolCall::ReadConversation { .. } - | ToolCall::PostConversationMessage { .. } - | ToolCall::ListAgentInbox { .. } - | ToolCall::ConsumeAgentDeliveries { .. } + | ToolCall::DetachAgentEndpoint(_) + | ToolCall::CreateConversation(_) + | ToolCall::ListConversations(_) + | ToolCall::ReadConversation(_) + | ToolCall::PostConversationMessage(_) + | ToolCall::ListAgentInbox(_) + | ToolCall::ConsumeAgentDeliveries(_) | ToolCall::BootstrapAgentConversation { .. } | ToolCall::ConsumeAgentWake { .. }) => { self.dispatch_agents_authorized(call, auth, window).await diff --git a/src/tool_runtime/tests/agent_continuation_refs.rs b/src/tool_runtime/tests/agent_continuation_refs.rs index 59d7656e5..146b726ff 100644 --- a/src/tool_runtime/tests/agent_continuation_refs.rs +++ b/src/tool_runtime/tests/agent_continuation_refs.rs @@ -19,11 +19,13 @@ fn create_agent( ) -> String { let result = runtime.create_agent_identity( auth, - format!("agent-{key}"), - format!("Agent {key}"), - None, - Vec::new(), - key.to_string(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: format!("agent-{key}"), + display_name: format!("Agent {key}"), + description: None, + specialty_labels: Vec::new(), + idempotency_key: key.to_string(), + }, ); assert!(result.success, "{:?}", result.output); result.output["agent"]["agent_id"] @@ -40,10 +42,12 @@ fn rotate( ) -> serde_json::Value { let result = runtime.attach_agent_endpoint( auth, - agent_id.to_string(), - "ChatGPT".to_string(), - None, - key.to_string(), + webcodex_tool_contracts::RotateAgentContinuationEndpointInput { + agent_id: agent_id.to_string(), + host: "ChatGPT".to_string(), + client_attachment_id: None, + idempotency_key: key.to_string(), + }, ); assert!(result.success, "{:?}", result.output); result.output @@ -88,7 +92,14 @@ fn agent_continuation_ref_presents_the_pinned_tuple_and_fails_closed() { let first_generation = first["endpoint"]["controller_generation"].as_i64().unwrap(); assert!(first_ref.starts_with("~ac")); - let listed = runtime.list_agent_identities(Some(&alice), Some(agent_id.clone()), None, None); + let listed = runtime.list_agent_identities( + Some(&alice), + webcodex_tool_contracts::ListAgentIdentitiesInput { + agent_id: Some(agent_id.clone()), + offset: None, + limit: None, + }, + ); assert!(listed.success, "{:?}", listed.output); assert_eq!( listed.output["agents"][0]["agent_continuation_ref"], @@ -182,7 +193,14 @@ fn agent_continuation_ref_presents_the_pinned_tuple_and_fails_closed() { current.output["agent_continuation"]["endpoint_id"], second_endpoint ); - let relisted = runtime.list_agent_identities(Some(&alice), Some(agent_id.clone()), None, None); + let relisted = runtime.list_agent_identities( + Some(&alice), + webcodex_tool_contracts::ListAgentIdentitiesInput { + agent_id: Some(agent_id.clone()), + offset: None, + limit: None, + }, + ); assert_eq!( relisted.output["agents"][0]["agent_continuation_ref"], second_ref @@ -264,12 +282,24 @@ fn agent_continuation_ref_presents_the_pinned_tuple_and_fails_closed() { "a workflow session must not join the continuation selector" ); - let detached = runtime.detach_agent_endpoint(Some(&alice), second_endpoint.to_string()); + let detached = runtime.detach_agent_endpoint( + Some(&alice), + webcodex_tool_contracts::DetachAgentEndpointInput { + endpoint_id: second_endpoint.to_string(), + }, + ); assert!(detached.success, "{:?}", detached.output); assert!(detached.output["agent_continuation_ref"].is_null()); let after_detach = present_ref(&runtime, Some(&alice), &second_ref); assert_eq!(after_detach.output["error_kind"], "endpoint_detached"); - let unattached = runtime.list_agent_identities(Some(&alice), Some(agent_id), None, None); + let unattached = runtime.list_agent_identities( + Some(&alice), + webcodex_tool_contracts::ListAgentIdentitiesInput { + agent_id: Some(agent_id), + offset: None, + limit: None, + }, + ); assert!(unattached.output["agents"][0]["agent_continuation_ref"].is_null()); } diff --git a/src/tool_runtime/tests/agent_task_attempt_refs.rs b/src/tool_runtime/tests/agent_task_attempt_refs.rs index c6890cfea..53436b4d3 100644 --- a/src/tool_runtime/tests/agent_task_attempt_refs.rs +++ b/src/tool_runtime/tests/agent_task_attempt_refs.rs @@ -18,11 +18,13 @@ fn create_agent( ) -> String { let result = runtime.create_agent_identity( auth, - format!("agent-{key}"), - format!("Agent {key}"), - None, - Vec::new(), - key.to_string(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: format!("agent-{key}"), + display_name: format!("Agent {key}"), + description: None, + specialty_labels: Vec::new(), + idempotency_key: key.to_string(), + }, ); assert!(result.success, "{:?}", result.output); result.output["agent"]["agent_id"] diff --git a/src/tool_runtime/tests/agent_tasks.rs b/src/tool_runtime/tests/agent_tasks.rs index c33e0256e..bfe45ed40 100644 --- a/src/tool_runtime/tests/agent_tasks.rs +++ b/src/tool_runtime/tests/agent_tasks.rs @@ -30,11 +30,13 @@ fn runtime_with_db() -> (tempfile::TempDir, Arc, ToolRuntim fn create_agent(runtime: &ToolRuntime, handle: &str) -> String { let result = runtime.create_agent_identity( None, - handle.to_string(), - format!("{handle} display"), - None, - Vec::new(), - format!("create-{handle}"), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: handle.to_string(), + display_name: format!("{handle} display"), + description: None, + specialty_labels: Vec::new(), + idempotency_key: format!("create-{handle}"), + }, ); assert!(result.success, "{:?}", result.output); result.output["agent"]["agent_id"] @@ -630,11 +632,13 @@ async fn coding_run_attempt_ref_executes_then_reconciles_from_reopened_db_and_fr .await; let agent_result = runtime.create_agent_identity( Some(&auth), - "a4a-runtime-agent".to_string(), - "A4a Runtime Agent".to_string(), - None, - Vec::new(), - "a4a-runtime-agent-create".to_string(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "a4a-runtime-agent".to_string(), + display_name: "A4a Runtime Agent".to_string(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "a4a-runtime-agent-create".to_string(), + }, ); assert!(agent_result.success, "{:?}", agent_result.output); let assignee = agent_result.output["agent"]["agent_id"] @@ -1088,11 +1092,13 @@ fn foreign_runtime_task_ids_are_existence_hidden_and_project_reference_grants_no let alice = auth_context(Some("alice"), false); let bob_agent = runtime.create_agent_identity( Some(&bob), - "bob-task-agent".to_string(), - "Bob Task Agent".to_string(), - None, - Vec::new(), - "bob-task-agent-create".to_string(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "bob-task-agent".to_string(), + display_name: "Bob Task Agent".to_string(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "bob-task-agent-create".to_string(), + }, ); assert!(bob_agent.success); let bob_agent_id = bob_agent.output["agent"]["agent_id"] diff --git a/src/tool_runtime/tests/agent_waits.rs b/src/tool_runtime/tests/agent_waits.rs index 115594f2f..a4ccf0abd 100644 --- a/src/tool_runtime/tests/agent_waits.rs +++ b/src/tool_runtime/tests/agent_waits.rs @@ -225,11 +225,13 @@ fn runtime_with_db() -> (tempfile::TempDir, Arc, ToolRuntim fn create_agent(runtime: &ToolRuntime, handle: &str) -> String { let result = runtime.create_agent_identity( None, - handle.to_string(), - format!("{handle} display"), - None, - Vec::new(), - format!("create-{handle}"), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: handle.to_string(), + display_name: format!("{handle} display"), + description: None, + specialty_labels: Vec::new(), + idempotency_key: format!("create-{handle}"), + }, ); assert!(result.success, "{:?}", result.output); result.output["agent"]["agent_id"] @@ -245,10 +247,12 @@ fn wait_runtime_surface_returns_exact_wait_and_existing_continuation_projection( let worker = create_agent(&runtime, "wait-runtime-worker"); let endpoint = runtime.attach_agent_endpoint( None, - watcher.clone(), - "ChatGPT".to_string(), - Some("wait-runtime-view".to_string()), - "wait-runtime-endpoint".to_string(), + webcodex_tool_contracts::RotateAgentContinuationEndpointInput { + agent_id: watcher.clone(), + host: "ChatGPT".to_string(), + client_attachment_id: Some("wait-runtime-view".to_string()), + idempotency_key: "wait-runtime-endpoint".to_string(), + }, ); assert!(endpoint.success, "{:?}", endpoint.output); let endpoint_id = endpoint.output["endpoint"]["endpoint_id"] @@ -426,10 +430,12 @@ fn goal_scoped_wait_model_projection_exposes_only_exact_goal_reference() { let worker = create_agent(&runtime, "goal-scoped-projection-worker"); let endpoint = runtime.attach_agent_endpoint( None, - controller.clone(), - "ChatGPT".to_string(), - Some("goal-scoped-projection-view".to_string()), - "goal-scoped-projection-endpoint".to_string(), + webcodex_tool_contracts::RotateAgentContinuationEndpointInput { + agent_id: controller.clone(), + host: "ChatGPT".to_string(), + client_attachment_id: Some("goal-scoped-projection-view".to_string()), + idempotency_key: "goal-scoped-projection-endpoint".to_string(), + }, ); assert!(endpoint.success, "{:?}", endpoint.output); let endpoint_id = endpoint.output["endpoint"]["endpoint_id"] @@ -578,11 +584,13 @@ async fn agent_wait_dispatch_is_sparse_while_app_and_durable_read_remain_complet let auth = super::support::auth_context(None, true); let created_agent = runtime.create_agent_identity( Some(&auth), - "sparse-wait-agent".into(), - "Sparse Wait".into(), - None, - Vec::new(), - "sparse-agent".into(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "sparse-wait-agent".into(), + display_name: "Sparse Wait".into(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "sparse-agent".into(), + }, ); assert!(created_agent.success); let agent = created_agent.output["agent"]["agent_id"] @@ -591,10 +599,12 @@ async fn agent_wait_dispatch_is_sparse_while_app_and_durable_read_remain_complet .to_string(); let endpoint = runtime.attach_agent_endpoint( Some(&auth), - agent.clone(), - "test".into(), - None, - "sparse-endpoint".into(), + webcodex_tool_contracts::RotateAgentContinuationEndpointInput { + agent_id: agent.clone(), + host: "test".into(), + client_attachment_id: None, + idempotency_key: "sparse-endpoint".into(), + }, ); let task = runtime.create_agent_task( Some(&auth), diff --git a/src/tool_runtime/tests/control_sidecars.rs b/src/tool_runtime/tests/control_sidecars.rs index 983f0f3da..d7f42fd4b 100644 --- a/src/tool_runtime/tests/control_sidecars.rs +++ b/src/tool_runtime/tests/control_sidecars.rs @@ -648,11 +648,13 @@ async fn control_sidecars_wake_exact_consume_replay_and_stale_proofs() { let principal = communication_principal(Some(&f.auth)).unwrap(); let agent = f.runtime.create_agent_identity( Some(&f.auth), - "control-agent".into(), - "Control Agent".into(), - None, - vec![], - "agent".into(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "control-agent".into(), + display_name: "Control Agent".into(), + description: None, + specialty_labels: vec![], + idempotency_key: "agent".into(), + }, ); assert!(agent.success); let agent_id = agent.output["agent"]["agent_id"].as_str().unwrap(); @@ -762,11 +764,13 @@ async fn control_sidecars_heartbeat_exact_lease_and_no_implicit_renewal() { let f = Fixture::new(); let agent = f.runtime.create_agent_identity( Some(&f.auth), - "heartbeat-agent".into(), - "Heartbeat Agent".into(), - None, - vec![], - "agent".into(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "heartbeat-agent".into(), + display_name: "Heartbeat Agent".into(), + description: None, + specialty_labels: vec![], + idempotency_key: "agent".into(), + }, ); let agent_id = agent.output["agent"]["agent_id"] .as_str() diff --git a/src/tool_runtime/tests/goal_workflow.rs b/src/tool_runtime/tests/goal_workflow.rs index 328321d84..ffce46af1 100644 --- a/src/tool_runtime/tests/goal_workflow.rs +++ b/src/tool_runtime/tests/goal_workflow.rs @@ -40,11 +40,13 @@ impl Workflow { .session_id; let agent = runtime.create_agent_identity( Some(&auth), - "workflow-agent".into(), - "Workflow Agent".into(), - None, - Vec::new(), - "workflow-agent".into(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "workflow-agent".into(), + display_name: "Workflow Agent".into(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "workflow-agent".into(), + }, ); assert!(agent.success, "{:?}", agent.output); let agent_id = agent.output["agent"]["agent_id"] @@ -253,10 +255,12 @@ async fn goal_continuity_carrier_readiness_tracks_exact_endpoint_generation() { let first = fixture.runtime.attach_agent_endpoint( Some(&fixture.auth), - fixture.agent_id.clone(), - "ChatGPT".into(), - Some("goal-continuity-view-1".into()), - "goal-continuity-endpoint-1".into(), + webcodex_tool_contracts::RotateAgentContinuationEndpointInput { + agent_id: fixture.agent_id.clone(), + host: "ChatGPT".into(), + client_attachment_id: Some("goal-continuity-view-1".into()), + idempotency_key: "goal-continuity-endpoint-1".into(), + }, ); assert!(first.success, "{:?}", first.output); let first_endpoint = first.output["endpoint"]["endpoint_id"] @@ -291,10 +295,12 @@ async fn goal_continuity_carrier_readiness_tracks_exact_endpoint_generation() { let second = fixture.runtime.attach_agent_endpoint( Some(&fixture.auth), - fixture.agent_id.clone(), - "ChatGPT".into(), - Some("goal-continuity-view-2".into()), - "goal-continuity-endpoint-2".into(), + webcodex_tool_contracts::RotateAgentContinuationEndpointInput { + agent_id: fixture.agent_id.clone(), + host: "ChatGPT".into(), + client_attachment_id: Some("goal-continuity-view-2".into()), + idempotency_key: "goal-continuity-endpoint-2".into(), + }, ); assert!(second.success, "{:?}", second.output); let second_endpoint = second.output["endpoint"]["endpoint_id"] @@ -519,11 +525,13 @@ async fn goal_continuity_does_not_retarget_an_existing_stall_wake_after_controll let replacement = fixture.runtime.create_agent_identity( Some(&fixture.auth), - "replacement-controller".into(), - "Replacement Controller".into(), - None, - Vec::new(), - "replacement-controller-agent".into(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "replacement-controller".into(), + display_name: "Replacement Controller".into(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "replacement-controller-agent".into(), + }, ); assert!(replacement.success, "{:?}", replacement.output); let replacement_id = replacement.output["agent"]["agent_id"] @@ -671,10 +679,12 @@ async fn goal_continuity_ignores_unrelated_task_wait_and_other_goal_wakes_on_sam let endpoint = fixture.runtime.attach_agent_endpoint( Some(&fixture.auth), - fixture.agent_id.clone(), - "ChatGPT".into(), - Some("unrelated-wait-view".into()), - "unrelated-wait-endpoint".into(), + webcodex_tool_contracts::RotateAgentContinuationEndpointInput { + agent_id: fixture.agent_id.clone(), + host: "ChatGPT".into(), + client_attachment_id: Some("unrelated-wait-view".into()), + idempotency_key: "unrelated-wait-endpoint".into(), + }, ); assert!(endpoint.success, "{:?}", endpoint.output); let endpoint_id = endpoint.output["endpoint"]["endpoint_id"] @@ -686,11 +696,13 @@ async fn goal_continuity_ignores_unrelated_task_wait_and_other_goal_wakes_on_sam .unwrap(); let worker = fixture.runtime.create_agent_identity( Some(&fixture.auth), - "unrelated-wait-worker".into(), - "Unrelated Wait Worker".into(), - None, - Vec::new(), - "unrelated-wait-worker-agent".into(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "unrelated-wait-worker".into(), + display_name: "Unrelated Wait Worker".into(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "unrelated-wait-worker-agent".into(), + }, ); assert!(worker.success, "{:?}", worker.output); let worker_id = worker.output["agent"]["agent_id"] @@ -1146,10 +1158,12 @@ async fn goal_workflow_revoked_correlated_project_is_partial_even_with_older_wor let fixture = Workflow::new(true).await; let endpoint = fixture.runtime.attach_agent_endpoint( Some(&fixture.auth), - fixture.agent_id.clone(), - "ChatGPT".into(), - Some("goal-partial-readiness-view".into()), - "goal-partial-readiness-endpoint".into(), + webcodex_tool_contracts::RotateAgentContinuationEndpointInput { + agent_id: fixture.agent_id.clone(), + host: "ChatGPT".into(), + client_attachment_id: Some("goal-partial-readiness-view".into()), + idempotency_key: "goal-partial-readiness-endpoint".into(), + }, ); assert!(endpoint.success, "{:?}", endpoint.output); let bound = fixture.runtime.agent_continuation_bind_for_window( @@ -1738,11 +1752,13 @@ async fn goal_workflow_same_durable_agent_is_worker_and_controller_and_stall_use let replacement = fixture.runtime.create_agent_identity( Some(&fixture.auth), - "post-resume-replacement".into(), - "Post Resume Replacement".into(), - None, - Vec::new(), - format!("post-resume-replacement-{delivery_unknown}"), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "post-resume-replacement".into(), + display_name: "Post Resume Replacement".into(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: format!("post-resume-replacement-{delivery_unknown}"), + }, ); assert!(replacement.success, "{:?}", replacement.output); let replacement_id = replacement.output["agent"]["agent_id"] diff --git a/src/tool_runtime/tests/goals.rs b/src/tool_runtime/tests/goals.rs index 9e77df781..2e046f87e 100644 --- a/src/tool_runtime/tests/goals.rs +++ b/src/tool_runtime/tests/goals.rs @@ -867,11 +867,13 @@ async fn prepare_goal_workflow_reauthorizes_session_project_controller_and_stays let controller = runtime.create_agent_identity( Some(&owner), - "prepare-goal-controller".to_string(), - "Prepare Goal Controller".to_string(), - None, - Vec::new(), - "prepare-goal-controller-create".to_string(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "prepare-goal-controller".to_string(), + display_name: "Prepare Goal Controller".to_string(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "prepare-goal-controller-create".to_string(), + }, ); assert!(controller.success, "{:?}", controller.output); let controller_id = controller.output["agent"]["agent_id"] @@ -972,11 +974,13 @@ async fn prepare_goal_workflow_reauthorizes_session_project_controller_and_stays let foreign_controller = runtime.create_agent_identity( Some(&foreign), - "prepare-foreign-controller".to_string(), - "Prepare Foreign Controller".to_string(), - None, - Vec::new(), - "prepare-foreign-controller-create".to_string(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "prepare-foreign-controller".to_string(), + display_name: "Prepare Foreign Controller".to_string(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "prepare-foreign-controller-create".to_string(), + }, ); assert!( foreign_controller.success, @@ -1109,11 +1113,13 @@ fn goal_runtime_controller_is_explicit_authorized_and_readable() { let alice = auth_context(Some("alice-goal-controller"), false); let controller = runtime.create_agent_identity( Some(&bob), - "bob-goal-controller".to_string(), - "Bob Goal Controller".to_string(), - None, - Vec::new(), - "bob-goal-controller-agent".to_string(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "bob-goal-controller".to_string(), + display_name: "Bob Goal Controller".to_string(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "bob-goal-controller-agent".to_string(), + }, ); assert!(controller.success, "{:?}", controller.output); let controller_id = controller.output["agent"]["agent_id"] @@ -1122,11 +1128,13 @@ fn goal_runtime_controller_is_explicit_authorized_and_readable() { .to_string(); let replacement = runtime.create_agent_identity( Some(&bob), - "bob-goal-controller-2".to_string(), - "Bob Goal Controller 2".to_string(), - None, - Vec::new(), - "bob-goal-controller-agent-2".to_string(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "bob-goal-controller-2".to_string(), + display_name: "Bob Goal Controller 2".to_string(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "bob-goal-controller-agent-2".to_string(), + }, ); assert!(replacement.success, "{:?}", replacement.output); let replacement_id = replacement.output["agent"]["agent_id"] @@ -1135,11 +1143,13 @@ fn goal_runtime_controller_is_explicit_authorized_and_readable() { .to_string(); let foreign = runtime.create_agent_identity( Some(&alice), - "alice-goal-controller".to_string(), - "Alice Goal Controller".to_string(), - None, - Vec::new(), - "alice-goal-controller-agent".to_string(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "alice-goal-controller".to_string(), + display_name: "Alice Goal Controller".to_string(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "alice-goal-controller-agent".to_string(), + }, ); assert!(foreign.success, "{:?}", foreign.output); let foreign_id = foreign.output["agent"]["agent_id"] @@ -1202,11 +1212,13 @@ async fn goal_plan_projection_is_exact_pure_revisioned_terminal_and_existence_hi let alice = auth_context(Some("alice-plan"), false); let controller = runtime.create_agent_identity( Some(&bob), - "bob-plan-controller".to_string(), - "Bob Plan Controller".to_string(), - None, - Vec::new(), - "bob-plan-controller-agent".to_string(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "bob-plan-controller".to_string(), + display_name: "Bob Plan Controller".to_string(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "bob-plan-controller-agent".to_string(), + }, ); assert!(controller.success, "{:?}", controller.output); let controller_id = controller.output["agent"]["agent_id"] @@ -1725,11 +1737,13 @@ fn goal_agent_task_link_reauthorizes_task_and_task_completion_never_completes_go let bob_agent = runtime.create_agent_identity( Some(&bob), - "bob-goal-worker".to_string(), - "Bob Goal Worker".to_string(), - None, - Vec::new(), - "bob-goal-worker-create".to_string(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "bob-goal-worker".to_string(), + display_name: "Bob Goal Worker".to_string(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "bob-goal-worker-create".to_string(), + }, ); assert!(bob_agent.success); let bob_agent_id = bob_agent.output["agent"]["agent_id"] @@ -1775,11 +1789,13 @@ fn goal_agent_task_link_reauthorizes_task_and_task_completion_never_completes_go let alice_agent = runtime.create_agent_identity( Some(&alice), - "alice-goal-worker".to_string(), - "Alice Goal Worker".to_string(), - None, - Vec::new(), - "alice-goal-worker-create".to_string(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "alice-goal-worker".to_string(), + display_name: "Alice Goal Worker".to_string(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "alice-goal-worker-create".to_string(), + }, ); assert!(alice_agent.success); let alice_agent_id = alice_agent.output["agent"]["agent_id"] @@ -1863,10 +1879,12 @@ fn goal_agent_task_link_reauthorizes_task_and_task_completion_never_completes_go let endpoint = runtime.attach_agent_endpoint( Some(&bob), - bob_agent_id.clone(), - "ChatGPT".to_string(), - Some("goal-attention-runtime".to_string()), - "goal-attention-runtime-endpoint".to_string(), + webcodex_tool_contracts::RotateAgentContinuationEndpointInput { + agent_id: bob_agent_id.clone(), + host: "ChatGPT".to_string(), + client_attachment_id: Some("goal-attention-runtime".to_string()), + idempotency_key: "goal-attention-runtime-endpoint".to_string(), + }, ); assert!(endpoint.success, "{:?}", endpoint.output); let endpoint_id = endpoint.output["endpoint"]["endpoint_id"] diff --git a/src/tool_runtime/tests/workbench.rs b/src/tool_runtime/tests/workbench.rs index 20ea469f0..bfa252780 100644 --- a/src/tool_runtime/tests/workbench.rs +++ b/src/tool_runtime/tests/workbench.rs @@ -88,11 +88,13 @@ async fn workbench_runner_submission_is_durable_and_same_key_replay_never_starts let auth = bootstrap_auth_context(); let agent = runtime.create_agent_identity( Some(&auth), - "workbench-runner-agent".into(), - "Workbench Runner Agent".into(), - None, - Vec::new(), - "workbench-runner-agent-create".into(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "workbench-runner-agent".into(), + display_name: "Workbench Runner Agent".into(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "workbench-runner-agent-create".into(), + }, ); assert!(agent.success, "{:?}", agent.output); let agent_id = agent.output["agent"]["agent_id"].as_str().unwrap(); @@ -411,11 +413,13 @@ async fn workbench_rejects_missing_project_and_stale_chat_route_before_creating_ let auth = bootstrap_auth_context(); let agent = runtime.create_agent_identity( Some(&auth), - "workbench-reject-agent".into(), - "Workbench Reject Agent".into(), - None, - Vec::new(), - "workbench-reject-agent-create".into(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "workbench-reject-agent".into(), + display_name: "Workbench Reject Agent".into(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "workbench-reject-agent-create".into(), + }, ); assert!(agent.success, "{:?}", agent.output); let agent_id = agent.output["agent"]["agent_id"].as_str().unwrap(); @@ -481,11 +485,13 @@ async fn workbench_chat_executor_is_projected_and_usable_only_in_its_bound_windo let auth = bootstrap_auth_context(); let agent = runtime.create_agent_identity( Some(&auth), - "workbench-chat-agent".into(), - "Workbench Chat Agent".into(), - None, - Vec::new(), - "workbench-chat-agent-create".into(), + webcodex_tool_contracts::CreateAgentIdentityInput { + handle: "workbench-chat-agent".into(), + display_name: "Workbench Chat Agent".into(), + description: None, + specialty_labels: Vec::new(), + idempotency_key: "workbench-chat-agent-create".into(), + }, ); assert!(agent.success, "{:?}", agent.output); let agent_id = agent.output["agent"]["agent_id"] @@ -494,10 +500,12 @@ async fn workbench_chat_executor_is_projected_and_usable_only_in_its_bound_windo .to_owned(); let attached = runtime.attach_agent_endpoint( Some(&auth), - agent_id.clone(), - "ChatGPT".into(), - None, - "workbench-chat-endpoint".into(), + webcodex_tool_contracts::RotateAgentContinuationEndpointInput { + agent_id: agent_id.clone(), + host: "ChatGPT".into(), + client_attachment_id: None, + idempotency_key: "workbench-chat-endpoint".into(), + }, ); assert!(attached.success, "{:?}", attached.output); let endpoint_id = attached.output["endpoint"]["endpoint_id"]