DOCUMENTATION

ply-agent.h
Agent Harness

ply-agent.h defines a C++ API for interacting with AI agents. Applications create Transcript objects and pass them to Agent objects; the agent's job is to extend the transcript in a logical way. It does this by communicating with a remote inference server and running tools in the local filesystem.

ply::Agent ply::Transcript Application LocalFilesystem RemoteInference Server Authorizer

The steps for creating and running a Plywood agent are as follows:

  1. Create a new Transcript object containing the user's prompt.
  2. Create a new Agent object, passing in the Transcript, the desired inference provider and a set of tools for the agent to use. The agent runs in a background thread.
  3. Receive Transcript::Event objects back from the Agent.
  4. As each event comes in, call applyTranscriptEvent and perform any application-specific handling.
  5. Once Agent::isWorking() returns false, no further events will be received and the Agent can be safely destroyed.

To send a followup prompt, create a new Transcript object, assign the previous Transcript as its parent, then create another Agent object.

Using ply-agent.h requires linking your project with libcurl for HTTPS support. Instructions for installing libcurl can be found in the documentation for building and running the agent sample.

Agent

The Agent class represents a live conversation with an agent running in a background thread. The background thread executes tool calls automatically and buffers Transcript::Event objects for the application to consume.

A working agent can be cancelled by any thread at any time by calling cancel(). Agent objects can be destroyed at any time as long as there are no racing member function calls from other threads.

Agent::Agent(const Agent::Settings& settings)

Constructor. The agent starts running in a background thread. Agent::Settings has the following data members:

const Transcript* startTranscript The transcript used to start the agent.
Agent::EndPoint endPoint Identifies the inference provider, protocol and model.
Agent::Capabilities capabilities Specifies the system prompt, working directory and available tools.
bool enableRawLog Enables raw HTTP-level logging (for debug purposes). Default is false.

Agent::Capabilities has the following data members:

String systemPrompt The system prompt passed to the agent.
Set<Owned<ToolDefinition>> tools The tools available to the agent.
String workingDir The agent's working directory.
Array<String> readableDirs Absolute directory paths granting recursive read access.
Array<String> writableDirs Absolute directory paths granting recursive write access. These paths should also be present in readableDirs.
Array<Transcript::Event> Agent::pollForEvents()

Returns all currently buffered events without waiting, or an empty array if no events are buffered. Only one thread is allowed to call pollForEvents, waitForEvents or waitForCompletion at a time.

Array<Transcript::Event> Agent::waitForEvents(s32 maxTimeInMillis)

Waits until at least one event is available, then returns all buffered events. A negative argument waits indefinitely. Only one thread is allowed to call pollForEvents, waitForEvents or waitForCompletion at a time.

Array<Transcript::Event> Agent::waitForCompletion(s32 maxTimeInMillis)

Waits until the agent stops working or the time limit is reached, then returns all buffered events. A negative argument waits indefinitely. Only one thread is allowed to call pollForEvents, waitForEvents or waitForCompletion at a time.

bool Agent::isWorking()

If true, the agent can still return more events. false means the agent has finished running, the buffer is empty and no more events will arrive. This function can be called by any thread at any time.

void Agent::cancel()

Stops running the agent. No new events will be generated after this function returns, but any events already buffered remain available for consumption. This function can be called by any thread at any time. If another thread is waiting inside waitForEvents or waitForCompletion, that thread will immediately return.

If cancel is called while a tool call is running in the background, the tool call might not stop immediately. Tool calls can continue running briefly after cancel returns, but they'll be stopped as soon as possible and won't generate any further Transcript::Events.

Transcript

A transcript consists of a sequence of turns, with each turn consisting of a sequence of messages. These concepts are represented by the Transcript, Transcript::Turn and Transcript::Message classes.

Each message in the transcript is associated with a Transcript::Role, which can take any of the following values:

User
AgentThinking
Agent
ToolCall
Error

Every Transcript object holds a reference to a parent Transcript object, allowing you to link them together into a graph. Each node in this graph corresponds to a followup prompt sent by the user.

Transcript::Event

Transcript changes are received as a stream of Transcript::Event objects. The agent never modifies the original Transcript object directly; instead, the application must call applyTranscriptEvent for each event it receives.

void applyTranscriptEvent(Transcript* transcript, const Transcript::Event& event)

Modifies transcript by applying the given event.

Applications are free to perform additional application-specific handling in response to each event. To facilitate this, Transcript::Event exposes the following data members:

s64 timeStamp The time when the event was created, expressed as a Unix timestamp in microseconds.
Operation operation The kind of change represented by the event.
Transcript::Role role The role of the message started by BeginMessage. Unused by other operations.
u32 toolCallID The index of a tool call within the current transcript.
String providerToolCallID The inference provider's identifier for a tool call. Used internally.
String text The content carried by AppendText, AppendToolResponse or AppendProviderOutputItem events.

Transcript::Event::Operation can have any of the following values:

BeginTurn Appends a new turn for an inference request.
BeginMessage Starts a message with the specified role, finalizing the preceding message if necessary.
AppendText Appends text to the current message.
AppendToolResponse Appends text to the response for the tool call identified by toolCallID.
EndToolResponse Finalizes the response for the tool call identified by toolCallID.
AppendProviderOutputItem Preserves an opaque provider output item for use when replaying the transcript as context.
SetTokenUsage Stores aggregate token usage on the current turn.
EndTurn Finalizes the current message after an inference request completes.

For every inference request that completes or reports an error, the agent emits EndTurn. If tool calls require another inference request, the agent then emits BeginTurn before any events belonging to that request. A canceled inference is incomplete and does not receive EndTurn.

Tools

To give an agent access to tools in the local filesystem, fill in the capabilities.tools member of Agent::Settings before creating the agent. Several built-in tools are available and custom tools can be easily created.

Built-In Tools

Built-in tools can be created by calling any of the following functions. Each function returns an Owned<ToolDefinition> that can be added to the capabilities.tools member of Agent::Settings.

Function name Tool name Description
createShellTool shell Runs a command using the system shell after authorization. Not available on iOS.
createReadTool read Reads part or all of a file.
createWriteTool write Creates or overwrites a file.
createListDirTool list_dir Lists the contents of a directory.
createFindInFilesTool find_in_files Searches for text in a directory tree.
createEditTool edit Edits a file using exact text replacements.

All built-in tools except shell are designed to respect the directory permissions outlined in Agent::Capabilities. Custom tools must implement their own permission checks. shell tool permissions are implemented by passing a callback to createShellTool, as described in the Tool Monitoring section.

Creating Custom Tools

In addition to the built-in tools, applications are free to create their own tools that integrate more closely with agents. To do so, simply fill in the public data members of ToolDefinition as appropriate.

String name The name of the tool, as presented to the agent.
String description A description that tells the agent when and how to use the tool.
Array<Parameter> parameters Describes the JSON parameters accepted by the tool.
Functor<...> handler The internal callback invoked when the agent uses the tool.
bool readOnly Indicates that the tool does not modify any data.

ToolContext

Each time a tool handler is invoked, it receives a ToolContext object. ToolContext provides the following member functions:

StringView ToolContext::getWorkingDirectory() const

Returns the agent's working directory.

String ToolContext::checkPathPermission(StringView path, bool withWriteAccess) const

Converts path to an absolute path if read access is permitted, or write access if withWriteAccess is true. Otherwise returns an empty string.

void ToolContext::appendResponse(Transcript::Message* toolCall, StringView text)

Adds text to the tool response in a thread-safe manner. Can be called more than once to stream a response. Each call to appendResponse creates a new Transcript::Event and buffers it in the Agent so that the application receives it as soon as possible. The complete tool response won't be sent to the remote inference server until the next turn.

bool ToolContext::isCanceled() const

Returns whether the agent has been canceled. Long-running tools should call this periodically and return promptly when it becomes true.

bool ToolContext::setCancelCallback(Functor<void()>&& callback)

If the agent hasn't already been canceled, registers a cancellation callback and returns true. Otherwise, if the agent was already canceled, clears any existing cancellation callback and returns false. A tool that performs an interruptible blocking operation can use setCancelCallback() to register a callback that unblocks it. The callback will be invoked from the client thread when Agent::cancel() is called.

void ToolContext::clearCancelCallback()

Clears the registered cancellation callback. The callback must be cleared before the tool handler returns.

For example, a tool to count the number of bytes in a string can be implemented as follows.

void byteCountToolHandler(ToolContext* toolCtx, Transcript::Message* toolCall,
                          const json::Node& arguments) {
    // Validate the argument.
    const json::Node& textArg = arguments.get("text");
    if (!textArg.isText()) {
        toolCtx->appendResponse(toolCall, "Error: 'text' argument is required.");
        return;
    }

    // Add response text to the transcript.
    toolCtx->appendResponse(toolCall, String::format("{} bytes", textArg.text().numBytes()));
}

void addByteCountTool(Agent::Capabilities* capabilities) {
    // Describe the tool and its arguments.
    Owned<ToolDefinition> tool = Heap::create<ToolDefinition>();
    tool->name = "byte_count";
    tool->description = "Return the length of a string in bytes.";
    ToolDefinition::Parameter& textParam = tool->parameters.append();
    textParam.name = "text";
    textParam.description = "Text to measure";
    textParam.type = "string";
    textParam.required = true;
    tool->handler = byteCountToolHandler;
    capabilities->tools.insertItem(std::move(tool));
}

Tool Monitoring

The shell tool is dangerous because it gives agents the ability to run arbitrary shell commands on the user's behalf. At the same time, it's a convenient way to give agents access to system applications, such as compilers and debuggers, so that they can automate large tasks end-to-end.

In Plywood, every shell request made by the agent is first passed to an application-defined callback, which must be specified when createShellTool is called:

Owned<ToolDefinition> createShellTool(Functor<bool(ToolContext* toolCtx, StringView shellCommand)>&& authorizer)

The authorizer callback lets the application decide whether each shell command is allowed to run. If the callback returns true, the shell command executes normally; if it returns false, permission is denied and an error is reported back to the agent.

The application responds to each tool request according to application-defined policies. If a shell command looks ambiguous, the application can even notify the user and await a response before returning from the callback. In addition, each tool request is automatically logged to the conversation transcript.

shell tool monitoring should be seen as the first line of defense against AI agents performing unwanted actions. In environments where stronger security guarantees are needed, the application should be sandboxed and monitored at the operating system level.

ShellAuthorizationPolicy

One way to implement the authorizer callback is to use a second agent to automatically review the requests made by the first agent. For convenience, Plywood provides a ShellAuthorizationPolicy helper class to support this case. It has the following public data members:

String policy A natural-language description of shell commands the agent is allowed to run.
Agent::EndPoint authorizerEndPoint The authorizer's provider, protocol and model.
Set<Owned<ToolDefinition>> authorizerTools The tools available to the authorizer.
Functor<void(Agent*)> loggingHook Optional hook for logging the authorizer's transcript. Must consume events from the provided agent until the agent finishes.

To run an authorizer agent, initialize a ShellAuthorizationPolicy object and call authorizeCommand().

bool ShellAuthorizationPolicy::authorizeCommand(ToolContext* toolCtx, StringView shellCommand) const

For an example of ShellAuthorizationPolicy being used in practice, see the agent sample.

ShellAuthorizationPolicy shellAuthorizationPolicy;

shellAuthorizationPolicy.policy = ...;
shellAuthorizationPolicy.authorizerEndPoint = ...;
shellAuthorizationPolicy.authorizerTools = ...;

Owned<ToolDefinition> shellTool = createShellTool(
    [policy = std::move(shellAuthorizationPolicy)](ToolContext* toolCtx,
                                                   StringView shellCommand) {
        return policy.authorizeCommand(toolCtx, shellCommand);
    });