Your First Agent
This guide walks you through creating and running a minimal custom agent using the agenticstar-platform SDK. The SDK provides infrastructure (DB, events, storage, etc.), while you freely design the agent logic.
Prerequisites
- Python 3.12 or higher
- pip or uv
- PostgreSQL database (local or Azure Database for PostgreSQL)
- Azure OpenAI API key (for LLM calls)
Step 1: Install the SDK
- All Modules
- Minimal
- Using uv
pip install agenticstar-platform[all]==3.0.15
# Events + DB only (minimal configuration)
pip install agenticstar-platform[db]==3.0.15
uv pip install agenticstar-platform[all]==3.0.15
Step 2: Create the Configuration File
Create a config.toml file. Each SDK module reads its configuration from this file.
[database]
host = "localhost"
port = 5432
database = "agenticstar"
username = "agent_user"
password = "your_password"
pool_min_size = 2
pool_max_size = 10
# Set to true only when using Azure AD auth, and configure [database.azure_ad]
use_azure_ad = false
[events]
enable_db_handler = true
enable_webhook_handler = false
PostgreSQLConfig.from_toml() reads the [database] section by default. The field names are username / pool_min_size / pool_max_size. To use Azure AD auth, set use_azure_ad = true directly under [database] and provide tenant_id / client_id / client_secret in [database.azure_ad].
Step 3: Implement the Agent
Create my_agent.py. This example implements a simple agent that retrieves job information using the SDK's DB module and streams progress via the Events module.
import asyncio
from openai import AsyncAzureOpenAI
from agenticstar_platform.db import PostgreSQLManager, PostgreSQLConfig, DataAccess
from agenticstar_platform.db.execution_access import ExecutionAccess
from agenticstar_platform.events import EventEmitter, EventType
# --- Configuration ---
db_config = PostgreSQLConfig.from_toml("config.toml")
llm_client = AsyncAzureOpenAI(
azure_endpoint="https://your-endpoint.openai.azure.com/",
api_version="2024-12-01-preview",
api_key="your-api-key",
)
async def run_agent(execution_id: str, user_message: str):
"""Minimal agent execution flow"""
# 1. Initialize DB connection
db_manager = PostgreSQLManager(db_config)
da = DataAccess(db_manager)
await da.initialize()
execution_access = ExecutionAccess(da)
# 2. Initialize EventEmitter
emitter = EventEmitter(execution_id=execution_id)
# 3. Emit execution start event
await emitter.emit_event(
event_type=EventType.PHASE_START,
message="Processing request...",
metadata={"phase": "processing"},
)
# 4. Retrieve past messages
history = await execution_access.get_messages(execution_id=execution_id)
# 5. Call LLM (agent logic is freely designed)
messages = [{"role": "system", "content": "You are a helpful assistant."}]
if history:
for h in history:
messages.append({"role": h["role"], "content": h["content"]})
messages.append({"role": "user", "content": user_message})
response = await llm_client.chat.completions.create(
model="gpt-4.1",
messages=messages,
)
answer = response.choices[0].message.content
# 6. Emit completion event
await emitter.emit_event(
event_type=EventType.COMPLETION_SUCCESS,
message=answer,
)
# 7. Cleanup
await emitter.cleanup()
await da.close()
return answer
if __name__ == "__main__":
result = asyncio.run(run_agent("exec-001", "Hello!"))
print(result)
Step 4: Verify
python my_agent.py
If everything works correctly, the LLM response will be printed to the console.
Run Marketplace-Compatible (runner, SDK 0.5.30+)
An agent function that works locally can run under the marketplace-compatible
terminal lifecycle just by passing it to run_marketplace_agent. The runner
takes care of receiving and validating identity (EXECUTION_ID etc.), fetching
the input message, persisting the result to the DB, webhook notification, the
terminal event (exactly once, no matter what happens), and cleanup — so the
main boilerplate you wrote in Step 3 above is no longer needed.
from agenticstar_platform import run_marketplace_agent
async def my_agent(emitter, message: str) -> str:
"""Your agent logic. Any framework (LangChain / OpenAI Agents SDK / custom)."""
return message.upper() # replace with your actual logic
if __name__ == "__main__":
run_marketplace_agent(my_agent)
pip install 'agenticstar-platform[runner]==3.0.15'
python marketplace_agent.py
On the platform, EXECUTION_ID / CONVERSATION_ID / USER_ID / MESSAGE_ID
are injected by the marketplace executor at pod startup. Configure the DB
connection (DB_HOST etc., following the PostgreSQLConfig.from_env()
contract) and WEBHOOK_URL as agent environment variables at registration
time. If a required variable is missing, the runner stops with
MarketplaceRunnerConfigError without invoking your agent.
- The agent function's return value is persisted and notified as
completion_success - Exceptions from the agent function converge to
completion_failure(traceback goes to logs only) - Agent functions that emit their own terminal event (like the local sample) do not cause duplicates
- For cycle executions (
mp-chat/1, conversations started by a project cycle on a schedule or by a webhook),PROJECT_ID(the ID of that project) is injected in addition to the variables above. For each enabled connection of the project, the OAuth token is passed asASTER_CONNECTION_<SERVICE>_TOKEN(<SERVICE>is the service name of the connection in upper case, with-replaced by_; for example,slack_mcp→ASTER_CONNECTION_SLACK_MCP_TOKEN). The execution does not start when more than 8 connections are enabled, when two connections of the same service are enabled, or when this variable name collides with an environment variable from the agent registration or the placement. Tokens of connections that are disconnected, or expired and cannot be refreshed, are not passed (an expired token is passed if it can be refreshed), so make your agent handle the case where the variable is missing. Your agent looks up the connection URLs from the settings itself (for example,get_mcp_configurations(execution_mode="pod")) - For cycle executions (
mp-chat/1, conversations the platform starts on a schedule), from v3.0.12 the runner takes a started fence in the database before calling your agent. If the pod is recreated and the same execution starts again, your agent is not called and the runner raisesMarketplaceRunnerFenceTaken(the platform closes the execution as failed with unknown effects). The fence uses the direct PostgreSQL pool (PostgreSQLManager.pool); with adata_accessthat has no pool, passstarted_fence=False(a recreated pod may then run your agent again)
Exit the process after a cycle execution
A cycle execution (mp-chat/1) runs in a Pod created for that single execution (restartPolicy: Never).
The platform decides that an execution has finished by looking at the container exit code, so if the
process stays alive after your work is done, the execution never finishes.
While it never finishes, the next scheduled trigger for that placement keeps being skipped (the previous execution is still considered running).
Cycle executions use the same protocol as chat, so this happens if you reuse an example that keeps a server answering health checks.
If you pass your agent to run_marketplace_agent (above), the runner handles termination and you need
no extra work.
If you run your own server (FastAPI / uvicorn and the like), close the server and exit after the work completes. Keep answering health checks while the work is in progress, so close it only after you have confirmed completion.
import asyncio, os
import uvicorn
# Set at the end of your agent body to signal that the run is done
_run_finished = asyncio.Event()
def exit_after_run() -> bool:
"""A cycle execution runs once, so end the process when the run completes."""
return os.environ.get("AGENT_MODE", "").strip().lower() == "cycle"
async def serve(config: uvicorn.Config) -> None:
server = uvicorn.Server(config)
serving = asyncio.create_task(server.serve())
if exit_after_run():
finished = asyncio.create_task(_run_finished.wait())
# Do not wait for completion forever if the server ends first (startup failure, SIGTERM)
await asyncio.wait({serving, finished}, return_when=asyncio.FIRST_COMPLETED)
finished.cancel()
# Closing before startup finishes cancels the cleanup, so wait for `started`
for _ in range(100): # up to 5 seconds
if server.started or serving.done():
break
await asyncio.sleep(0.05)
server.should_exit = True
await serving
if __name__ == "__main__":
asyncio.run(serve(uvicorn.Config("main:app", host="0.0.0.0", port=int(os.environ.get("PORT", "8000")))))
- Switch on cycle mode with an environment variable set in the agent registration (for example
AGENT_MODE=cycle). If the same image also serves chat, keep the server resident for chat - Do not call the uvicorn APIs that prepare the loop (
Config.setup_event_loop/Config.get_loop_factory). They differ between versions, andsetup_event_loopwas removed in 0.36.0 - Exit with code 0 even when the work fails (the runner does this for you). Report what went wrong through completion reporting
Reporting completion for cycle executions (v3.0.14+)
A cycle execution whose agent finishes normally is recorded as "finished, completion not measured" and is not counted as a success, because a clean process exit is not treated as completion. To have it counted, your agent reports whether it achieved its goal with report_completion. Reporting is optional; agents that do not report behave as before.
import asyncio
import os
from agenticstar_platform import arun_marketplace_agent, report_completion
from agenticstar_platform.db import DataAccess, PostgreSQLConfig, PostgreSQLManager
async def main() -> None:
# build the DataAccess yourself and pass it to the runner, so that you can use its pool
manager = PostgreSQLManager(PostgreSQLConfig.from_env())
async with DataAccess(manager) as data_access:
async def my_agent(emitter, message: str) -> str:
reply = message.upper() # replace with your actual processing
await report_completion(
manager.pool,
user_id=os.environ["USER_ID"],
conversation_id=os.environ["CONVERSATION_ID"],
message_id=os.environ["MESSAGE_ID"],
execution_id=os.environ["EXECUTION_ID"],
outcome="completed", # e.g. "incomplete" if the goal was not achieved
had_mutating_success=False, # wrote something outside? None if unknown
)
return reply
await arun_marketplace_agent(my_agent, data_access=data_access)
if __name__ == "__main__":
asyncio.run(main())
- Choose
outcomefromcompleted/incomplete/failed/timeout/stopped/denied/skipped. What counts as completion is up to your agent - To give a reason, pass a short snake_case code as
reason(for exampleno_new_items) and put sentences insummary. The platform uses onlyoutcomeandreasonfor the execution's result - Report only once (a second call, even a concurrent one, writes nothing and returns
False). A failed database write does not raise; the execution just stays unmeasured - The platform adopts the report only when it sees the container exit 0 (not after a cancellation, the deadline passing while your agent runs, or a lost pod). The runner exits 0 even when your agent raises, so a report already written is adopted in that case too. Report once the result is final (at the end of your agent function)
- Calling it in a normal (chat) execution changes nothing. For the arguments and behavior, see the SDK API Reference
Local Integration Lab (SDK 0.5.35+)
Experience the full integration journey across DB / RAG / storage
(ingest → retrieve → artifact/result persist → terminal outcome) with a single
command — no external service contracts, no cloud credentials, no .env editing.
All you need is Docker (with compose v2) and Python 3.11+.
pip install 'agenticstar-platform[lab]==3.0.15'
python -m agenticstar_platform.lab # run the journey (one command)
python -m agenticstar_platform.lab doctor # read-only diagnostics
python -m agenticstar_platform.lab --reset # tear down (lists targets, confirms / --yes)
- Starts PostgreSQL / Qdrant / S3-compatible storage (MinIO) via a version-pinned Docker Compose manifest (bound to 127.0.0.1 only)
- Embeddings use a deterministic offline implementation — no cloud endpoint or API key is ever used
- The only credentials involved are fixed, non-production lab values; the lab never asks for, stores, or prints production credentials
- A local copy of the artifact is written to
agenticstar-lab-artifacts/under the current working directory - On success it shows the retrieved document, the artifact location, and the
next Marketplace runner command to try. If something fails,
doctorclassifies the cause (Docker not running / port conflict / missing extras etc.) with the next command to run
Step 5: Add Tools (Optional)
Here is an example of adding tools (function calling) to your agent. This uses the SDK's RAG module to search a knowledge base.
from agenticstar_platform.rag import QdrantManager, QdrantConfig
from agenticstar_platform.rag import EmbeddingGenerator, EmbeddingConfig
async def search_knowledge(query: str, top_k: int = 5) -> list[dict]:
"""Search the knowledge base for documents related to the query"""
# Initialize the embedding generator and the Qdrant manager
# QdrantManager requires an EmbeddingGenerator (it embeds the query internally at search time)
embed_config = EmbeddingConfig.from_toml("config.toml")
generator = EmbeddingGenerator(embed_config)
qdrant_config = QdrantConfig.from_toml("config.toml") # collection_name comes from config
qdrant = QdrantManager(qdrant_config, generator)
await qdrant.initialize()
# Search by query text (no query_vector needed; embedding is generated internally)
result = await qdrant.search(query_text=query, limit=top_k)
# The return value is a dict; hits are in result["data"]["results"]
return [
{"content": hit["payload"].get("content", ""), "score": hit["score"]}
for hit in result["data"]["results"]
]
Example Project Structure
my-agent/
├── config.toml # SDK configuration file
├── my_agent.py # Agent main logic
├── tools/
│ ├── search_knowledge.py # RAG search tool
│ └── file_manager.py # Storage operations tool
├── Dockerfile # Container build
├── requirements.txt
└── README.md
Next Steps
- Architecture Guide — SDK module structure and design philosophy
- Deploy Guide — Docker build and platform registration
- Events / Streaming Guide — SSE event delivery details
- SDK API Reference — Complete specifications for all modules and classes