AgentBridge is a .NET 10 web application that hosts the AIOrchestrator library (which is a library, not directly executable) and exposes its chat pipeline through standard OpenAI-compatible REST endpoints, plus a small set of documented proprietary extensions for the features that have no OpenAI equivalent (voice speech, LLM switching, platform capabilities). Any OpenAI SDK, script or standalone client — such as Giraffe AI — can drive the AI agents without modification.
- AIOffice uses the agents in-process (Blazor Server UI + Voice panel) — no HTTP layer needed.
- Standalone clients (a plain HTML file, a Python/curl script, an OpenAI SDK) have no
access to that in-process pipeline. AgentBridge gives them the same agents through
POST /v1/chat/completions, plus a file-upload flow for attachments, text-to-speech, speech recognition (Windows) and on-the-fly LLM switching.
Standalone client ──HTTP──▶ AgentBridge (this project)
│ hosts (references)
▼
AIOrchestrator (AgentHarness)
│
▼
LLM (DeepSeekBridge / DeepSeek / Z.ai / Gemini / Ollama / ExLlamaV2)
+ agent tools (WebTool, FileTool, DocumentTool, SpreadsheetTool, EMailTool)
+ Kokoro neural TTS (in-process) + VoiceAgent.Win (Windows STT)
| Mode | How |
|---|---|
| Terminal UI (default) | agent / dotnet run — the UI opens when the console is interactive |
| Server only | agent --headless (or --no-gui) — plain server console for scripts/CI |
| Force UI | agent --tui — falls back to server-only when the console is not interactive |
When the UI starts while another instance already owns the port, it connects to that instance instead of failing (useful to attach a UI to a running service).
Release and DEBUG builds are designed to coexist on the same machine. The installed
release keeps the documented default ports; a DEBUG build automatically shifts its own
defaults so the two never fight for the same resource. The shift happens in Program.cs
at configuration load and applies only when the effective value is still the release
default — an explicit override (CLI --Urls / --Sip:ListenPort, ASPNETCORE_URLS, a
hand-edited appsettings.json) always wins:
| Resource | Release default | DEBUG default | Where it lives |
|---|---|---|---|
HTTP server (Urls) |
http://localhost:5290 |
http://localhost:5291 |
AgentBridge Program.cs (config shift, #if DEBUG) |
SIP (Sip:ListenPort) |
5060 |
6071 |
AgentBridge Program.cs (config shift, #if DEBUG) |
| Puppet TCP (TUI automation tests) | — never compiled in | 5292 |
AgentBridge Program.cs (#if DEBUG listener) |
| WebTool CDP browser port | 9222 |
9223 |
AIOrchestrator API/WebTool.cs (CdpPort) |
| WebTool persistent browser profile | %TEMP%\aioffice_webtool_session |
%TEMP%\aioffice_webtool_session_9223 |
AIOrchestrator API/WebTool.cs |
What this means for the user-visible flows:
- A second instance no longer silently attaches to the running one (TUI mode, breakpoints
never fired) nor fails with a bind exception (
--headlessmode): each instance hosts its own server and the launching instance's TUI attaches to itself. - The clients the TUI launches follow the launching instance automatically, because they
derive the URL from the same
urlsconfiguration value that drives the bind (Tui.cs_serverUrl):/officemanageropens{serverUrl}/OfficeManager(served by that same server) and the Giraffe web client is told that instance's/v1/chat/completionsendpoint.
The IDE debug profiles below are no longer required (the DEBUG default already shifts), but they stay valid — explicit flags always win — and document the scheme:
| IDE | File | How it launches |
|---|---|---|
| Visual Studio | Properties/launchSettings.json |
F5 → applicationUrl: http://localhost:5291 + --Sip:ListenPort 6071 |
| VS Code | .vscode/launch.json + .vscode/tasks.json |
Run and Debug → "AgentBridge (Debug ports)" → --Urls http://localhost:5291 --Sip:ListenPort 6071 |
Manual equivalent:
agent.exe --Urls http://localhost:5291 --Sip:ListenPort 6071
Note: %LocalAppData%\agent\ runtime files (sipstate.json, setup.json) are shared
between all instances on the machine — the SIP PIN lockout state is machine-wide by design.
Configuration files are user-editable and live under <app folder>\PersistentData\
(appsettings.json, providers.json, telegram.json + telegram.session, tools.json,
and the TUI's agent tool-set toolset.json) —
never next to the executable (single-directory rule, see "Where files live" below and
RELEASE-CHECKLIST.md). AppConfig.Initialize() seeds the default appsettings.json and
migrates legacy root files on the first run; every key is overridable from the command line.
appsettings.json (under PersistentData\):
{
"Logging": { ... },
"AllowedHosts": "*",
"Urls": "http://localhost:5290",
"SkipIndexingOnStartup": false,
"LLM": {
"Provider": "DeepSeekBridge",
"Anonymize": false
},
"Voice": {
"ExePath": ""
},
"Sip": {
"Enabled": false,
"ListenPort": 5060,
"Registrar": "",
"Username": "",
"Password": "",
"AnswerMode": "pin",
"Pin": "12345",
"MaxPinAttempts": 3,
"LockoutHours": 24,
"AllowedCallers": [],
"Agent": "default-agent",
"Lang": "",
"SttExePath": "",
"RtpPortRange": ""
}
}| Key | Values | Description |
|---|---|---|
LLM:Provider |
Ollama, DeepSeek, DeepSeekBridge, Zai, Gemini, ExllamaV2, ... |
Default LLM provider for the AgentHarness. The provider in use can be switched per-request / per-session — see LLM switching. |
LLM:Anonymize |
true / false |
NameOrKey anonymization (see AIOrchestrator docs). |
SkipIndexingOnStartup |
true / false |
Skip the DocumentsPath index build/refresh + file watcher at startup (debug/dev). |
AutoUpdate:Enabled |
true / false |
Automatic update check at startup (default true). Overridden by the CLI --no-update flag and the TUI Help → Auto-Update menu — see autoupdate.md. |
Voice:ExePath |
path | Path to AIOffice.VoiceAgent.Win.exe for POST /v1/voice/listen. Empty (default) = the voiceagent\ folder next to the server executable — see Build / assets for where it comes from in development vs. releases. |
Sip:* |
see sip.md | SIP telephony: auto-answer + PIN gate, outgoing calls, voice conversation over RTP (whisper STT + Kokoro TTS). |
Urls |
e.g. http://localhost:5290 |
Kestrel listening address. |
Every key is overridable from the command line (--LLM:Provider Zai, --SkipIndexingOnStartup true, --Voice:ExePath ..., --Sip:Enabled true); run --help for the list.
Telegram is a text-chat client medium — like the TUI and the HTML client, not like SIP.
A private message (text and/or file attachments) is handed to the agent through a per-user
chat session and the reply (text + attachments) is sent back to the same chat. There is no
audio: the Telegram Client API has no audio-call support, so Telegram adds no
IAudioMedia and no VoiceConversation — the media list stays SIP (phone calls) and
Voice (desktop microphone). The transport is
WTelegramClient 4.4.8 (userbot, MTProto).
Configuration lives in its own file, telegram.json under PersistentData\ — separate
from appsettings.json, and never touched by updates (single-directory rule — see
RELEASING.md). The
.session file (Telegram auth) lives in the same folder. Keys: Enabled, ApiId,
ApiHash, PhoneNumber, SessionPath, AllowedUsers,
Agent (see docs/telegram.md). When Enabled=true the bridge starts in
the background at boot — a pending first login (verification code / 2FA password) never
blocks the boot; the TUI /telegram command drives status, config, the pending-login code
and the allow-list in-process (the bridge exposes no HTTP endpoints — Telegram is a
chat client, not a web client, so nothing about it travels over HTTP).
Access control (closed by default). Only AllowedUsers entries (numeric ids/@usernames)
may talk. An unlisted user who sends the shared external-client access PIN (Sip:Pin,
set once from the TUI with /sip config set Pin) is enrolled into the allow-list and
welcomed with the same "How can I help you?" used after a SIP login, as text
(SipBridge.SubmitClientPin → shared PinAuthGate: one attempt/lockout budget across SIP
and Telegram, lockout persisted in the app-data sipstate.json). No PIN configured → the
allow-list is the only way in.
LLM providers are defined in providers.json under PersistentData\ (the same file the
AIOrchestrator library ships — embedded factory default as fallback; ProviderConfigs.ConfigDirectory
redirects the library to PersistentData\ for this host, other hosts keep the default next to
the executable). Each entry carries its own ApiKey field: the single source of truth
for keys. Setup.ApiKey resolves the active provider's key from here; the legacy per-provider
Setup properties are honored only as a fallback when a provider config has no key.
- Cloud providers (DeepSeek, Z.ai, Gemini, Anthropic, …) require
ApiKey— sent asAuthorization: Bearer(OpenAI/Anthropic) or in the query string (Gemini). - Local providers are keyless by design: any provider whose
BaseAddressis on loopback (localhost/127.0.0.1) is treated as keyless regardless of name — a dynamically added Ollama/ExLlamaV2/DeepSeekBridge entry works out of the box. - Keys are edited via the TUI provider dialog (
/setup→ Edit, masked field), the AIOffice Settings panel, or directly in the file.providers.jsonis never touched by updates (single-directory rule — see autoupdate.md), so keys survive every update. - The Z.ai key also enables the image OCR pipeline (
ZaiOcrConverterreads theZaientry's key).
Full field reference: the AIOrchestrator docs/providers-config.md.
⚠️ Startup indexing: the server indexesDocumentsPathat startup (minutes on large folders). When the feature under test does not need document searches, start with--SkipIndexingOnStartup true.
Persisted files are split into three tiers; every update mechanism (release archives, future auto-updater) may only replace the distribution tier. The full rule table is in RELEASING.md; summary:
- User-editable configuration — the single-directory rule —
<app folder>\PersistentData\holds EVERY file the user edits or the app persists that must survive updates:appsettings.json,providers.json,telegram.json+telegram.session,tools.json,rag_settings.json. Never overwritten by updates; legacy root copies are migrated there byAppConfig.Initialize()on first run (in DEBUG the app refuses to start while a config json still sits next to the executable —AppConfig.GuardNoStrayRootJson). - Application data & secrets — the OS app-data folder in a subfolder named after the
running executable:
%LocalAppData%\agent\setup.jsonon Windows (~/.local/share/agenton Linux,~/Library/Application Support/agenton macOS). SMTP/IMAP credentials, DPAPI-encrypted on Windows, plusautoupdate.json,crashreport.json,sipstate.json. Outside the app folder, so updates never touch it. (LLM API keys are not here: they live per-provider inproviders.json— see the AIOrchestratordocs/providers-config.md; the legacy key fields ofsetup.jsonremain only as a fallback.) - Distribution content — everything else next to the executable (what the archive
ships):
agent(.exe),agent.xml,voices/,kokoro.onnx,assets/,Lingua/(SearchPioneer.Lingua language models),.playwright/,docs/,Tools/,voiceagent/(Windows), the SDK-generatedagent.staticwebassets.endpoints.json. Replaced on every update with no exceptions and no whitelist — user config is never in the archive, so replacing the distribution tier cannot touch it. All.jsonat the archive root are generated content and must be overwritten. The automatic updater implements these rules — see autoupdate.md.
Ephemeral runtime files (TTS WAVs) go to the OS temp folder (%TEMP%) and are never part of
an update. The Giraffe web GUI client is installed next to the executable (a
GiraffeAIWebClient folder, not in the source tree) from the client's GitHub release and
kept at the latest version — see WebClientUpdater.cs.
The project references KokoroSharp (ships voices/ + voices-zh/ to the output; the
phonemizer is the pure-managed MisakiSharp since 0.8.4 — no espeak-ng binaries) and needs
kokoro.onnx (~325 MB, not tracked in git). The build target DownloadKokoroModel provides
it automatically: copy from the sibling AIOffice.VoiceAgent.Win build output if present,
else curl from GitHub. TTS returns 501 until the model is present. The native ONNX Runtime
engine (per-RID) comes from the explicit Microsoft.ML.OnnxRuntime reference — KokoroSharp
only pulls the managed wrapper.
On Windows, if the sibling AIOffice.VoiceAgent.Win build output exists, the target
CopyVoiceAgentOutput copies the whole VoiceAgent (exe + voices + espeak + model + runtime
deps) into the voiceagent\ subfolder next to the server, enabling POST /v1/voice/listen
out of the box. This is the development rule: the release pipeline downloads the
VoiceAgent.Win release from the public Graphene-Lab/AIOffice.VoiceAgent.Win repository,
so public Windows releases ship the component inside the archive (same voiceagent\
folder — the TUI message and the voice_unavailable 501 both point the user at updating
the app, not at files). Without the component the voice endpoint self-reports as
unavailable — copy the exe manually or set Voice:ExePath.
The automated release pipeline (NuGet dependency packages, version scheme,
IsPrereleasegate, sync-all/pre-push hook, wait-for-packages, release.yml) is documented in RELEASING.md. The commands below are the manual, development-oriented way to build and publish locally.
The executable is named agent (agent.exe on Windows, agent on Linux/macOS) — like
qwen in Qwen Code. Development:
dotnet build # → bin/Debug/net10.0/agent(.exe)
dotnet run --project AgentBridge.csproj # terminal UI + server
dotnet run --project AgentBridge.csproj -- --headless # server onlyProduction single-file (self-contained, no .NET install needed on the target):
# Windows
dotnet publish AgentBridge.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
# Linux server
dotnet publish AgentBridge.csproj -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=trueThe produced agent.exe / agent starts the terminal UI by default and keeps
answering API calls — run it on a server with --headless (optionally under
systemd) so only the HTTP API is exposed.
Assets stay next to the executable. Single-file bundles the managed code; the Kokoro TTS voices (
voices/,kokoro.onnx~325 MB) and, on Windows, theAIOffice.VoiceAgent.Win.exevoice bridge (in thevoiceagent\subfolder) are published alongside (never embedded — that would bloat the exe by hundreds of MB). Keep the whole publish folder together and runagentfrom it. On Windows the publish copies the sibling VoiceAgent output automatically intovoiceagent\(CopyVoiceAgentOutputalso runs on Publish); release archives are produced the same way, with the VoiceAgent.Win release downloaded in CI.
Design decision (2026-08-27, issue #2), implemented in the following release. The preset table in
AgentTools.csis the static form of a three-level tool policy. The model is config-driven so new plugins need no code change and users cannot accidentally cripple the agent. Implemented: per-tool config (tools.jsonunderPersistentData\, "unspecified ⇒ ON"), the dynamicall-filespreset, core tools appended to every preset and locked in the TUI picker. Not implemented (future hardening): server-side enforcement — an explicit APItoolslist can still omit the core tools (documented bypass, option (a) in the issue discussion).
| Level | Tools | Default | Changeable from the TUI? |
|---|---|---|---|
| Core (always-on) | FileTool, GitTool, TaskSchedulerTool |
Always ON | No — locked (separate read-only line in the picker, not toggleable) |
| Class A (our plugins) | DocumentTool, SpreadsheetTool, PresentationTool, OfficeSupportTool, ... |
ON at first start (rule below) | Yes (per-tool config) |
| Class B (vendored engine behind our adapter) | OfficeTool |
OFF unless enabled | Yes (per-tool config) |
Core tools are architectural primitives. FileTool, GitTool and TaskSchedulerTool
live in the AIOrchestrator assembly (not in the Tools/ plugin folder) and other tools
depend on them: FileTool is the sandbox search/read surface (FileSearch, AnswerQuery,
GetDirectoryTree); GitTool owns rollback and versioning — plugins snapshot through
GitSupport (AGENT_TOOLS_GUIDE: "Rollback lives in the shared GitTool");
TaskSchedulerTool owns scheduled automated task chats (timers + JSON persistence in
/.taskscheduler, run logs in /.taskscheduler/logs). Removing them would cripple the
tools that rely on them, so they are locked ON and changeable only by editing the config
file directly.
Default-state rule — "unspecified ⇒ ON". The per-tool config file records only
deviations: a tool present at first start without an explicit status is ON. This makes
hot-added plugins (PluginUpdater, dynamic Tools/ discovery) work immediately — zero
code, zero user action — and prevents the "the plugin does not activate" support ticket.
Why OfficeTool (class B) defaults OFF:
- Domain overlap — OfficeTool covers DOCX/XLSX/PPTX, duplicating
DocumentTool(Word) andSpreadsheetTool(Excel). Overlapping tools cost tokens (the agent reads both skill/help surfaces) and hurt tool-selection reliability. At first start the user finds only the class-A tools active, with no overlapping pairs. - Trust/control — OfficeTool is our adapter around a vendored engine (
officecli); the engine's internals are opaque and regenerated byupdate-vendor.ps1. The adapter is fully sandboxed (every filesystem path entry point resolves viaSandboxPathbefore the engine sees a path; results render throughToAgent), so this is a trust/control trade-off, not a sandbox hole. Users who want it enable it explicitly.
PPTX is still covered with OfficeTool OFF:
PresentationToolbuilds HTML decks (no PowerPoint needed, browser + F11 fullscreen) and does not overlap OfficeTool's real.pptxediting — they are complementary.
The "all-files" preset is dynamic, not a fixed list — it resolves to core tools +
every class-A plugin present (default ON) − class-B tools (default OFF), so new class-A
plugins join it automatically. The token cost of a large catalog (~155 methods in the
full set, re-sent into the system prompt on every agent iteration) is paid only by users
who choose the all-in-one preset; the narrow presets (web-agent, email-agent, ...)
remain for focused tasks and small models. Any custom combination remains possible via
the additive tools field (API) and the /tools checklist (TUI).
| File | Purpose |
|---|---|
Program.cs |
All endpoints + helpers (top-level statements) + DTOs + launch-mode selection |
Tui.cs |
The Terminal.Gui terminal UI: menu bar, AGENT logo panel, streaming chat panel + input line, status bar, / command palette, @ file palette, shortcuts, mouse |
SessionStore.cs |
Multi-turn sessions (orchestrator + history + feature flags; session-backed orchestrators record the conversation to the AIOrchestrator Memory at disposal — idle timeout from AgentHarness.SuggestedConversationTimeout; lifecycle events SessionCreated/SessionRemoved feed the OfficeManager hub) |
OfficeBridge.cs |
OfficeManager WebSocket hub (/ws/office): every agent/subagent instance of this process becomes an employee (spawn/assign/running/method/closed + chat protocol); accepts forwarded events from other processes (POST /v1/office/events) |
StatelessConversation.cs |
Dynamic transcript-hash correlation for stateless chat requests (no session_id): full-transcript SHA-256 → session dictionary, so a third-party client that resends its transcript stays ONE conversation (one employee) instead of a one-shot per message |
TtsEngine.cs |
In-process Kokoro TTS (lazy init, WAV synthesis) |
VoiceBridge.cs |
VoiceAgent.Win subprocess bridge (one-shot recognition) |
TelegramBridge.cs |
Telegram text-chat medium: WTelegramClient 4.4.8 userbot (private-chat → agent session → reply) |
SystemLang.cs |
Machine language resolution (two-letter ISO code) |
AgentBridge.csproj |
Web SDK, AssemblyName=agent, references AIOrchestrator + Terminal.Gui + KokoroSharp, asset targets |
AppConfig.cs |
Single persistent-config directory: PersistentData\ layout, CWD anchoring, legacy-root migration, default-appsettings seed, DEBUG stray-json guard |
AGRNT_ascii_art.txt |
Source of the colored AGENT wordmark shown in the TUI |
appsettings.json |
Repo copy of the DEFAULT user config — embedded and seeded into PersistentData\ on first run (port, LLM provider, voice path) |
telegram.json |
Telegram chat medium config, created under PersistentData\ at runtime — never part of an update |
e2e/ |
PowerShell regression harness (33+ tests, requires DeepSeekBridge) |
e2e/TuiSmoke/ |
ConPTY harness that launches the real TUI, injects keystrokes and asserts the UI (logo, /model picker + Esc, chat) |
dotnet build AgentBridge.csprojSmoke: health → models → upload a CSV → files/{id}/content → DELETE → chat with
file_ids → SSE → create session → multi-turn chat → control switch → audio/speech.
The offline unit tests for the conversion pipeline live in
AIOrchestrator/AgentHarness.Tests.
Terminal UI smoke (Windows): e2e/TuiSmoke launches the real agent.exe in a
pseudoconsole (ConPTY), types /model, presses Esc, sends a chat message and asserts the
rendered UI. Requires port 5290 free and the Debug build:
dotnet run --project e2e\TuiSmoke # 8 checks; exit 0 = all passSee also: README · Terminal UI · API reference