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 local tools.
The steps for interacting with agents are as follows:
- Create a new
Transcriptobject containing the user's prompt. - Create a new
Agentobject, passing in theTranscript, the desired inference provider and a set of tools for the agent to use. The agent runs in a background thread. - Receive
TranscriptEventobjects back from theAgent. - As each event comes in, call
applyTranscriptEventand perform any application-specific handling. - Once
Agent::isWorking()returnsfalse, no further events will be received and theAgentcan 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.
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.
Agent
The Agent class represents an agent running in a background thread. As the agent runs, it generates TranscriptEvents, which are buffered internally until the application calls pollForEvents, waitForEvents or waitForCompletion. Only one thread is allowed to call pollForEvents, waitForEvents or waitForCompletion at a time.
You can destroy an Agent at any time as long as there are no racing member function calls from other threads. If the agent is still running at destruction time, it's immediately canceled.
Agent::Agent(const Agent::Settings& settings)Constructor. The agent starts running in a background thread.
Agent::Settingshas the following data members:const Transcript* startTranscriptThe transcript used to start the agent. EndPoint endPointIdentifies the inference provider, protocol and model. ToolSet toolSetSpecifies the system prompt, working directory and available tools. bool enableHttpLogEnables HTTP-level logging (for debugging purposes). Default is false.ToolSethas the following data members:String systemPromptThe system prompt passed to the agent. Set<Owned<Handler>> handlersThe tool handlers available to the agent. String workingDirectoryThe agent's working directory. Array<TranscriptEvent> Agent::pollForEvents()Returns all currently buffered events without waiting, or an empty array if no events are buffered.
Array<TranscriptEvent> Agent::waitForEvents(s32 maxTimeInMillis)Waits until at least one event is available, then returns all buffered events. A negative argument waits indefinitely.
Array<TranscriptEvent> 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.
bool Agent::isWorking()If
true, the agent can still return more events.falsemeans 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
waitForEventsorwaitForCompletion, that thread will immediately return.If
cancelis called while a tool call is running in the background, the tool call might not stop immediately. Tool calls can continue running briefly aftercancelreturns, but they'll be stopped as soon as possible and won't generate any furtherTranscriptEvents.
TranscriptEvent
Transcript changes are received as a stream of TranscriptEvent 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 TranscriptEvent& event)Modifies
transcriptby applying the givenevent.
Applications are free to perform additional application-specific handling in response to each event. To facilitate this, TranscriptEvent 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. |
TranscriptEvent::Operation can have any of the following values:
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. |
EndTurn |
Finalizes the current message and appends an empty turn for subsequent messages. |
Tools
The tools available to an agent are defined by filling in ToolSet::handlers.
Several built-in tools are available. To add them to a ToolSet, call any of the following functions. Each function returns a pointer a new ToolSet::Handler owned by the ToolSet.
| Function name | Tool name | Description |
|---|---|---|
addShellTool |
shell |
Runs a command using the system shell. Not available on iOS. |
addReadTool |
read |
Reads part or all of a file. |
addWriteTool |
write |
Creates or overwrites a file. |
addListDirTool |
list_dir |
Lists the contents of a directory. |
addFindInFilesTool |
find_in_files |
Searches for text in a directory tree. |
addEditTool |
edit |
Edits a file using exact text replacements. |
ToolSet::Handler has the following data members:
String name |
Tool name 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. |
Array<String> permittedDirectories |
Directories that the tool is permitted to access. |
When a tool handler is added, its permittedDirectories is initially empty. The application can add directories before creating the agent. All built-in tools currently enforce these permissions except the shell tool, which should be used with caution; ideally in a sandboxed environment. (Note: An auto-approve mode for the shell tool is planned.)
Defining Custom Tools
In addition to the built-in tools, applications are free to create their own tools to integrate more closely with agents. 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(ToolSet* toolSet) {
// Describe the tool and its arguments.
Owned<ToolSet::Handler> tool = Heap::create<ToolSet::Handler>();
tool->name = "byte_count";
tool->description = "Return the length of a string in bytes.";
ToolSet::Parameter& textParam = tool->parameters.append();
textParam.name = "text";
textParam.description = "Text to measure";
textParam.type = "string";
textParam.required = true;
tool->handler = byteCountToolHandler;
toolSet->handlers.insertItem(std::move(tool));
}
ToolContext
Each time a tool is invoked, it receives a ToolContext object. ToolContext provides the following member functions:
StringView ToolContext::getWorkingDirectory() constReturns the agent's working directory.
ArrayView<const String> ToolContext::getPermittedDirectories() constReturns the directories that the tool is permitted to access.
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
appendResponsecreates a newTranscriptEventand buffers it in theAgentso 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() constReturns whether the agent has been canceled.
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 returnsfalse. The callback will be invoked from the client thread whenAgent::cancel()is called.void ToolContext::clearCancelCallback()Clears the registered cancellation callback.
Long-running tools should call isCanceled() periodically and return promptly when it becomes true. A tool blocked in an interruptible operation can use setCancelCallback() to register a callback that unblocks it. The callback must be cleared before the tool handler returns.