Skip to documentation
SLOP

tiny.acp.protocol

Reference 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.

No direct callersNo direct callstiny.acpprotocol
Static calls · unresolved targets: unknown · external targets: unknown.

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

Definitions1
Public names1
Members0
Version26.7.0
Revisiondaab053ee433