This document describes how AgentBridge ships: the dependency NuGet packages, the version scheme, the release gate, and what to do when adding a new dependency project. It is meant for developers and coding agents. Users only need the README (installation via the GitHub Releases page).
git commit + git push origin master (AgentBridge) → pure code sync (pre-release)
└─ pre-push hook (hooks/pre-push, installed by install-hooks.ps1)
└─ sync-all.ps1 -SkipSelf → pushes every dependency repo (recursive ProjectReference scan)
└─ each repo's publish.yml triggers ONLY on v* tag pushes → a plain push publishes NOTHING
└─ AgentBridge master pushed cleanly
release of the dependency packages (explicit): push a tag on each repo, in dependency order
git tag v1.yy.MM.dd && git push origin v1.yy.MM.dd (per dependency repo)
└─ repo's publish.yml (trigger: tag v*) → packs + pushes its NuGet package (1.yy.MM.dd, --skip-duplicate)
(release.ps1 performs this tag step automatically for every core repo that changed since
its last tag — no manual tagging needed for a release)
AgentBridge release (automatic): push master with IsPrerelease=false in the committed csproj
└─ release.yml (trigger: push to master):
1. check-version: reads the version + the IsPrerelease gate from the csproj
(skips when IsPrerelease=true or when today's tag v1.yy.MM.dd already exists)
2. wait for today's dependency packages on nuget.org (GLOBAL 30-min window, see below)
3. build 5 single-file archives (win-x64, linux-x64, linux-arm64, osx-x64, osx-arm64)
with the Kokoro TTS assets → create the GitHub release (tag auto-created)
4. store-msi job: build GrapheneAgentBridge-<v>.msi (WiX v5, tools/store) from the
win-x64 payload → attached to the GitHub release too
5. store-submit job: update the Microsoft Store draft package to the stable MSI URL
and submit for certification (see "Microsoft Store channel" below)
Store users: after certification is published by Microsoft, the Store delivers the update
automatically (no action needed). GitHub-release users: unchanged (archives + now the MSI).
General rule (see AIOrchestrator
github-push-and-release.md): the dependency repos publish only on av*tag push — plain master pushes never publish, and no project-file changes are ever needed (current or future repos).
In addition to the GitHub archives, every release also produces a Windows installer and updates the Microsoft Store app ("Graphene AgentBridge", win32 EXE/MSI product).
- MSI creation. release.yml's
store-msijob (windows-latest,continue-on-error) runstools/store/New-StoreInstaller.ps1(WiX v5, manifesttools/store/dotnet-tools.json) over the published win-x64 payload and uploads the MSI; thereleasejob attachesGrapheneAgentBridge-<version>.msito the GitHub release. An installer problem never blocks the archives release. - Store updates. Store EXE/MSI products reference the installer by an external
package URL that must answer HTTP 200 without redirects — GitHub download URLs
are rejected because they always redirect. The MSI is therefore streamed by a small
python3 proxy on the AIOffice VPS (
tools/store/vps/mirror-msi.py, systemdagentbridge-mirror) at https://aitechnology.it/agentbridge/msi: it always serves the MSI of the LATEST GitHub release, nothing is stored on the VPS disk. After the release is out, thestore-submitjob callstools/store/Submit-Store.ps1(Store Submission API,api.store.microsoft.com): swap the draft package URL → commit → submit. Microsoft certifies; once PUBLISHED, Store users update automatically. - Store credentials are the repo secrets
STORE_TENANT_ID,STORE_CLIENT_ID,STORE_CLIENT_SECRET,STORE_PRODUCT_ID,STORE_SELLER_ID(local runs can usetools/store/store-secrets.local.json, gitignored). The Entra ID app must hold the Manager role in Partner Center.
Operational details (account setup, proxy install, manual runs): tools/store/README.md
and tools/store/vps/.
AgentBridge and the AIOrchestrator library split persisted files into three storage tiers. Every update mechanism (today: the release archives + manual replace; tomorrow: an auto-updater) must respect them:
| Tier | Location | Purpose | Update rule |
|---|---|---|---|
| User-editable configuration | <app folder>\PersistentData\ |
Every JSON/settings file a user can edit or the app persists that must survive updates: appsettings.json (server config), providers.json (LLM providers), telegram.json + telegram.session (Telegram medium), tools.json (per-tool policy), toolset.json (TUI agent tool-set), rag_settings.json (persisted DocumentsPath) |
Never delete or overwrite — not part of the archive, never touched |
| Application data & secrets | OS app-data folder, <AppData>\<AppName>\ (Windows %LocalAppData%\<AppName>, Linux ~/.local/share/<AppName>, macOS ~/Library/Application Support/<AppName>) |
App-owned state and credentials — setup.json (SMTP/IMAP, DPAPI-encrypted on Windows, provider name; legacy per-provider API keys as fallback only — keys live per-provider in providers.json), autoupdate.json, crashreport.json, sipstate.json |
Never touch — outside the app folder by construction |
| Distribution content | <app folder>\ (everything the archive ships) |
The runtime: agent(.exe), agent.xml, voices/, kokoro.onnx, Lingua/ (SearchPioneer.Lingua language models), assets/, .playwright/, docs/, Tools/, the SDK-generated agent.staticwebassets.endpoints.json, … |
Replace on every update — no exceptions, no whitelist |
The folder name of the app-data tier is the entry-assembly name of the running
executable (agent for AgentBridge → %LocalAppData%\agent\setup.json), not the
product name: each host executable gets its own folder so several apps using the
AIOrchestrator library never share credentials.
Single-directory rule (no root exceptions). User-editable configuration lives ONLY
under PersistentData\ — nothing user-editable is shipped or written next to the
executable anymore. The archive therefore contains no user config and the updater needs no
whitelist: replacing the distribution tier can never touch user settings. Every .json at
the archive root is generated or shipped content that must be overwritten
(agent.staticwebassets.endpoints.json, .playwright/package/*.json); a "don't touch
.json files" rule would break the update, not protect the user.
Migration (one-time, automatic). Legacy root files (appsettings.json,
providers.json, telegram.json, telegram.session, tools.json) are moved into
PersistentData\ by AppConfig.Initialize() on the first run after an upgrade (only when
the destination does not exist yet). In DEBUG builds, the app refuses to start while any
config/state json still sits next to the executable (AppConfig.GuardNoStrayRootJson) —
the tripwire that keeps this layout from regressing (see RELEASE-CHECKLIST.md).
Both storage conventions are implemented in AIOrchestrator Setup.cs
(PersistentDataDir/SettingsFile and SetupFilePath) and ProviderConfigs.ConfigDirectory
(the default stays "next to the executable" for other hosts; AgentBridge redirects it to
PersistentData\ and seeds the embedded factory providers.json there via
ProviderConfigs.EnsureDefaultFile()). AgentBridge's own layout is centralized in
AppConfig.cs.
The split is deliberate: user-editable JSON stays inside the app folder (portable install
keeps its configuration when the folder moves) but in its own directory, while credentials
are per-user OS state. The automatic updater enforces these rules — see autoupdate.md.
The wait exists ONLY to guarantee that a release ships today's version of a core dependency
that CHANGED today: the floating 1.* restore would otherwise pick the latest available
package — i.e. the previous engine when a repo changed but its today's package is still
propagating.
The wait is now conditional. Before releasing, release.ps1 runs a pre-flight check
(Assert-CorePackagesReady) over the 5 core repos:
- a core repo unchanged since its last tag (no commits, no pending changes) → nothing to publish, no wait needed for it;
- a core repo changed since its last tag →
release.ps1creates and pushes today's tagv1.yy.MM.ddautomatically right after the sync, at the commit that contains the pending changes — a release without it would silently ship the stale engine, and the wait cannot help a changed-but-untagged repo (it never publishes today's package); - after the tag is on origin: if today's package is already visible on nuget.org → no
wait; if it is still propagating → the
<NuGetWait>marker is set.
The marker travels inside the gate-off commit (<NuGetWait>true|false</NuGetWait> in
AgentBridge.csproj): release.yml runs the 30-minute wait step only when it is true.
The push trigger and workflow_dispatch behave identically. A manual gate-off push without
release.ps1 leaves the marker at its conservative default (true).
When the wait does run, it uses a global 30-minute window (nuget.org's official
propagation time): every cycle (30 s) it checks all packages at today's version and stops
as soon as every one is visible. After the window, a still-missing package is a hard
failure: the wait list only contains packages that should be at today's version (release.ps1
narrows <NuGetWaitPackages> to the repos that changed today), so expiry means that repo's
publish.yml run failed or nuget.org exceeded its documented window — proceeding would
restore yesterday's engine and either crash the build with misleading errors (2026-09-06:
"CrashReporter does not exist") or ship it silently. Fix the failed publish and re-run
release.ps1; the wait is skipped as soon as the package is visible.
Consequence: a release on a day when no core repo changed skips the wait entirely
(fast); when a core repo changed, its today-tag is created + pushed automatically and the
wait resolves in a few minutes. The only "blocking" case is a core repo whose today-tag is
already on origin (today's package already published and immutable) with newer changes
beyond it — that engine state can only ship with tomorrow's version, so release.ps1 aborts
with a clear message instead of a silent stale release.
Every project (AgentBridge and all dependencies) versions itself as 1.yy.MM.dd
(date-based, same scheme as UISupportBlazor/UISupportGeneric), computed at build time:
<Version>$([System.DateTime]::Now.ToString("1.yy.MM.dd"))</Version>NuGet normalizes leading zeros: 1.26.08.09 → 1.26.8.9 (the flat-container index and the
wait step use the normalized form).
Release gate — IsPrerelease in AgentBridge.csproj (default true):
false→ version1.yy.MM.dd; the tagv1.yy.MM.ddtriggers a full release.true→ version1.yy.MM.dd-prerelease; thecheck-versionjob detects the suffix and skips the build (no assets, no GitHub release).
Set IsPrerelease=true while iterating and to false only when the test cycles proved
the version works. The gate is the switch: pushing master with IsPrerelease=false in the
committed csproj triggers the release automatically (release.yml); the workflow pins the tag
v1.yy.MM.dd to the triggering commit. No tag push needed. The status-bar button runs
release.ps1, which flips the gate to false, pushes master (the release trigger), then
flips the gate back to true and pushes that too — nothing stays pending locally. The
restore push's own run is skipped by the gate, and release.yml pins the tag to the
triggering commit (github.sha), so that later push cannot move it.
A prerelease push publishes nothing. With IsPrerelease=true, pushing master produces
no GitHub release (release.yml runs but the gate skips the build) and no NuGet update: the
dependency repos' publish.yml triggers only on v* tag pushes, so a plain master push does
not even run it. Real publishes only come from pushing a v* date tag per dependency repo
(see "The dependency packages") or from the next date's release with the gate off.
AgentBridge and AIOrchestrator reference their dependencies with the dual-reference pattern (the same used by UISupportBlazor):
<ProjectReference Include="..\X\X.csproj" Condition="Exists('..\X\X.csproj')" />
<PackageReference Include="X" Version="1.*" />- The local sibling project wins in solution builds (development).
- The NuGet package (
1.*floating = always the latest published) is restored when the sibling source is absent (CI, standalone builds).
Consequences:
- CI never checks out the private sibling repos: it builds against the published packages.
- A release ships with today's version of every dependency that CHANGED today (the wait step enforces visibility within its 30-min window); unchanged repos keep their latest available version, which is identical to what today's version would contain.
- The
Naiadpackage (transitive dependency ofGraphene.AIOrchestrator) requires<Papyrine_SponsorshipLicenseIgnored>true</Papyrine_SponsorshipLicenseIgnored>in every project consuming the package — SC021 blocks Release builds otherwise.
| NuGet package | Source repo | Notes |
|---|---|---|
Graphene.AIOrchestrator |
Graphene-Lab/AgentHarness | the engine; pins the four packages below |
AllToMarkdown |
Graphene-Lab/AllToMarkdown | |
MermaidRendering |
Graphene-Lab/MermaidRendering | ships assets/chart.umd.min.js + InstallMermaidRendering.sh via contentFiles |
Graphene.ReverseMarkdown |
Graphene-Lab/ReverseMarkdown | fork of the MIT library → renamed id + Andrea Bruno License 1.4 |
UISupportGeneric |
Graphene-Lab/UISupportGeneric | predates this pipeline |
All are versioned 1.yy.MM.dd and published on a v* tag push (per-repo publish.yml). The
wait list in release.yml ("Wait for dependency packages on NuGet") must contain every
package AgentBridge depends on, in lowercase — and NOTHING else (the tool plugins are not
build dependencies, see below).
The agent-tool plugins (DocumentTool, SpreadsheetTool, OfficeTool, PresentationTool,
OfficeSupportTool) are not build dependencies and are not in the NuGet wait list.
They are loaded dynamically from Tools/ by ToolPluginHost (byte-loaded, never referenced)
and ship in the release archives as self-contained zips from their own GitHub Releases:
- every plugin repo (
Graphene-Lab/<Tool>, PUBLIC) publishes a<Tool>-<version>.zipon eachv*tag via its standardplugin-release.yml(plugin dll + xml + assets + unique deps, minus the AIOrchestrator graph); release.ymlfetches the latest release zip of each plugin intopublish/Tools/<Tool>/and merges each payload'sassets/into the hostassets/(the plugins resolve host-level assets fromAppContext.BaseDirectory\assets);- the plugins' NuGet packages (
Graphene.DocumentTool, …) exist for third-party consumers only — the AgentBridge build and release never use them.
Consequences:
- Plugin and host releases are independent: a new plugin version is published by tagging
the plugin repo (
git tag v1.yy.MM.dd && git push origin v1.yy.MM.dd), and the next AgentBridge release ships it automatically (no NuGet wait, no version coordination). - A plugin with no release yet is skipped with a
::warning::in the fetch step — the tool is simply missing fromTools/until its repo publishes one. - The AgentBridge release gate (
IsPrerelease) controls the host release only; it has no effect on plugin publishing.
To add a project that AgentBridge (or a dependency) depends on, so it joins the automatic update + release system:
- Create the project as a sibling folder with the package metadata in its csproj:
- date-based
<Version>(same format as above); <PackageId>,<Description>,<PackageReadmeFile>README.md</PackageReadmeFile>,<PackageLicenseFile>LICENSE.md</PackageLicenseFile>,<PackageRequireLicenseAcceptance>False</PackageRequireLicenseAcceptance>,<Copyright>,<RepositoryUrl>;LICENSE.md(Andrea Bruno License 1.4 — copy fromUISupportGeneric) andREADME.md, both packed (<None Update="..."><Pack>True</Pack><PackagePath>\</PackagePath></None>);- the pack targets
SetPackageVersion,CleanOldNuGetPackages,PublishPackageToNuGet(copy fromUISupportGeneric; the push target is skipped in CI with-p:SkipNuGetPush=true).
- date-based
- Publish the repo on GitHub (Graphene-Lab; private is fine) and add
.github/workflows/publish.yml(copy from an existing dependency repo). - Reference it from the consumer with the dual-reference pattern (ProjectReference
with
Condition="Exists(...)"+PackageReference Version="1.*"). - Check the package-id on nuget.org first (
https://www.nuget.org/packages/<id>): if the name is taken, use aGraphene.*-style id. - Add the lowercase package id to the wait list in
release.yml("Wait for dependency packages on NuGet"). - If the package depends on
Graphene.AIOrchestrator(or Naiad), set<Papyrine_SponsorshipLicenseIgnored>true</...>in its consumers. - Release by pushing master with
IsPrerelease=false(automatic — see "Version scheme and the prerelease flag" above).
Discovery is automatic: sync-all.ps1 walks the ProjectReference tree from AgentBridge, so
the new repo is pushed by the pre-push hook without any script edit.
- Missing packages at release time: not an error anymore — the wait step gives a 30-min
window and then proceeds with a
::warning::for the packages that were not published today (their repo had no push). Only investigate if a package you EXPECTED to publish is reported missing (check that repo's publish.yml run). - SC021 (Naiad): add the SponsorCheck property to the consumer (see above).
- Package-id collision: verify availability before the first publish.
- TTS missing in the archives:
CopyTtsAssetsToPublishcopieskokoro.onnx,voices/,voices-zh/into the publish directory; the native onnxruntime engine comes from theMicrosoft.ML.OnnxRuntimepackage reference (KokoroSharp itself only pulls the managed wrapper — without that reference the archives ship TTS that fails at inference). Any other runtime content must stay next to the executable (single-file bundles only the managed code). - Lingua language models missing in the archives: SearchPioneer.Lingua copies its
per-language models (
Lingua/LanguageModels/…) only into the output of a project that references the package directly; via the Graphene.AIOrchestrator NuGet dependency nothing is copied, so withoutCopyLinguaModelsToOutput/CopyLinguaModelsToPublishthe first LanguageDetector call fails with "Could not find a part of the path …\Lingua\LanguageModels
<lang>\…" (podcast language detection, document language-word tracking). - Playwright driver missing in the archives: the package's build targets place the
per-RID node driver under
.playwright\at build time, but publish does not carry it over — released archives shipped an incomplete.playwrightand WebTool browser launch failed with a bare NullReferenceException inside the single-file exe (the driver node.exe was absent).CopyPlaywrightToPublishcopies the whole.playwrightpayload; each RID build carries its own driver (win32_x64/linux-x64/darwin-*), so every platform archive gets the right one. Check the archive for.playwright\node\<rid>\node.exewhen the browser feature regresses. - linux-arm64: released since KokoroSharp 0.8.4 — the phonemizer is the pure-managed
MisakiSharp (no
espeak-ng-linux-arm64binary needed) andMicrosoft.ML.OnnxRuntimeshipslibonnxruntime.sofor linux-arm64. - SIP STT missing on Linux/macOS:
CopySttAgentOutput(thevoiceagent-stt/folder with the whisper-basedAIOffice.VoiceAgent) runs only when the sibling repo exists on the build machine — the CI archives never contain it. Deploy it manually next to the binary (see sip.md → "Deploying the speech-to-text executable"); without it the SIP signalling and PIN gate keep working, only the speech recognition is unavailable.