Make Your Delphi App Agent-Ready

· Components
Make Your Delphi App Agent-Ready with TsgcAIAgent

Asking a language model a question is the easy part. An agent goes further. The model decides which of your functions to call, your application runs them, the results go back to the model, and the loop continues until the model has an answer. Writing that loop is only the start. A shipped application also has to keep the model away from what it must not touch, keep personal data on the machine, survive the day a provider retires a model, and know what each user costs.

sgcWebSockets 2026.11 adds the pieces for all of that to the sgcAI components, for Delphi 7 to 13:

The Agent Loop in One Component

Give the agent a configured TsgcAIChat, describe a tool and call Run. The agent sends the prompt with the tool definitions, runs every tool the model asks for, sends the results back and repeats until the model answers in plain text. Run returns that answer.

oChat := TsgcAIChat.Create(nil);
oChat.Provider := aicpAnthropic;
oChat.ChatOptions.ApiKey := 'API_KEY';
oChat.ChatOptions.Model := 'claude-opus-5-5';

oAgent := TsgcAIAgent.Create(nil);
oAgent.Chat := oChat;

oTool := oAgent.Tools.Add('get_weather', 'Current weather of a city');
oTool.AddParam('city', aatpString, 'City name');
oTool.ReadOnly := True;
oTool.OnExecute := DoGetWeather;

Memo1.Lines.Add(oAgent.Run('Do I need an umbrella in Paris today?'));

The tool receives its arguments as the JSON object the model wrote, and returns its result as text, usually JSON:

procedure TForm1.DoGetWeather(Sender: TObject; const aArguments: string;
  var aResult: string);
begin
  // aArguments is a JSON object, for example {"city":"Paris"}
  aResult := '{"city":"Paris","celsius":18,"sky":"rain"}';
end;

The agent works on top of TsgcAIChat.ChatWithTools, which now supports tool calling on every provider. Switching from a hosted model to a local Ollama model is a change of Provider and Model, and the tools stay the same. If you prefer to run the loop yourself, ChatWithTools and AddToolResult are public too.

Control the Loop

Options.MaxSteps caps the number of model requests in one run, Cancel stops a run from a button, a timer or another thread, and StopReason tells you why the run ended. When an answer is cut by the token limit, the run stops with aasrMaxTokens and the tool calls of that answer are not run, because their arguments may be incomplete. OnStep reports every step as it happens, so the user can see what the agent is doing.

oAgent.Options.MaxSteps := 5;
oAgent.OnStep := oAgentStep;
vAnswer := oAgent.Run('Plan a three day trip to Rome');
case oAgent.StopReason of
  aasrCompleted:
    Memo1.Lines.Add(vAnswer);
  aasrMaxSteps:
    Memo1.Lines.Add('Stopped after 5 model requests');
  aasrMaxTokens:
    Memo1.Lines.Add('Cut by the token limit (' + oChat.LastFinishReason + ')');
  aasrCancelled:
    Memo1.Lines.Add('Cancelled');
end;
procedure TForm1.oAgentStep(Sender: TObject; const aStep: TsgcAIAgentStep);
begin
  case aStep.Kind of
    aaskToolCall:
      Memo1.Lines.Add('Calling ' + aStep.ToolName + ' ' + aStep.Arguments);
    aaskToolResult, aaskToolDenied:
      Memo1.Lines.Add(aStep.ToolName + ': ' + aStep.Content);
    aaskFinal:
      Memo1.Lines.Add('Answer: ' + aStep.Content);
  end;
end;

A tool that fails does not end the run. Its exception goes back to the model as an error text, and the model can try something else. An error of the chat itself, a network failure for example, ends the run with aasrError and the exception reaches your code. OnUsage reports the tokens of every model request of the run.

Your Delphi Methods as Tools

A tool is a name, a description, typed parameters and a handler, as above. When you already have a method that does the job, AddMethod publishes it as it is. The tool name is the method name, and the parameters, their types and the result are read with extended RTTI, so this one needs Delphi 2010 or newer.

function TForm1.Forecast(const aCity: string; aDays: Integer): string;
begin
  Result := Format('%s: sunny for %d days', [aCity, aDays]);
end;
oAgent.Tools.AddMethod(Self, 'Forecast',
  'Weather forecast of a city for the next days');

For a tool with a richer input, set InputSchema to your own JSON schema. Enabled takes a tool out of the next runs without removing it.

Tools From Your MCP Servers

Add one or more MCP clients to MCPClients and the agent offers the tools of those servers next to your own. They are listed once per run and called through the client when the model asks for them. A tool the server marks with readOnlyHint counts as read only, and when a local tool and a server tool share a name, the local one wins.

oMCP := TsgcWSAPIClient_MCP.Create(nil);
oMCP.MCPOptions.HttpOptions.URL := 'http://127.0.0.1:8080/mcp';
oAgent.MCPClients.Add(oMCP);

Memo1.Lines.Add(oAgent.Run('List the support tickets opened today'));

Approval Before Anything Changes

Every tool has a ReadOnly flag. Read only tools run as soon as the model asks for them. A tool that changes data, sends a mail or spends money stays not read only, and the agent asks OnToolApproval before it runs, with the tool name and the exact arguments the model chose.

oTool := oAgent.Tools.Add('send_reminder', 'Sends a payment reminder');
oTool.AddParam('invoice', aatpString, 'Invoice number');
oTool.OnExecute := DoSendReminder; // ReadOnly is False: it changes data

oAgent.OnToolApproval := oAgentToolApproval;
procedure TForm1.oAgentToolApproval(Sender: TObject; const aToolName,
  aArguments: string; var aApproved: Boolean);
begin
  aApproved := MessageDlg('Run ' + aToolName + ' with ' + aArguments + '?',
    mtConfirmation, [mbYes, mbNo], 0) = mrYes;
end;

A refused tool is not an error. The model is told that the tool was not approved and can answer the user without it. With guardrails assigned, as in the next section, a tool that needs approval and finds no handler is refused, so a forgotten event never lets a write tool through.

Guardrails

TsgcAIGuardrails is a separate component that you assign to the chat, so the same rules apply to the agent and to plain chat requests. It has three jobs.

oGuard := TsgcAIGuardrails.Create(nil);
// tools: reading is allowed, deleting never
oGuard.AllowedTools.Add('get_*');
oGuard.AllowedTools.Add('list_*');
oGuard.AllowedTools.Add('send_reminder');
oGuard.DeniedTools.Add('*delete*');
oGuard.ToolApproval := aigaNotReadOnly;
// personal data never leaves the application
oGuard.PII.Enabled := True;
oGuard.PII.CustomPatterns.Add('EmployeeId=EMP-####');
// structured answers must match the schema
oGuard.Schema.Enabled := True;
oGuard.Schema.JSONSchema := '{"type":"object","properties":' +
  '{"price":{"type":"number","minimum":0}},"required":["price"]}';

oChat.Guardrails := oGuard;

You can call the redaction yourself to see what the provider will receive, and what your application gets back:

vText := oGuard.Redact('Mail bob@example.com, call +34 600 111 222, ' +
  'employee EMP-1234');
// Mail [EMAIL_1], call [PHONE_1], employee [EMPLOYEEID_1]
vText := oGuard.Restore(vText);
// Mail bob@example.com, call +34 600 111 222, employee EMP-1234

Pin the Tools of Your MCP Servers

The description and the input schema of an MCP tool are read by the model as instructions. A server that changes them after you trusted it, because it was compromised or because its owner changed it, can hide new instructions in a tool you already use. This is known as tool poisoning.

With tool pinning, TsgcWSAPIClient_MCP keeps a SHA-256 fingerprint of the name, description, input schema and annotations of every tool the first time it lists it. When a later list returns a different definition, the tool is blocked. It is removed from the list, the agent never offers it, and a call to it raises an exception before anything is sent. OnMCPToolChanged lets you accept the change or keep the tool blocked for review, and the other tools of the server keep working.

oMCP.MCPOptions.ToolPinning.Enabled := True;
oMCP.MCPOptions.ToolPinning.FileName := 'mcp_pins.json';
oMCP.OnMCPToolChanged := oMCPMCPToolChanged;
procedure TForm1.oMCPMCPToolChanged(Sender: TObject; const aToolName,
  aOldHash, aNewHash: string; var aAccept: Boolean);
begin
  // the definition of a trusted tool changed: keep it blocked
  // until somebody reviews it, then call oMCP.PinTool(aToolName)
  Memo1.Lines.Add('Tool ' + aToolName + ' changed, blocked for review');
  aAccept := False;
end;

A Retired Model Will Not Stop a Shipped Exe

Model names change faster than installed applications. A model name compiled into an exe stops working the day the provider retires it, and every customer needs a new build. With model aliases, the code asks for smart or fast, and the alias decides the model and, if you want, the provider. The list loads from a file, from JSON or from a URL, so moving to a new model is a change on your server, not a new release.

oChat.ModelAliases.Add('fast=gpt-6-luna');
oChat.ModelAliases.Add('smart=anthropic:claude-opus-5-5');
oChat.ProviderApiKeys.Add('anthropic=ANTHROPIC_KEY');
oChat.ChatOptions.Model := 'smart';

// or keep the list on your server and change it without a new build
oChat.LoadModelAliasesFromURL('https://config.example.com/ai/models.json');

Fallbacks cover outages. When the provider fails with a connection error, an HTTP 5xx, an HTTP 429 or a model it does not know, the same request goes to the next provider and model of the list. The conversation history, the tools and JSON answers carry over, and OnFallback tells you that it happened. A local Ollama model makes a good last resort.

oChat.Provider := aicpOpenAI;
oChat.ChatOptions.ApiKey := 'OPENAI_KEY';
oChat.ChatOptions.Model := 'gpt-6-luna';

oFallback := oChat.Fallbacks.Add;
oFallback.Provider := aicpGemini;
oFallback.Model := 'gemini-3-flash';
oFallback.ApiKey := 'GEMINI_KEY';

oFallback := oChat.Fallbacks.Add;
oFallback.Provider := aicpOllama;
oFallback.Model := 'llama3.1';

oChat.OnFallback := oChatFallback;
procedure TForm1.oChatFallback(Sender: TObject; const aFromProvider,
  aFromModel, aToProvider, aToModel, aError: string);
begin
  Memo1.Lines.Add(aFromProvider + ' ' + aFromModel + ' failed (' + aError +
    '), trying ' + aToProvider + ' ' + aToModel);
end;

Know What Every Request Costs

TsgcAIUsageMeter records the tokens of every request, cached input tokens included, prices them with the price list you give it, and keeps totals per user and per session. Assign it to the chat and set UserId and SessionId. Because the meter sits on the chat, every model request of an agent run is counted too.

oMeter := TsgcAIUsageMeter.Create(nil);
// example prices per million tokens, use the ones of your contract
oPrice := oMeter.Prices.Add;
oPrice.Provider := 'openai';
oPrice.Model := 'gpt-6*';
oPrice.InputPerMillion := 1.25;
oPrice.OutputPerMillion := 10;
oPrice.CachedPerMillion := 0.125;

oChat.UserId := 'user-1';
oChat.SessionId := 's-42';
oChat.UsageMeter := oMeter;
oChat.Chat('Hello');

Memo1.Lines.Add(Format('user-1 spent %.6f %s and %d tokens',
  [oMeter.TotalCost('user-1'), oMeter.Currency, oMeter.TotalTokens('user-1')]));

The price list matches provider and model with wildcards and can be loaded from JSON or from a file, so a price change does not need a new build either. Budgets turn the meter into a limit. A budget covers the whole application, each user or each session, for a day, a month or in total, in money or in tokens. When a budget is used up, the next request is refused before it is sent, so a budget can also stop an agent run half way.

// every user can spend 2 USD a day
oBudget := oMeter.Budgets.Add;
oBudget.Scope := aubsUser;
oBudget.Period := aubpDay;
oBudget.MaxCost := 2;
// the whole application can spend 500 USD a month
oBudget := oMeter.Budgets.Add;
oBudget.Scope := aubsGlobal;
oBudget.Period := aubpMonth;
oBudget.MaxCost := 500;
try
  Memo1.Lines.Add(oAgent.Run('Remind every customer with an unpaid invoice'));
except
  on E: EsgcAIChat do
    // Usage budget "each user, day" for user-1 exceeded: ...
    Memo1.Lines.Add(E.Message);
end;

Set BlockWhenExceeded to False for a budget that only raises OnBudgetExceeded as an alert. For reports, the records go to a CSV file or to any TDataSet. With OpenTelemetry enabled, every request becomes a span that follows the GenAI semantic conventions and goes to your OTLP/HTTP collector in the background, without slowing the chat down.

oMeter.OpenTelemetry.Endpoint := 'http://localhost:4318';
oMeter.OpenTelemetry.ServiceName := 'invoice-assistant';
oMeter.OpenTelemetry.Enabled := True;

oMeter.SaveToCSV('usage.csv');

The Demo: an Invoice Assistant

The new Delphi demo in Demos\15.AI\02.Applications\11.AIAgent is an assistant for a list of unpaid invoices. The agent has three tools. list_unpaid_invoices and get_invoice are read only, while send_reminder changes data and asks for your approval first. The agent runs in a background thread, so the window stays responsive while it works. The demo also lets you try the rest of this post: denied tools and PII redaction, a budget per session with a live token and cost counter and a CSV export, model aliases, a fallback provider, and an MCP server whose tools are pinned.

Docs Your Coding Agent Can Read

A coding agent writes good code only for the libraries it knows. Every component of sgcAI, sgcHTML, sgcCrypto and the other sgcWebSockets packs now has its own Markdown page for language models, 224 pages in all. Each page says what the component does and links its product page, and most list its key properties, methods and events. The page of TsgcAIAgent, ai-agent.md, also has a complete usage sample. llms.txt points Claude Code, Copilot, Cursor and other agents to these pages, so the agent reads the real API before it writes a line.

The sgcWebSockets agent skills learn the new components too. The AI skill of 2026.11.0 has ready patterns for the agent loop, tool approval, guardrails, model aliases with fallbacks and the usage meter, and every pattern comes from code that compiles. AI Agent Skills for Delphi, C++Builder and .NET explains how to install them.

Availability

TsgcAIAgent, TsgcAIGuardrails, TsgcAIUsageMeter, the model aliases and fallbacks of TsgcAIChat and MCP tool pinning arrive in sgcWebSockets 2026.11.0, for Delphi 7 to 13 and C++Builder. They are part of the sgcAI components, included in the Enterprise and All-Access editions and in the standalone sgcAI product. The help documents every property, method and event, with pages on the agent loop, on tools, on aliases and fallbacks, on PII redaction, on schema validation and on tool pinning.

Read more on the component pages of TsgcAIAgent, TsgcAIGuardrails and TsgcAIUsageMeter.

Questions or feedback? Get in touch. You will get a reply from the people who wrote the code.