Building a Parrot Agent with BeaconForge PHP
In this short guide, we will build a simple parrot agent together. The parrot agent will simply repeat everything you send it with a small 🦜 emoji in front of the return. We will create the Open Floor Protocol-compliant agent with the help of BeaconForge PHP (bfPHP).
TL;DR: The GitHub repository of the framework.
Initial Setup
BeaconForge PHP requires no package manager, no build step, and no dependencies. If your host can run PHP, it can run this agent.
You only need four files, two of which you will never touch:
| File | Edit? | Purpose |
|---|---|---|
run.php |
Rarely | HTTP entry point |
beaconforgeV3.php |
Never | Core framework engine |
agDef.json |
Yes | Your agent's identity and capabilities |
myAgFun.php |
Yes | Your agent's conversational logic |
Download all four files from the bfPHP directory and place them together in a folder on your server. A suggested structure:
public/parrot/
├── run.php
├── beaconforgeV3.php
├── agDef.json
└── myAgFun.php
Your agent's endpoint will be:
https://yourdomain.com/parrot/run.php
Now that the basic setup is done, let us start coding together!
Step 1: Configuring the Entry Point
Open run.php. You will find three lines you may want to configure:
$agentFunctionsFileName = 'myAgFun.php'; // Your agent logic file
$agentDefinitionJSON = 'agDef.json'; // Your agent manifest file
$pathForPersistantStorage = ''; // Where to store conversation state
For development, you can leave all three as-is. For production, it is strongly recommended to store conversation state outside your public web directory so those files are not directly web-accessible:
// Example: if your public directory is at /home/username/public_html/
// store persistent data alongside it at /home/username/agentdata/
$pathForPersistantStorage = '../../../../agentdata/';
The framework will automatically create a subdirectory there named after your agent (e.g., agentdata/Parrot/). Make sure the path exists and is writable by PHP. That is all you need to change in run.php.
Step 2: Defining the Agent Manifest (agDef.json)
The agDef.json file is your agent's identity card. The framework reads it once at startup and sends it to floor managers whenever a getManifests event arrives.
Step 2.1: The top-level structure
Your agDef.json has three sections nested under a manifest key:
{
"manifest": {
"identification": { },
"character": { },
"capabilities": { }
}
}
Step 2.2: The identification section
This section is mandatory and defines the core identity of your agent. It is used by floor managers and discovery agents to find and address your agent.
"identification": {
"serviceUrl": "https://yourdomain.com/parrot/run.php",
"speakerUri": "tag:yourdomain.com,2026:parrot",
"conversationalName": "Parrot",
"role": "Echo Specialist",
"department": "Demo",
"organization": "OpenFloor Demo Corp",
"synopsis": "A simple parrot agent that echoes back messages with a 🦜 emoji"
}
Why these fields?
speakerUri— The permanent identity of your agent. Use atag:URI. This never changes, even if you move the agent to a new URL.serviceUrl— Where the agent lives right now. This must exactly match the hosted URL of yourrun.phpfile.conversationalName— The name your agent introduces itself by. It also doubles as the persistent storage subdirectory name, and the framework automatically sets$directedToMe = truewhenever this name appears in an utterance.
Step 2.3: The character section
This is a BeaconForge-specific extension (not part of the core OFP spec) that hints to compatible clients how to visually render and voice your agent:
"character": {
"headShot": "aParrot.png",
"voice": {
"vendor": "MS_EDGE",
"name": "Zira",
"uri": "Microsoft Zira - English (United States)"
}
}
Step 2.4: The capabilities section
This section is mandatory. Floor managers and discovery agents use it to decide whether to route a task to your agent.
"capabilities": {
"keyphrases": ["echo", "repeat", "parrot", "say"],
"languages": ["en-us"],
"descriptions": [
"Echoes back any text message with a 🦜 emoji",
"Repeats user input verbatim",
"Simple text mirroring functionality"
],
"supportedLayers": ["text", "voice"]
}
What each field does:
keyphrases— Short searchable words used by simple text-based discovery.descriptions— Full sentences used by AI-powered discovery agents.languages— IETF BCP 47 language tags your agent supports.supportedLayers— The dialog modalities your agent can handle (text,voice,ssml).
Step 2.5: The complete agDef.json
{
"manifest": {
"identification": {
"serviceUrl": "https://yourdomain.com/parrot/run.php",
"speakerUri": "tag:yourdomain.com,2026:parrot",
"conversationalName": "Parrot",
"role": "Echo Specialist",
"department": "Demo",
"organization": "OpenFloor Demo Corp",
"synopsis": "A simple parrot agent that echoes back messages with a 🦜 emoji"
},
"character": {
"headShot": "aParrot.png",
"voice": {
"vendor": "MS_EDGE",
"name": "Zira",
"uri": "Microsoft Zira - English (United States)"
}
},
"capabilities": {
"keyphrases": ["echo", "repeat", "parrot", "say"],
"languages": ["en-us"],
"descriptions": [
"Echoes back any text message with a 🦜 emoji",
"Repeats user input verbatim",
"Simple text mirroring functionality"
],
"supportedLayers": ["text", "voice"]
}
}
}
Step 3: Building the Parrot Agent (myAgFun.php)
This is where the magic happens. Open myAgFun.php — you will implement the agentFunctions class, which extends the framework's baseAgentFunctions.
Step 3.1: The class skeleton and persistent state
Start with the constructor and the two state lifecycle hooks. The parent:: calls are required — they wire up the framework's save/restore machinery:
<?php
class agentFunctions extends baseAgentFunctions {
public function __construct( $fileName ) {
parent::__construct( $fileName );
}
Now add startUpAction(). This runs at the beginning of every request and restores your state from disk:
public function startUpAction() {
parent::startUpAction(); // REQUIRED — restores $this->persistObject from disk
if ( $this->persistObject == null ) {
// First turn of a brand new conversation — initialize your state
$this->persistObject = [
'exchanges' => []
];
}
}
And wrapUpAction(), which runs at the end of every request and saves your state back to disk:
public function wrapUpAction() {
// Add any cleanup here before state is saved
parent::wrapUpAction(); // REQUIRED — saves $this->persistObject to disk
}
**What **$this->persistObject** gives you:**
This is a plain PHP array that the framework automatically serializes to a JSON file after every turn and restores at the start of the next. You can store anything JSON-serializable in it, like the conversation history, user preferences, counters, and so on.
Step 3.2: Responding to an invitation
The inviteAction() method is called when a floor manager invites your agent to join a conversation. Return the string you want your agent to say, or '' to join silently:
public function inviteAction( $reason ) {
return 'Hello! How can I help you today?';
}
Step 3.3: Implementing the parrot logic
utteranceAction() is the most important method. It is called every time anyone says anything in the conversation, that includes messages not directed at you.
Add the method signature and parameters:
public function utteranceAction( $heard, $fromUri, $directedToMe, $directedToSomeoneElse ) {
// $heard — [string] what was said
// $fromUri — [string] the speakerUri of who said it
// $directedToMe — [bool] true if explicitly addressed to this agent,
// OR if conversationalName appears in the utterance
// $directedToSomeoneElse — [bool] true if explicitly addressed to a different agent
Now handle the three cases. First, if the utterance is directed at our agent this is where we parrot:
if ( $directedToMe ) {
// Combine the original text and add the 🦜 emoji prefix
$parrotText = '🦜 ' . $heard;
$say = $parrotText;
The parroting logic:
- We simply prepend the
🦜emoji to whatever was said.
Next, if it was directed to someone else we stay silent:
} elseif ( $directedToSomeoneElse ) {
$say = ''; // Stay silent — it was not meant for us
Finally, handle undirected (broadcast) utterances:
} else {
// Undirected utterance — we heard it but are not sure if it was for us
$say = '';
}
To finish the method, save the exchange to persistent state and return the response:
// Log the exchange across conversation turns
$this->persistObject['exchanges'][] = [
'heard' => $heard,
'said' => $say
];
return $say;
}
}
?>
Step 3.4: The complete myAgFun.php
<?php
class agentFunctions extends baseAgentFunctions {
public function __construct( $fileName ) {
parent::__construct( $fileName );
}
public function startUpAction() {
parent::startUpAction();
if ( $this->persistObject == null ) {
$this->persistObject = ['exchanges' => []];
}
}
public function wrapUpAction() {
parent::wrapUpAction();
}
public function inviteAction( $reason ) {
return 'Hello! How can I help you today?';
}
public function utteranceAction( $heard, $fromUri, $directedToMe, $directedToSomeoneElse ) {
if ( $directedToMe ) {
$say = '🦜 ' . $heard;
} elseif ( $directedToSomeoneElse ) {
$say = '';
} else {
$say = '';
}
$this->persistObject['exchanges'][] = [
'heard' => $heard,
'said' => $say
];
return $say;
}
}
?>
Step 4: Test Your Implementation
Upload all four files to your server and send a test request. You can use our playground or curl to POST directly to your run.php endpoint.
Test an utterance directed at the parrot:
{
"openFloor": {
"conversation": { "id": "test_convo_001" },
"sender": {
"speakerUri": "ofpSocketClient",
"serviceUrl": "https://testclient.example.com"
},
"events": [
{
"eventType": "utterance",
"parameters": {
"dialogEvent": {
"features": {
"text": {
"tokens": [
{ "value": "Hello Parrot, how are you?" }
]
}
}
}
}
}
]
}
}
The agent should respond with 🦜 Hello Parrot, how are you?
Test an invite:
{
"openFloor": {
"conversation": { "id": "test_convo_001" },
"sender": {
"speakerUri": "floorManager",
"serviceUrl": "https://floormgr.example.com"
},
"events": [
{
"eventType": "invite",
"to": { "speakerUri": "tag:yourdomain.com,2026:parrot" }
}
]
}
}
Request the manifest:
{
"openFloor": {
"conversation": { "id": "test_convo_001" },
"sender": {
"speakerUri": "ofpSocketClient",
"serviceUrl": "https://testclient.example.com"
},
"events": [
{
"eventType": "getManifests",
"to": { "speakerUri": "tag:yourdomain.com,2026:parrot" }
}
]
}
}
You can also use the simple single-file chat UI from azettl/openfloor-js-chat to test your agent locally.
How It Works (Under the Hood)
Here is the full request lifecycle so you can see how the framework and your code interact:
HTTP POST → run.php
│
▼
simpleProcessOFP() in beaconforgeV3.php
│
├─ Loads agDef.json, instantiates agentFunctions (myAgFun.php)
├─ Restores $this->persistObject from disk ← startUpAction()
├─ Ignores messages sent by this agent itself
│
├─ For each OFP event in the request:
│ ├─ "invite" → inviteAction() → acceptInvite + utterance
│ ├─ "utterance" → utteranceAction() → utterance response
│ └─ "getManifests" → reads agDef.json → publishManifest
│
├─ Saves $this->persistObject to disk ← wrapUpAction()
└─ Returns complete OFP JSON response
The framework handles all OFP envelope formatting, sender/recipient routing, manifest serialization, and HTTP response construction. Your myAgFun.php code only ever deals with plain PHP strings, arrays, and simple booleans.