Hello World: Chat Loop
Build a working console chatbot in under 2 minutes.
Step 1 — Install the package
dotnet add package HPD.AgentStep 2 — Create the agent
There are three ways to create an agent. Pick the one that fits your situation.
Builder (quickest)
using HPD.Agent;
var agent = await new AgentBuilder()
.WithProvider("anthropic", "claude-sonnet-4-5")
.WithInstructions("You are a helpful assistant.")
.BuildAsync();Good for scripts, prototypes, and simple apps. Swap the provider key and model name to use any supported provider.
Authentication
The framework resolves your API key automatically in this order:
Environment variable — recommended for development
bashexport ANTHROPIC_API_KEY="sk-ant-..."No code change needed — the framework picks it up automatically.
appsettings.json— recommended for productionjson{ "anthropic": { "ApiKey": "sk-ant-..." } }Explicit in the builder — for testing only, never commit this
csharp.WithAnthropic(model: "claude-sonnet-4-5", apiKey: "sk-ant-...")
Use provider-specific builder methods like
.WithAnthropic()or.WithOpenAI()when you need authentication options or provider-specific settings..WithProvider()works for the simple case.
Config object
var config = new AgentConfig
{
Name = "MyAgent",
SystemInstructions = "You are a helpful assistant.",
Provider = new ProviderConfig
{
ProviderKey = "anthropic",
ModelName = "claude-sonnet-4-5"
}
};
var agent = await config.BuildAsync();Good when you want to store or share configuration as data — serialize it, load it from a database, pass it around.
JSON file
{
"Name": "MyAgent",
"SystemInstructions": "You are a helpful assistant.",
"Provider": {
"ProviderKey": "anthropic",
"ModelName": "claude-sonnet-4-5"
}
}var agent = await AgentConfig.BuildFromFileAsync("agent-config.json");Good for production apps where configuration lives outside the code.
All three produce the same agent — this cookbook uses the builder for brevity.
Step 3 — Create a session
// Let the framework generate a GUID
var sessionId = await agent.CreateSessionAsync();
// Or provide your own ID — useful when you already have a user/conversation ID
var sessionId = await agent.CreateSessionAsync("user-123");A session holds the conversation history. Every message you send and every response the agent gives is stored under this ID — without it, the agent has no memory between turns. Always capture the return value and pass it to every RunAsync() call.
CreateSessionAsync throws if the session ID already exists, so session creation is always intentional.
Step 4 — The chat loop
while (true)
{
Console.Write("You: ");
var input = Console.ReadLine();
if (string.IsNullOrEmpty(input)) break;
Console.Write("Agent: ");
await foreach (var evt in agent.RunAsync(input, sessionId: sessionId))
{
// handle events
}
}agent.RunAsync() returns an IAsyncEnumerable<AgentEvent> — events stream in real time as the agent works. The loop continues until the user presses Enter on an empty line.
Step 5 — Handle events
This is where you choose how much detail to show. Start minimal, add granularity as needed.
Minimal — just the essentials
All you need to display a response and know when it's done:
switch (evt)
{
case TextDeltaEvent delta:
Console.Write(delta.Text);
break;
case MessageTurnFinishedEvent:
Console.WriteLine("\n");
break;
}Full granularity — the complete lifecycle
Every event type, with start and end boundaries for each phase:
switch (evt)
{
// ── Turn lifecycle (start) ───────────────────────────────
// Fires once when the agent begins processing the message
case MessageTurnStartedEvent:
Console.WriteLine("[Turn started]");
break;
// ── Reasoning ───────────────────────────────────────────
// Only fires on models with extended thinking enabled
case ReasoningMessageStartEvent:
Console.Write("[Thinking: ");
break;
case ReasoningDeltaEvent reasoning:
Console.Write(reasoning.Text);
break;
case ReasoningMessageEndEvent:
Console.WriteLine("]");
break;
// ── Text ────────────────────────────────────────────────
// Wraps the streaming text response
case TextMessageStartEvent:
Console.WriteLine("[Message started]");
break;
case TextDeltaEvent delta:
Console.Write(delta.Text);
break;
case TextMessageEndEvent:
Console.WriteLine("[Message ended]");
break;
// ── Tool calls ──────────────────────────────────────────
// Fires when the agent calls a function
case ToolCallStartEvent toolStart:
Console.WriteLine($"\n[Calling: {toolStart.Name}]");
break;
case ToolCallResultEvent toolResult:
Console.WriteLine($"[Result: {toolResult.Result}]");
break;
// ── Turn lifecycle (end) ─────────────────────────────────
// MessageTurnFinishedEvent = agent is fully done
// Don't use AgentTurnFinishedEvent — that fires after each
// internal LLM call, not at the end of the full response
case MessageTurnFinishedEvent:
Console.WriteLine("\n[Turn finished]");
break;
}Event order in a typical turn:
MessageTurnStartedEvent
ReasoningMessageStartEvent
ReasoningDeltaEvent (×N)
ReasoningMessageEndEvent
TextMessageStartEvent
TextDeltaEvent (×N)
TextMessageEndEvent
ToolCallStartEvent
ToolCallResultEvent
TextMessageStartEvent ← agent may emit more text after tool result
TextDeltaEvent (×N)
TextMessageEndEvent
MessageTurnFinishedEventThis covers the most common events. HPD-Agent emits 50+ event types in total — including permission requests, clarifications, structured output, streaming control, and full observability events. See the Event Types Reference for the complete list.
Complete program
using HPD.Agent;
var agent = await new AgentBuilder()
.WithProvider("anthropic", "claude-sonnet-4-5")
.WithInstructions("You are a helpful assistant.")
.BuildAsync();
var sessionId = await agent.CreateSessionAsync();
while (true)
{
Console.Write("You: ");
var input = Console.ReadLine();
if (string.IsNullOrEmpty(input)) break;
Console.Write("Agent: ");
await foreach (var evt in agent.RunAsync(input, sessionId: sessionId))
{
switch (evt)
{
case MessageTurnStartedEvent:
Console.WriteLine("[Turn started]");
break;
case ReasoningMessageStartEvent:
Console.Write("[Thinking: ");
break;
case ReasoningDeltaEvent reasoning:
Console.Write(reasoning.Text);
break;
case ReasoningMessageEndEvent:
Console.WriteLine("]");
break;
case TextMessageStartEvent:
Console.WriteLine("[Message started]");
break;
case TextDeltaEvent delta:
Console.Write(delta.Text);
break;
case TextMessageEndEvent:
Console.WriteLine("[Message ended]");
break;
case ToolCallStartEvent toolStart:
Console.WriteLine($"\n[Calling: {toolStart.Name}]");
break;
case ToolCallResultEvent toolResult:
Console.WriteLine($"[Result: {toolResult.Result}]");
break;
case MessageTurnFinishedEvent:
Console.WriteLine("\n[Turn finished]");
break;
}
}
}Run it
dotnet run