tiny.acp.protocol
Defined in tiny.acp.
Zig shapes for the Agent Client Protocol messages the client reads and writes: the agent's answer to initialize, the session's modes, the blocks a prompt carries, the progress reports the agent sends, its permission requests, and the result of a prompt.
API (13)
Types and contracts
Public types and contracts.
Initialize: A caller uses this structure to learn which protocol version the agent speaks, which kinds of prompt block it accepts, and how it authenticates.Modes: A caller uses this structure to learn which mode the session starts in, such asplanoract, and how many modes the agent offers.Overflow: Records what a reply kept when one more chunk would have passed a limit, and which update the client left out, so the caller tells a cut reply from a whole one and sees where the reply stopped.OverflowKind: The reply limit a prompt reached.PermissionRequest: Onesession/request_permissionrequest from the agent: the tool call it names, the decision, and the exact line.PromptBlobResource: A caller uses this structure to put a resource's content in the prompt as a blob string.PromptContent: A prompt is a list of these blocks, so a caller can mix text with files, links and media in one prompt.PromptMedia: A caller uses this structure to put an image or an audio clip in a prompt.PromptResourceLink: A caller uses this structure to point the agent at a resource by URI without putting its content in the prompt.PromptResult: The caller gets the reply with the reason the agent stopped and whether the reply was cut short becauseClient.promptDetailedandClient.promptDetailedContentreturn one of these results.PromptTextResource: A caller uses this structure to put a file's text in the prompt, such as source code the agent should read.Update: Onesession/updatenotification from the agent, with the two numbers the client gave it, the exact line, and fields read from it, received by the observer so the caller records and inspects the agent's progress.UpdateKind: The kind of one progress report (session update), read from the report'ssessionUpdatename, so the caller switches on the kind of each progress report the agent sends.
Source
Source: lib/acp/src/protocol.zig
zig
//! Zig shapes for the Agent Client Protocol messages the client reads and writes: the agent's//! answer to `initialize`, the session's modes, the blocks a prompt carries, the progress reports//! the agent sends, its permission requests, and the result of a prompt.//!//! A caller has to read what the agent reported without walking the JSON itself, and it has to keep//! that data after the parsed message is gone.//!//! The messages carry many optional fields, and an agent can send kinds of progress report//! unfamiliar to the code.//!//! The fields follow the protocol's JSON keys, such as `protocolVersion`, `agentCapabilities`,//! `sessionUpdate`, `toolCall` and `stopReason`. Prompt blocks are written with the protocol's type//! names `text`, `image`, `audio`, `resource` and `resource_link`.//!//! Each parsed shape copies the strings it keeps with the caller's allocator, so it outlives the//! parsed JSON, and its `deinit` frees them. Each parsed shape breaks out a few fields as typed//! values and keeps a minified JSON copy (*raw copy*) of the rest of its part of the message. A//! missing optional string reads as an empty slice, a missing flag as false, a missing list as a//! count of 0, and a progress report of an unknown kind as `other`. When the client writes a prompt//! block, it leaves out every optional string that is empty. The one borrowed field is the exact//! line the agent sent (*envelope*), in `Update` and `PermissionRequest`: it points into the//! reader's buffer and is valid only while the caller's callback runs.//!//! - *observer*: the caller's callbacks for updates and permission decisions//! - *summary text*: one short string picked from an update's fields//! - *transport epoch*: a nonzero number the caller picks for one client//! - *update sequence*: an update's number, counted from 1 in arrival order//! - *overflow*: a record that a reply stopped at a limit//! - *permission policy*: two caller lists of tool kinds and title prefixes to allowconst std = @import("std");const pretty = @import("pretty");const json = @import("json.zig");const Allocator = std.mem.Allocator;const pretty_json = pretty.json;/// A caller uses this structure to learn which protocol version the agent speaks, which kinds of/// prompt block it accepts, and how it authenticates. The structure holds the agent's answer to/// `initialize`: protocol version, capability flags, authentication methods, and a raw copy of the/// whole result. `Client.start` fills the structure and keeps it in `Client.initialize`. Every/// field has a default, and the default value stands for an absent answer. The structure owns its/// two strings, and `deinit` frees them.pub const Initialize = struct { /// The protocol version the agent answered with, from `result.protocolVersion`. `Client.start` /// accepts only 1. The field defaults to 0. protocol_version: i64 = 0, /// The agent's `agentCapabilities.loadSession` flag. The value is false when the key is absent. load_session: bool = false, /// The agent's `promptCapabilities.audio` flag, for prompts that carry audio blocks. The value /// is false when the key is absent. prompt_audio: bool = false, /// The agent's `promptCapabilities.image` flag, for prompts that carry image blocks. The value /// is false when the key is absent. prompt_image: bool = false, /// Reports true when `promptCapabilities` sets either `embeddedContent` or `embeddedContext` to /// true. The value is false when both keys are absent. prompt_embedded_content: bool = false, /// The number of entries in the answer's `authMethods` list. The value is 0 when the key is /// absent or holds a non-list value. auth_methods: usize = 0, /// The ids of the authentication methods as one minified JSON array of strings, such as /// `["token"]`. A method lacking a string `id` is skipped, and the slice is empty when all /// methods lack one. The slice is owned. auth_method_ids: []u8 = &.{}, /// A minified JSON copy of the whole `result` object. The slice is owned. raw: []u8 = &.{}, /// A caller uses this function to free the answer once the caller is done with it. The function /// frees the two owned strings with the given allocator and leaves the value undefined. The /// call is safe on the default value, which owns zero allocations. The caller provides the /// allocator that `fromResponse` used. pub fn deinit(self: *Initialize, allocator: Allocator) void { if (self.auth_method_ids.len != 0) allocator.free(self.auth_method_ids); if (self.raw.len != 0) allocator.free(self.raw); self.* = undefined; } /// A caller uses this function to turn a parsed `initialize` response into typed fields, as /// `Client.start` does. The function reads a parsed JSON-RPC response and returns its result as /// an `Initialize`. The function fails with `error.AgentProtocolError` when the value is a /// non-object, carries an `error` member, lacks an object `result`, or the result lacks an /// integer `protocolVersion`. The parser reads a missing capability flag as false. The call /// copies the strings it keeps with the allocator, so the result outlives the parsed JSON, and /// the caller frees it with `deinit`. The function fails with `error.OutOfMemory` and leaves /// zero allocations when a copy fails. pub fn fromResponse(allocator: Allocator, value: std.json.Value) !Initialize { const result = try responseResult(value); const protocol_version = json.objectInteger(result, "protocolVersion") orelse return error.AgentProtocolError; const raw = try rawJsonAlloc(allocator, result); errdefer if (raw.len != 0) allocator.free(raw); const auth_method_ids = try authMethodIdsAlloc(allocator, result); errdefer if (auth_method_ids.len != 0) allocator.free(auth_method_ids); var load_session = false; var prompt_audio = false; var prompt_image = false; var prompt_embedded_content = false; if (json.objectObject(result, "agentCapabilities")) |capabilities| { load_session = json.objectBool(capabilities, "loadSession") orelse false; if (json.objectObject(capabilities, "promptCapabilities")) |prompt| { prompt_audio = json.objectBool(prompt, "audio") orelse false; prompt_image = json.objectBool(prompt, "image") orelse false; prompt_embedded_content = (json.objectBool(prompt, "embeddedContent") orelse false) or (json.objectBool(prompt, "embeddedContext") orelse false); } } return .{ .protocol_version = protocol_version, .load_session = load_session, .prompt_audio = prompt_audio, .prompt_image = prompt_image, .prompt_embedded_content = prompt_embedded_content, .auth_methods = arrayLength(result, "authMethods"), .auth_method_ids = auth_method_ids, .raw = raw, }; }};/// A caller uses this structure to learn which mode the session starts in, such as `plan` or `act`,/// and how many modes the agent offers. The structure holds the session's current mode id, the/// number of modes on offer, and a raw copy of the `modes` object from the `session/new` answer./// `Client.start` keeps the structure in `Client.modes`, and the value stays empty when the agent/// reports zero modes. The structure owns `current` and `raw`, and `deinit` frees them.pub const Modes = struct { /// The current mode's id, from `modes.currentModeId`. The slice is owned, and empty by default. current: []u8 = &.{}, /// The number of entries in `modes.availableModes`. The count is 0 by default and when the key /// is absent. available: usize = 0, /// A minified JSON copy of the `modes` object. The slice is owned, and empty by default. raw: []u8 = &.{}, /// A caller uses this function to free the modes once the caller is done with them. The /// function frees the two owned strings with the given allocator and leaves the value /// undefined. The call is safe on the default value, which owns zero allocations. pub fn deinit(self: *Modes, allocator: Allocator) void { if (self.current.len != 0) allocator.free(self.current); if (self.raw.len != 0) allocator.free(self.raw); self.* = undefined; } /// A caller uses this function to read the session's modes from a parsed `session/new` /// response, as `Client.start` does. The function reads a parsed JSON-RPC response and returns /// the modes in its result. The function returns null when the result lacks a `modes` object or /// that object lacks a string `currentModeId`. The function fails with /// `error.AgentProtocolError` when the value is a non-object, carries an `error` member, or /// lacks an object `result`. The call copies the strings it keeps with the allocator, and the /// caller frees them with `deinit`. pub fn fromResponse(allocator: Allocator, value: std.json.Value) !?Modes { const result = try responseResult(value); const modes = json.objectObject(result, "modes") orelse return null; const current = json.objectString(modes, "currentModeId") orelse return null; const owned_current = try allocator.dupe(u8, current); errdefer allocator.free(owned_current); const raw = try rawJsonAlloc(allocator, modes); errdefer if (raw.len != 0) allocator.free(raw); return .{ .current = owned_current, .available = arrayLength(modes, "availableModes"), .raw = raw, }; }};/// A caller uses this structure to put an image or an audio clip in a prompt. The structure holds/// an image or an audio clip for a prompt: its MIME type, its data, and an optional URI. The client/// writes it as a block of type `image` or `audio` with `mimeType`, `data`, and `uri` when one is/// set. The strings are borrowed and read only while the prompt is written.pub const PromptMedia = struct { /// The media's MIME type, sent as `mimeType`. The field is required. mime_type: []const u8, /// The media's content, sent unchanged as the JSON string `data`. The field is required. data: []const u8, /// The media's URI, sent as `uri`. The field is empty by default, and an empty URI is left out. uri: []const u8 = "",};/// A caller uses this structure to put a file's text in the prompt, such as source code the agent/// should read. The structure holds a text resource embedded in a prompt: its URI, its text, and an/// optional MIME type. The client writes it as a block of type `resource` whose `resource` object/// holds `uri`, `text`, and `mimeType` when one is set.pub const PromptTextResource = struct { /// The resource's URI, sent as `resource.uri`. The field is required. uri: []const u8, /// The resource's text, sent as `resource.text`. The field is required. text: []const u8, /// The resource's MIME type, sent as `resource.mimeType`. The field is empty by default, and an /// empty value is left out. mime_type: []const u8 = "",};/// A caller uses this structure to put a resource's content in the prompt as a blob string. The/// structure holds a blob resource embedded in a prompt: its URI, its content as a string, and an/// optional MIME type. The client writes it as a block of type `resource` whose `resource` object/// holds `uri`, `blob`, and `mimeType` when one is set.pub const PromptBlobResource = struct { /// The resource's URI, sent as `resource.uri`. The field is required. uri: []const u8, /// The resource's content, sent unchanged as the JSON string `resource.blob`. The field is /// required. blob: []const u8, /// The resource's MIME type, sent as `resource.mimeType`. The field is empty by default, and an /// empty value is left out. mime_type: []const u8 = "",};/// A caller uses this structure to point the agent at a resource by URI without putting its content/// in the prompt. The structure holds a link to a resource: a URI and a name, with an optional MIME/// type, title, description and size. The client writes it as a block of type `resource_link` with/// each field that is set. Empty strings and a null size are left out.pub const PromptResourceLink = struct { /// The resource's URI, sent as `uri`. The field is required. uri: []const u8, /// The resource's name, sent as `name`. The field is required. name: []const u8, /// The resource's MIME type, sent as `mimeType`. The field is empty by default, and an empty /// value is left out. mime_type: []const u8 = "", /// A title for the resource, sent as `title`. The field is empty by default, and an empty value /// is left out. title: []const u8 = "", /// A description of the resource, sent as `description`. The field is empty by default, and an /// empty value is left out. description: []const u8 = "", /// The resource's size, sent as the number `size`. The field is null by default, and a null /// size is left out. size: ?u64 = null,};/// A prompt is a list of these blocks, so a caller can mix text with files, links and media in one/// prompt. The union holds one block of prompt content, of one of six kinds./// `Client.promptDetailedContent` sends a list of blocks in order. `Client.prompt` and/// `Client.promptDetailed` send a single text block. The client writes every kind as given, without/// checking the capability flags in `Initialize`, so the caller checks them.pub const PromptContent = union(enum) { /// Plain text, written as a block of type `text` with the string in `text`. text: []const u8, /// An image, written as a block of type `image`. image: PromptMedia, /// An audio clip, written as a block of type `audio`. audio: PromptMedia, /// A text resource embedded in the prompt, written as a block of type `resource`. resource_text: PromptTextResource, /// A blob resource embedded in the prompt, written as a block of type `resource`. resource_blob: PromptBlobResource, /// A link to a resource, written as a block of type `resource_link`. resource_link: PromptResourceLink,};/// The kind of one progress report (session update), read from the report's `sessionUpdate` name,/// so the caller switches on the kind of each progress report the agent sends. Eleven tags carry/// the eleven names the package test replays, one each, and every other name maps to `other`.pub const UpdateKind = enum { /// The name `user_message_chunk`: a piece of a user message, with its text in `content.text`. user_message_chunk, /// The name `agent_message_chunk`: a piece of the agent's reply, with its text in /// `content.text`. This tag is the only kind whose text the client adds to the reply. agent_message_chunk, /// The name `agent_thought_chunk`: a piece of the agent's thinking, with its text in /// `content.text`. agent_thought_chunk, /// The name `tool_call`: a new tool call, with `toolCallId`, `title`, `kind` and `status`. tool_call, /// The name `tool_call_update`: a change to a tool call, with its `toolCallId` and new /// `status`. tool_call_update, /// The name `plan`: the agent's plan, a list of `entries` each with `content` and `status`. plan, /// The name `available_commands_update`: the commands the agent offers, in `availableCommands`. available_commands_update, /// The name `current_mode_update`: the session's mode changed, with the new id in /// `currentModeId`. current_mode_update, /// The name `config_option_update`: a change to the agent's `configOptions` list. config_option_update, /// The name `session_info_update`: session details such as `title` and `updatedAt`. session_info_update, /// The name `usage_update`: usage figures in `used` and `size`. usage_update, /// Any other name, which stays in `Update.name`. other,};/// One `session/update` notification from the agent, with the two numbers the client gave it, the/// exact line, and fields read from it, received by the observer so the caller records and inspects/// the agent's progress. The client builds one structure per notification, hands it to the/// observer, and frees it when the callback returns. The structure owns every `[]u8` field, and/// `deinit` frees them. A field the notification lacks is empty, null or 0.pub const Update = struct { /// The caller's number for the client that received the notification. transport_epoch: u64, /// The notification's place among all the notifications the client has received, counting /// from 1. update_sequence: u64, /// The exact line the agent sent, trimmed of surrounding whitespace. The client trims spaces, /// tabs, carriage returns and newlines from both ends of the line. This slice is borrowed from /// the reader's buffer and remains valid only until the observer's callback returns. The /// `deinit` function leaves it alone. envelope: []const u8, /// The kind read from `sessionUpdate`. kind: UpdateKind, /// The `sessionUpdate` string as the agent sent it. The update owns this field. name: []u8, /// The notification's `params.sessionId`, empty when absent. The update owns this field. session_id: []u8, /// A summary text for the notification. For an `agent_message_chunk`, this field carries the /// chunk's text, and the client adds it to the reply. The client picks the first of these the /// update carries: the content text, the title, the mode id, the first plan entry's content, /// the first command name, the usage as "used/size", and else the update's name. The update /// owns this field. text: []u8, /// A minified JSON copy of the `params.update` object. The update owns this field. raw: []u8, /// The update's `toolCallId`, empty when absent. The update owns this field. tool_call_id: []u8 = &.{}, /// The update's `status`, empty when absent. The update owns this field. status: []u8 = &.{}, /// The update's `kind`, the tool call's kind, empty when absent. The update owns this field. tool_kind: []u8 = &.{}, /// The update's `currentModeId`, empty when absent. The update owns this field. mode_id: []u8 = &.{}, /// The update's integer `used`, or null when absent. usage_used: ?i64 = null, /// The update's integer `size`, or null when absent. usage_size: ?i64 = null, /// The number of entries in the update's `entries` list, 0 when absent. plan_entries: usize = 0, /// The number of entries in the update's `availableCommands` list, 0 when absent. available_commands: usize = 0, /// The number of entries in the update's `configOptions` list, 0 when absent. config_options: usize = 0, /// The update's `title`, empty when absent. The update owns this field. title: []u8 = &.{}, /// The update's `updatedAt`, empty when absent. The update owns this field. updated_at: []u8 = &.{}, /// Frees every owned string with the given allocator and leaves the value undefined, so the /// caller frees an update built with `fromSessionNotification`. The call leaves the borrowed /// line alone. pub fn deinit(self: *Update, allocator: Allocator) void { if (self.name.len != 0) allocator.free(self.name); if (self.session_id.len != 0) allocator.free(self.session_id); if (self.text.len != 0) allocator.free(self.text); if (self.raw.len != 0) allocator.free(self.raw); if (self.tool_call_id.len != 0) allocator.free(self.tool_call_id); if (self.status.len != 0) allocator.free(self.status); if (self.tool_kind.len != 0) allocator.free(self.tool_kind); if (self.mode_id.len != 0) allocator.free(self.mode_id); if (self.title.len != 0) allocator.free(self.title); if (self.updated_at.len != 0) allocator.free(self.updated_at); self.* = undefined; } /// Reads `params.update` from a parsed notification object, so the caller turns one parsed /// `session/update` notification into an `Update`, as the client does for each one it reads. /// The call returns null when `params`, `params.update` or its `sessionUpdate` string is /// missing, and the client then fails its call with `error.AgentProtocolError`. The function /// stores the given epoch, sequence and line, and keeps the line by reference. The call copies /// every string it keeps with the allocator, and the caller frees them with `deinit`. The /// function fails with `error.OutOfMemory` and leaves nothing allocated when a copy fails. The /// call reads the object as given and does not look at `method`, because the client checks the /// method name before it calls this. pub fn fromSessionNotification( allocator: Allocator, object: std.json.ObjectMap, transport_epoch: u64, update_sequence: u64, envelope: []const u8, ) !?Update { const params = json.objectObject(object, "params") orelse return null; const update = json.objectObject(params, "update") orelse return null; const name = json.objectString(update, "sessionUpdate") orelse return null; const session_id = json.objectString(params, "sessionId") orelse ""; const owned_name = try allocator.dupe(u8, name); errdefer allocator.free(owned_name); const owned_session = try allocator.dupe(u8, session_id); errdefer allocator.free(owned_session); const text = try summaryTextAlloc(allocator, update, name); errdefer if (text.len != 0) allocator.free(text); const raw = try rawJsonAlloc(allocator, update); errdefer if (raw.len != 0) allocator.free(raw); const tool_call_id = try optionalStringAlloc(allocator, update, "toolCallId"); errdefer if (tool_call_id.len != 0) allocator.free(tool_call_id); const status = try optionalStringAlloc(allocator, update, "status"); errdefer if (status.len != 0) allocator.free(status); const tool_kind = try optionalStringAlloc(allocator, update, "kind"); errdefer if (tool_kind.len != 0) allocator.free(tool_kind); const mode_id = try optionalStringAlloc(allocator, update, "currentModeId"); errdefer if (mode_id.len != 0) allocator.free(mode_id); const title = try optionalStringAlloc(allocator, update, "title"); errdefer if (title.len != 0) allocator.free(title); const updated_at = try optionalStringAlloc(allocator, update, "updatedAt"); errdefer if (updated_at.len != 0) allocator.free(updated_at); return .{ .transport_epoch = transport_epoch, .update_sequence = update_sequence, .envelope = envelope, .kind = kindFromName(name), .name = owned_name, .session_id = owned_session, .text = text, .raw = raw, .tool_call_id = tool_call_id, .status = status, .tool_kind = tool_kind, .mode_id = mode_id, .usage_used = json.objectInteger(update, "used"), .usage_size = json.objectInteger(update, "size"), .plan_entries = arrayLength(update, "entries"), .available_commands = arrayLength(update, "availableCommands"), .config_options = arrayLength(update, "configOptions"), .title = title, .updated_at = updated_at, }; } /// Returns true for an `agent_message_chunk`, the only kind the client adds to a reply, so the /// caller tells whether an update carries reply text. pub fn assistantMessage(self: Update) bool { return self.kind == .agent_message_chunk; }};/// The reply limit a prompt reached.pub const OverflowKind = enum { /// The reply text would have passed `TransferLimits.response_bytes`. response_bytes, /// The number of chunks would have passed `TransferLimits.response_segment_count`. response_segments,};/// Records what a reply kept when one more chunk would have passed a limit, and which update the/// client left out, so the caller tells a cut reply from a whole one and sees where the reply/// stopped. The client sets this record at most once per prompt, and the reply stops taking text/// after it. `PromptResult.overflow` carries the record. The fields name the limit reached, the/// reply bytes and chunks kept before the limit, and the update sequence of the first chunk left/// out.pub const Overflow = struct { /// The limit the reply reached. kind: OverflowKind, /// The reply bytes kept before the limit. admitted_bytes: usize, /// The chunks kept before the limit. admitted_segments: usize, /// The update sequence of the first chunk left out of the reply. The observer still received /// that update. withheld_update_sequence: u64,};/// One `session/request_permission` request from the agent: the tool call it names, the decision,/// and the exact line. The caller records what the agent asked for and what was answered because/// the observer receives each permission request and its decision as one of these records. The/// client builds one twice per request: a preview for `permissionFn` with outcome `pending`, empty/// option fields, and an empty line, then the full record for `permissionCommitFn`. The record owns/// every `[]u8` field, and `deinit` frees them.pub const PermissionRequest = struct { /// The exact line the agent sent, trimmed of surrounding whitespace, and empty in the preview. /// The line is borrowed from the reader's buffer and remains valid only until /// `permissionCommitFn` returns. envelope: []const u8, /// The request's `params.sessionId`, empty when absent. session_id: []u8, /// The tool call's `toolCallId`, empty when absent. tool_call_id: []u8, /// The tool call's `title`, empty when absent. The permission policy matches its title prefixes /// against it. title: []u8, /// The tool call's `kind`, empty when the tool call omits a kind. tool_kind: []u8, /// The tool call's `status`, empty when absent. status: []u8, /// The id of the chosen option, empty when the outcome is `cancelled` and in the preview. option_id: []u8, /// The kind of the chosen option, such as `allow_once` or `reject_once`, empty when an option /// remains unchosen. option_kind: []u8, /// The decision: `selected`, `cancelled`, or `pending` in the preview. outcome: []u8, /// The number of options the agent offered. The field defaults to 0. options: usize = 0, /// A minified JSON copy of the request's `params` object. raw: []u8, /// Frees every owned string with the given allocator and leaves the value undefined for a /// record built with `fromClientRequest`. The call leaves the borrowed line alone. pub fn deinit(self: *PermissionRequest, allocator: Allocator) void { if (self.session_id.len != 0) allocator.free(self.session_id); if (self.tool_call_id.len != 0) allocator.free(self.tool_call_id); if (self.title.len != 0) allocator.free(self.title); if (self.tool_kind.len != 0) allocator.free(self.tool_kind); if (self.status.len != 0) allocator.free(self.status); if (self.option_id.len != 0) allocator.free(self.option_id); if (self.option_kind.len != 0) allocator.free(self.option_kind); if (self.outcome.len != 0) allocator.free(self.outcome); if (self.raw.len != 0) allocator.free(self.raw); self.* = undefined; } /// Turns one parsed permission request and its decision into a `PermissionRequest` by reading /// `params` and `params.toolCall` from a parsed request object and adding the given decision /// and line, as the client does for the preview and the record. The call fails with /// `error.AgentProtocolError` when `params` or `params.toolCall` is missing. The function /// copies every string it keeps with the allocator, keeps the line by reference, and the caller /// frees the rest with `deinit`. The call fails with `error.OutOfMemory` and leaves nothing /// allocated when a copy fails. pub fn fromClientRequest( allocator: Allocator, object: std.json.ObjectMap, envelope: []const u8, outcome: []const u8, option_id: []const u8, option_kind: []const u8, ) !PermissionRequest { const params = json.objectObject(object, "params") orelse return error.AgentProtocolError; const tool_call = json.objectObject(params, "toolCall") orelse return error.AgentProtocolError; const session_id = try optionalStringAlloc(allocator, params, "sessionId"); errdefer if (session_id.len != 0) allocator.free(session_id); const tool_call_id = try optionalStringAlloc(allocator, tool_call, "toolCallId"); errdefer if (tool_call_id.len != 0) allocator.free(tool_call_id); const title = try optionalStringAlloc(allocator, tool_call, "title"); errdefer if (title.len != 0) allocator.free(title); const tool_kind = try optionalStringAlloc(allocator, tool_call, "kind"); errdefer if (tool_kind.len != 0) allocator.free(tool_kind); const status = try optionalStringAlloc(allocator, tool_call, "status"); errdefer if (status.len != 0) allocator.free(status); const owned_option_id = try allocator.dupe(u8, option_id); errdefer allocator.free(owned_option_id); const owned_option_kind = try allocator.dupe(u8, option_kind); errdefer allocator.free(owned_option_kind); const owned_outcome = try allocator.dupe(u8, outcome); errdefer allocator.free(owned_outcome); const raw = try rawJsonAlloc(allocator, params); errdefer if (raw.len != 0) allocator.free(raw); return .{ .envelope = envelope, .session_id = session_id, .tool_call_id = tool_call_id, .title = title, .tool_kind = tool_kind, .status = status, .option_id = owned_option_id, .option_kind = owned_option_kind, .outcome = owned_outcome, .options = arrayLength(params, "options"), .raw = raw, }; }};/// The caller gets the reply with the reason the agent stopped and whether the reply was cut short/// because `Client.promptDetailed` and `Client.promptDetailedContent` return one of these results./// The result holds the reply text, the stop reason, and an optional overflow record. The result/// owns the reply and the stop reason, and `deinit` frees them.pub const PromptResult = struct { /// The reply: the text of the agent's message chunks, joined in arrival order, within the reply /// limits. The result owns this slice. response: []u8, /// The `stopReason` string from the prompt's response, such as `end_turn` or `cancelled`. The /// result owns this slice. stop_reason: []u8, /// The overflow record set when the reply reached a limit, and null otherwise. overflow: ?Overflow = null, /// Frees the reply and the stop reason with the given allocator and leaves the value undefined /// once the caller is done with a prompt result. The call takes the client's allocator. pub fn deinit(self: *PromptResult, allocator: Allocator) void { if (self.response.len != 0) allocator.free(self.response); if (self.stop_reason.len != 0) allocator.free(self.stop_reason); self.* = undefined; }};fn responseResult(value: std.json.Value) !std.json.ObjectMap { const object = json.getObject(value) orelse return error.AgentProtocolError; if (object.get("error") != null) return error.AgentProtocolError; const result = object.get("result") orelse return error.AgentProtocolError; return json.getObject(result) orelse return error.AgentProtocolError;}fn arrayLength(object: std.json.ObjectMap, key: []const u8) usize { const value = object.get(key) orelse return 0; return switch (value) { .array => |array| array.items.len, else => 0, };}fn kindFromName(name: []const u8) UpdateKind { if (std.mem.eql(u8, name, "user_message_chunk")) return .user_message_chunk; if (std.mem.eql(u8, name, "agent_message_chunk")) return .agent_message_chunk; if (std.mem.eql(u8, name, "agent_thought_chunk")) return .agent_thought_chunk; if (std.mem.eql(u8, name, "tool_call")) return .tool_call; if (std.mem.eql(u8, name, "tool_call_update")) return .tool_call_update; if (std.mem.eql(u8, name, "plan")) return .plan; if (std.mem.eql(u8, name, "available_commands_update")) return .available_commands_update; if (std.mem.eql(u8, name, "current_mode_update")) return .current_mode_update; if (std.mem.eql(u8, name, "config_option_update")) return .config_option_update; if (std.mem.eql(u8, name, "session_info_update")) return .session_info_update; if (std.mem.eql(u8, name, "usage_update")) return .usage_update; return .other;}fn summaryTextAlloc(allocator: Allocator, update: std.json.ObjectMap, fallback: []const u8) ![]u8 { if (contentText(update)) |text| return try allocator.dupe(u8, text); if (json.objectString(update, "title")) |title| return try allocator.dupe(u8, title); if (json.objectString(update, "currentModeId")) |mode| return try allocator.dupe(u8, mode); if (planText(update)) |text| return try allocator.dupe(u8, text); if (commandText(update)) |text| return try allocator.dupe(u8, text); if (try usageTextAlloc(allocator, update)) |text| return text; return try allocator.dupe(u8, fallback);}fn contentText(update: std.json.ObjectMap) ?[]const u8 { const content = json.objectObject(update, "content") orelse return null; return json.objectString(content, "text");}fn planText(update: std.json.ObjectMap) ?[]const u8 { const entries_value = update.get("entries") orelse return null; const entries = switch (entries_value) { .array => |array| array, else => return null, }; if (entries.items.len == 0) return null; const first = json.getObject(entries.items[0]) orelse return null; return json.objectString(first, "content");}fn commandText(update: std.json.ObjectMap) ?[]const u8 { const commands_value = update.get("availableCommands") orelse return null; const commands = switch (commands_value) { .array => |array| array, else => return null, }; if (commands.items.len == 0) return null; const first = json.getObject(commands.items[0]) orelse return null; return json.objectString(first, "name");}fn usageTextAlloc(allocator: Allocator, update: std.json.ObjectMap) Allocator.Error!?[]u8 { const used = json.objectInteger(update, "used") orelse return null; const size = json.objectInteger(update, "size") orelse return null; return try std.fmt.allocPrint(allocator, "{d}/{d}", .{ used, size });}fn optionalStringAlloc(allocator: Allocator, object: std.json.ObjectMap, key: []const u8) Allocator.Error![]u8 { const value = json.objectString(object, key) orelse return &.{}; return try allocator.dupe(u8, value);}fn authMethodIdsAlloc(allocator: Allocator, object: std.json.ObjectMap) ![]u8 { const value = object.get("authMethods") orelse return &.{}; const methods = switch (value) { .array => |array| array, else => return &.{}, }; if (methods.items.len == 0) return &.{}; var out: std.Io.Writer.Allocating = .init(allocator); errdefer out.deinit(); var writer = pretty_json.Writer.init(&out.writer, .minified); const ids = try writer.array(); var written: usize = 0; for (methods.items) |item| { const method = json.getObject(item) orelse continue; const id = json.objectString(method, "id") orelse continue; try ids.element(id); written += 1; } if (written == 0) { out.deinit(); return &.{}; } try ids.end(); return try out.toOwnedSlice();}fn rawJsonAlloc(allocator: Allocator, update: std.json.ObjectMap) ![]u8 { var out: std.Io.Writer.Allocating = .init(allocator); errdefer out.deinit(); try pretty_json.writeMinified(&out.writer, std.json.Value{ .object = update }); return try out.toOwnedSlice();}test "protocol parses session update metadata" { const testing = std.testing; const envelope = \\{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"s1","update":{"sessionUpdate":"tool_call_update","toolCallId":"tc1","title":"read file","kind":"read","status":"completed","content":[]}}} ; var parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, envelope, .{}); defer parsed.deinit(); const object = json.getObject(parsed.value).?; var update = (try Update.fromSessionNotification(testing.allocator, object, 4, 7, envelope)).?; defer update.deinit(testing.allocator); try testing.expectEqual(UpdateKind.tool_call_update, update.kind); try testing.expectEqualStrings("tc1", update.tool_call_id); try testing.expectEqualStrings("completed", update.status); try testing.expectEqualStrings("read", update.tool_kind); try testing.expectEqual(@as(u64, 4), update.transport_epoch); try testing.expectEqual(@as(u64, 7), update.update_sequence); try testing.expectEqualStrings(envelope, update.envelope);}test "protocol parses session update summaries" { const testing = std.testing; const envelope = \\{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"s1","update":{"sessionUpdate":"plan","entries":[{"content":"inspect","status":"pending"}]}}} ; var parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, envelope, .{}); defer parsed.deinit(); const object = json.getObject(parsed.value).?; var update = (try Update.fromSessionNotification(testing.allocator, object, 1, 1, envelope)).?; defer update.deinit(testing.allocator); try testing.expectEqual(UpdateKind.plan, update.kind); try testing.expectEqualStrings("plan", update.name); try testing.expectEqualStrings("s1", update.session_id); try testing.expectEqualStrings("inspect", update.text); try testing.expect(std.mem.indexOf(u8, update.raw, "\"sessionUpdate\":\"plan\"") != null);}test "protocol parses initialize capabilities" { const testing = std.testing; var parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, \\{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"audio":true,"image":true,"embeddedContext":true}},"authMethods":[{"id":"oauth\"\n"},{"id":"token"}]}} , .{}); defer parsed.deinit(); var initialize = try Initialize.fromResponse(testing.allocator, parsed.value); defer initialize.deinit(testing.allocator); try testing.expectEqual(@as(i64, 1), initialize.protocol_version); try testing.expect(initialize.load_session); try testing.expect(initialize.prompt_audio); try testing.expect(initialize.prompt_image); try testing.expect(initialize.prompt_embedded_content); try testing.expectEqual(@as(usize, 2), initialize.auth_methods); try testing.expectEqualStrings("[\"oauth\\\"\\n\",\"token\"]", initialize.auth_method_ids); try testing.expect(std.mem.indexOf(u8, initialize.raw, "\"loadSession\":true") != null);}test "protocol parses session modes" { const testing = std.testing; var parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, \\{"jsonrpc":"2.0","id":1,"result":{"sessionId":"s1","modes":{"currentModeId":"plan","availableModes":[{"id":"plan","name":"Plan"},{"id":"act","name":"Act"}]}}} , .{}); defer parsed.deinit(); var modes = (try Modes.fromResponse(testing.allocator, parsed.value)).?; defer modes.deinit(testing.allocator); try testing.expectEqualStrings("plan", modes.current); try testing.expectEqual(@as(usize, 2), modes.available); try testing.expect(std.mem.indexOf(u8, modes.raw, "\"currentModeId\":\"plan\"") != null);}Source: lib/acp/src/root.zig:63
zig
pub const protocol = @import("protocol.zig");Audit
| Definitions | 1 |
|---|---|
| Public names | 1 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |