Reference
Release notes
Every Tracon package is cut from one version line, so a release below covers every NuGet package of the family and the npm client at once. Each package’s release-notes metadata links straight to the entry for the version you installed.
Read Versions and upgrades before you move between previews: it carries the pinning rules and the upgrade sequence this page’s entries assume.
Releases
Section titled “Releases”1.0.0-preview.6 — 2026-10-27
Section titled “1.0.0-preview.6 — 2026-10-27”Agents and workflows gain the controls a production host needs before the surface
freezes. The new Tracon.AGUI package lets a front end that speaks the AG-UI protocol
run a Tracon agent. An agent keeps files in a workspace for each session and remembers
what its user said in earlier sessions, a tool can ask the user for input in the middle
of a call (MCP elicitation), and a workflow waits for a tool approval that a person
answers. A callable agent can be pinned to a version, and a version number is never
given twice for one name. A spent call-tree budget or tenant cost quota now stops a tool
call, a function node or an agent step before it starts; a workflow continuation keeps
the budget of the runs before it; and a configuration value Tracon cannot read stops the
host at startup. The release adds three migrations to each SQL provider and breaks
source in several places: read Upgrading from 1.0.0-preview.5 first.
Upgrading from 1.0.0-preview.5
Section titled “Upgrading from 1.0.0-preview.5”- Migrations. PostgreSQL
0063–0065, SQL Server0051–0053and SQLite0050–0052. WithAutoApplyMigrations(the default) they run at startup. They add theexpected_tool_callscolumn of an eval case, an index on the agent files of a tenant by path, and theagent_definition_version_counterstable, which starts the counter of each agent at the version it holds today. No column is dropped. An agent that was deleted and created again before the upgrade keeps only the numbers it holds now (see theIAgentDefinitionStoreentry, Changed). - Public types changed. Each entry below (Changed) says what to do:
AgentDefinition.CallableAgentNamesandAgentDefinitionRequest.CallableAgentNamesareCallableAgents, a list ofCallableAgentReference; a body that still carriescallableAgentNamesis refused with400.ModelBinding.AllowConcurrentToolCallsisModelBinding.ToolLoop.AllowConcurrentToolCalls; a body that still carries the old key is refused with400.EvalCase.ExpectedToolsandEvalCaseDraft.ExpectedToolsareExpectedToolCalls; a body that still carriesexpectedToolsis refused with400.IJobStore.LeaseAsynctakes oneJobLeaseRequest.- A custom
IAgentDefinitionStore,IRunStoreorIJobStoremust pass the new cases ofAgentDefinitionStoreContract,RunStoreContractandJobStoreContract. AddSpeechSynthesizer(name, null)andAddSpeechTranscriber(name, null)no longer compile.Tracon.Client:EvalCaseInput.SourceKindandEvalCasePromotionRequest.SourceKindareEvalCaseSource,VoiceSessionRecord.EndReasonisVoiceSessionEndReason, and the emptySourceKind2class is gone (see the nullable enum entry, Fixed).
- Behavior changes most likely to reach an upgrade, each described in its entry below:
- A configuration value Tracon cannot read stops startup, and so does a list or map key that holds one value. Start the application once: the keys it names were ignored until now.
- The eight
tracon.*metrics that carriedtracon.tenant.idcarry it only whenTracon:Observability:IncludeTenantTagistrue. - A tool call, a workflow function node or an agent step does not start once the call-tree budget or the cost quota of the tenant is spent, and a workflow continuation starts from what the runs before it spent.
- An
IToolAuthorizationHandlerthat deniesToolEffect.Externalnow also closessearch_knowledgeandsearch_conversation_memory. /v1/chat/completionsrefuses a model setting the agent owns, tool-call history and unknown roles with400.- With strict session ownership, an A2A message needs a caller identity to own the conversation.
agentReviewandtoolCallArgsMatchare built-in names; a criterion or check registered in code under either name is refused.- An
IAgentDecoratorwith anOrderabove 0 no longer seesTraconRunOptions.
- Pin every Tracon package to the same exact version. The pre-release Microsoft Agent Framework
hosting packages stay pinned at
1.25.0-preview.261014.1,Microsoft.Agents.AI.Hosting.OpenAIat1.25.0-alpha.261014.1andA2A.AspNetCoreat1.0.0-preview3. The Model Context Protocol SDK moves to2.3.0.
-
A callable agent can be pinned to a version of the callee:
CallableAgentReference.Version. An entry ofAgentDefinition.CallableAgentswith a version runs that stored version in every call mode (background tasks, one tool for each agent), its child run row records that version, and, for an agent kept in the database, the description the model reads comes from it. An entry with no version runs the callee’s current version, as before. A caller that a workflow step or an approval resumption pins to a version of its own now runs the callee versions that version names: a parent pinned to version 1 no longer calls a child at version 3 unless version 1 says so. A new version of a pinned callee leaves the compiled caller as it is. A name converts to a reference without a version (CallableAgents = ["editor"]), so a list of names stays a list of names. A save,POST /api/agents/validateand the compile refuse a version below 1, a version the callee does not have, a pin on an agent defined in code and a name listed twice: the codes arecallable_agent_version_invalid,callable_agent_version_not_foundandcallable_agent_duplicate, each with the path of the entry. A pin that stopped holding after the save fails the caller by name before its first model call. The agent editor of the console shows a Version of … to run select under each checked agent that has versions and keeps a pin it saved; the version compare shows a change of pin. The OpenAPI document and both generated clients carry the new schema. -
WorkflowAgentStepOptions.AgentVersionParameterlets a run choose the version of an agent step in a graph built in code. Set it to the name of a run parameter declared inWorkflowRegistrationOptions.Parameters. The step reads the value each time it runs: empty or absent runs the agent’s current version, a whole number from 1 runs that version and the step’s run row records it. A value that is not a whole number from 1, a name the workflow does not declare, a version the agent does not have and an agent defined in code fail the step by name before its model call; the error for a value that is not a number never repeats it. It cannot be set together withAgentVersion(GetWorkflowAgentStepthrowsArgumentException). A run that continues after an answer reads the value the run started with: a number keeps its version through the wait, an empty value still means the current version when each step runs. -
openai.strictJsonSchemaandazure-openai.strictJsonSchemaask OpenAI to enforce the response schema while the model generates. Set either totrueinModelBinding.ProviderSettings(OpenAIProviderNames.StrictJsonSchemaSetting,AzureOpenAIProviderNames.StrictJsonSchemaSetting;SupportedSettingsis no longer empty). The request then carriesstrict: trueon the response format of an agent whoseResponseFormatis a JSON schema. It reaches the response format only: the tools of the agent stay non-strict, and a fallback link, which receives no provider settings, is not strict. The setting is off by default.openaireads it on both API surfaces and on everyUseOpenAICompatible()registration; a server that does not know strict mode can refuse the request. -
POST /api/agents/validatereturns the response schema each provider’s client sends. The report has a new fieldresponseSchemas, one entry per binding and per fallback link (provider,model,strict,schema), for providers that implement the newIResponseSchemaSupport(OpenAIandAzure). Three warnings name what differs from the schema you wrote, each with JSON Pointers into it:response_schema_optional_required(an optional property that does not allownullis sent as required),response_schema_keyword_not_enforced(a bound such asmaxLengthorpatternis sent as description text) andresponse_schema_strict_unsupported(strict mode only: the parts OpenAI refuses with HTTP 400, which areoneOf,allOf,if,then,dependentRequired,dependentSchemasand an object withoutadditionalProperties: false). The last one is reported foropenaiandopenai-responsesonapi.openai.com, the only service measured, and it is a warning so that it never stops a definition from being saved. The OpenAPI document and both generated clients carry the new field and the three codes. -
IResponseSchemaSupportandResponseSchemaShape. An optional interface of anIModelProvider: given a binding and the author’s schema, it returns the schema the provider’s client sends (EffectiveSchema), whether the request is strict, and the three lists above. A provider that does not implement it adds nothing to the report. -
UseGoogleSpeechregisters a Gemini text-to-speech model as the named providergemini, withGoogleSpeechOptions(Model,Name,MaxCharactersPerRequest,MaxDialogueCharactersPerRequest,DialogueSpeakerMode,Voices,UseAsDefault). One instance speaks one voice (POST /api/voice/speakwith"provider": "gemini"answers a WAV file) and voices a dialogue of exactly two voices throughGetDialogueSynthesizerAsync. It shares the client ofUseGoogleand the provider namegoogle, so one egress rule covers chat, images and speech, and a tenant with its own Google key is served with that key on the client its chat requests use. The speaker format of a dialogue depends on the model generation, and the wrong one answers400: setDialogueSpeakerModetoPartMetadata(thegemini-3.8models, the default) orTextLabels(gemini-2.5-flash-preview-tts,gemini-3.1-flash-tts-preview). The audio is a WAV file or raw 16-bit PCM (OutputFormatwavorpcm); a stream is raw PCM. Character timing is refused, and billing is an estimate. -
SpeechProviderDescriptor.DialogueandGET /api/voice/providersreport whether a speech provider can voice a dialogue. The value is the provider’sSpeechDialogueCapabilities(most voices, most characters, whether it reports timing) ornull. A caller no longer learns it from the failure ofGetDialogueSynthesizerAsync. The OpenAPI document and both generated clients carry the new field. -
SpeechDialogueCapabilities.Check(request, providerName)is the rule a dialogue implementation applies before it calls its service. The built-in ElevenLabs client and the Gemini synthesizer use it, with the same messages; a synthesizer of your own calls it to refuse an empty turn, a turn without a voice, too many voices or too many characters, and never to split or cut the dialogue. -
A front end that speaks the AG-UI protocol can run a Tracon agent. The new package
Tracon.AGUIaddsMapTraconAGUI()(POST /tracon/agui/{agentName}, afterMapTracon()): the body is an AG-UIRunAgentInput, the answer is the AG-UI event stream. It is a Tracon run: recorded, tenant-scoped, role-, key-scope- and session-owner-checked, counted against the quota and priced, with the same protection asMapTracon. The thread is the session, and only the messages after the last assistant message are new input. A tool the front end declares never becomes a tool the model can call: a client-side tool registered withAddClientTooland named in the agent runs on the front end with the schema from the code, a carried client-side tool the request does not declare is400, and a declared tool the agent does not carry is ignored and recorded on the run (RunEventCustomTypes.AGUIToolsIgnored). A tool that waits for approval, and an MCP server’s request for input, end the run with an interrupt; the answer is the next request’sresume. The approval is audited asapproval.decisionwith"surface": "ag-ui", before it is applied. The first event afterRUN_STARTEDis aCUSTOMevent namedtracon.runthat carries the Tracon run id. The package takes the AG-UI SDK (AGUI.Server,AGUI.Abstractions) and is not part of theTraconmeta package. See AG-UI front ends. -
A tool can ask the user for information in the middle of a call (MCP elicitation). Tracon now tells every MCP server it connects to that it can answer such a request, with form fields only. In a run started with
POST /api/agents/{name}/run(or over the AG-UI endpoint) and asessionId, the request stops the run asAwaitingInput: the stream sends aninputsevent right beforedone(a response withIdempotency-Keycarriesinputsin its body), one entry per request withrequestId,toolName,serverName,messageandrequestedSchema. Answer in the next request of the session withAgentRunRequest.InputResponses(ToolInputResponse:RequestId,ActionofToolInputActionAccept,DeclineorCancel, andContentforAccept). The tool call then runs again from its start with the answer, so a server must take the same call twice; the replay asks no second approval. Every other request gets an explicitdeclineand, inside a run, atracon.tool.input.declinedrun event (RunEventCustomTypes.ToolInputDeclined) that names the reason: a run without a session, an OpenAI-compatible call, an A2A task, an MCP tool call, a queued run, an agent called by another agent, a request to open an address (urlmode), and a server on the earlier protocol. A tool call stops for input at most once: if the server asks something else the second time, it getsdecline, never the old answer. A wrong answer is400(unknown or repeatedrequestId, a field the server did not ask for, a required field left out, a value of the wrong type) or409(already answered); another user’s session is403, as for every run. The values go to the server and nowhere else. The audit entrytool_input.answernames the request, the server, the tool, the action and the names of the fields, never a value, and it is written before the call runs again: it is the eighth fail-closed audit operation (the published set of seven grows by one), so an answer that cannot be recorded is not applied. The playground shows a form made from the requested schema and says which server the answer goes to.RunStatus.AwaitingInputnow also appears on the root agent run of a suspended call, and itsRunAwaitingInputevent carriesrequestId,toolNameandserverName. -
An agent can keep files in a private workspace for each session. Set
AgentDefinition.Workspace(WorkspaceSettings:Enabled,ReadOnly,DisableReadApproval; alsoAgentDefinitionRequest.Workspace,AgentDescriptor.Workspaceandworkspacein the HTTP body). The agent then lists, reads, searches, writes, edits and deletes files with the eightfile_access_*tools of the Microsoft Agent Framework, in an area that belongs to one session of one agent: another session does not see the files, and they are not the agent’s file memory. Tracon runs each tool through the same chain as a tool registered in code: yourIToolAuthorizationHandler, argument validation, the tool timeout and the approval step. The tools that change a file (write, replace, replace lines, delete) always ask for approval;DisableReadApprovalonly reaches the tools that read, andReadOnlyoffers the tools that read and nothing else.Tracon:Workspace(TraconWorkspaceOptions) limits a file (256 KiB), the files of a session (5 MiB) and the workspaces of a tenant (500 MiB), and a file also counts 1 KiB for its row; a write past a limit is refused, the model reads “Workspace quota reached”, and the run goes on. The files go with their session:DELETE /api/sessions/{id}, a data subject erasure (its preview, export and delete include them undersession_workspace) and thesessionsretention target delete them. A run with no session, a workflow run and a replay have no workspace. A host that exposes an agent over A2A or MCP must make its workspace read-only withDisableReadApproval, or the host refuses to open the agent. The console has three boxes for it, and the sample has aworkspace-drafteragent. A tool of your own cannot take the name of one of the eight file tools, an agent that has a workspace cannot have a/in its name, and an agent with file memory cannot be named~workspace…. The automatic continuation of an interrupted run refuses an agent that has a workspace. The default file store is in memory and loses the files at a restart: use a SQL provider. SQL Server counts the UTF-16 bytes it stores, so a limit there holds half as many ASCII characters. Migration0064(PostgreSQL),0052(SQL Server) and0051(SQLite) add an index onagent_files (tenant_id, path); the queries behind the limits and the deletion would otherwise read the whole table. -
An agent remembers what its user said in earlier sessions. Set
MemorySettings.EnableConversationMemoryon a definition, callUseConversationMemory(options => options.VectorDimensions = …)once, and register aMicrosoft.Extensions.VectorData.VectorStore. The agent then finds what the same user told it in an earlier session and gets it as context, before each turn (ConversationMemorySearch.BeforeEachTurn, the default) or through asearch_conversation_memorytool the model calls (OnDemand). A record carries the tenant, the agent, the user and the session; a search looks at tenant, agent and user, so a user is remembered across sessions and never seen by another user, agent or tenant. A run with no user or no session neither reads nor writes. Only user and assistant text is stored, not tool results. The records go with their session:DELETE /api/sessions/{id}, a data subject erasure (its preview, export and delete include them underconversation_memory) and thesessionsretention target delete them first. The store embeds the text, so that model call is not on the run’s usage. The console has one box and one choice for it. Check the connector first: theMicrosoft.SemanticKernel.Connectors.*packages (1.74 preview) call a member thatMicrosoft.Extensions.VectorData.Abstractions10.7 removed, and fail at search time without a build warning;CommunityToolkit.VectorData.*1.0 works. A workflow run, and the agent steps below it, remember nothing: the workflow’s session id never gets a session record, so nothing could delete what they kept. The records hold the message text in plain text, in your store, outside content protection. -
A definition can turn the approval of the two skill read tools off. Microsoft Agent Framework asks for approval before
load_skill,read_skill_resourceandrun_skill_script, so an agent with skills could not run where nobody can answer: an A2A or MCP call, a run with no session, a scheduled run. The newSkillSettingsrecord (AgentDefinition.Skills,AgentDefinitionRequest.Skills,AgentDescriptor.Skills, andskillsin the HTTP body) has one member,DisableReadApproval. Withtrue,load_skillandread_skill_resourcerun without approval.run_skill_scriptkeeps its approval: it runs code, so no member of the definition turns that off (a standing approval rule can still approve it). The default is unchanged, a definition stored before this release reads back with all three tools asking, and the console’s skills section has one box for it. A definition that setsSkillsbut names no skill gets the validation warningskill_settings_without_skills. Skill text reaches the model as instructions, so turn the box on only for skills that a tenant administrator writes. -
A workflow waits for a tool that needs approval, and a person answers it. Before, an agent step whose tool needs approval failed the whole workflow unless an auto-approval rule existed. Where a Microsoft Agent Framework pattern hosts the agent (a stored
Sequential,Concurrent,Handoff,GroupChatorMagenticworkflow defined withagentNames, and a code graph that binds the agent withGetWorkflowAgent), the workflow now stops: the step’s run closesAwaitingApproval, the workflow run closesAwaitingInput, and the request has the new formWorkflowRequestForm.ToolApproval, whose prompt names the tool and adds what yourIToolApprovalPresentersays about the call (the arguments are not copied). Answer it withPOST /api/workflows/runs/{runId}/respond:approved, an optional reason intextthat the agent reads, andremember(new onWorkflowRespondRequest) to save a rule for that agent and tool. The answer carries a decision only: the tool call comes from the request the workflow recorded, so adatavalue is refused with400, and so isrememberwithapproved: falseor on any other form. The decision is written to the audit trail asapproval.decisionbefore the workflow continues; an entry that cannot be written refuses the answer. The tool runs once, in the new run the answer opens, in whichever process takes the answer. A request nobody answers closes the step that waited with the workflow when its time limit passes. The console’s workflow card asks for the decision, a reason and the rule. An agent node of aSequentialnode list and a step bound withGetWorkflowAgentSteprun the agent in a plain function node that cannot wait, so a tool that needs approval still fails the run there, and so does a tool of theMagenticmanager; an agent that a step’s agent calls as a sub-agent still cannot ask. A tool approval is answered once, whatever the workflow’s response mode: the same decision again follows the run the first one started, and a different one is409. -
An A2A conversation keeps its history, and it belongs to the caller that started it. A message that carries a
contextIdnow continues a conversation: Tracon stores the session and the agent sees the earlier turns. Before, the reply carried thecontextIdback but the agent saw one message, so a caller believed it was in a conversation that did not exist. A conversation is stored under the tenant, the agent, the caller and thecontextId, and a caller that sends thecontextIdof another caller starts a new conversation. The caller is the signed-in user (Tracon:Audit:ActorClaimTypeapplies), the API key, the staticAuthToken, or, with no identity at all, the local operator; all callers without an identity share one conversation space, and a signed-in user whose identity cannot be read is refused. The session identity isa2a:followed by a SHA-256 digest of the four parts, so acontextIdof any length fits the session identity column. Each turn re-sends the history to the model, so the tokens per message grow with the conversation.UseA2A()registers the store for each exposed agent and anAgentIsolationKeyProvider; a store or a provider the application registers itself wins. A message without acontextIdis still a single turn, but Tracon stores a session for it too (it makes up acontextIdand the reply carries it), and session retention is off by default; set a maximum age for thesessionstarget to bound the table. The change adds no migration. -
An eval case lists the tool calls it expects, with their arguments, and
toolCallArgsMatchchecks them.EvalCase.ExpectedToolCallsis a list ofEvalExpectedToolCall: a toolNameand optionalArguments. The new check kindtoolCallArgsMatchpasses a case when, for every entry, the agent called a tool of that name and one such call carried every listed argument. The listed arguments are a subset (the call may carry others) and a value compares by its JSON meaning:2equals2.0, an object equals one with the same members in any order, and anullvalue means “absent or null”. Microsoft Agent Framework’s ownToolCallArgsMatchcompares a number by its .NET type and an object by its text, so an expected2never matched; Tracon’s check does not use it. The reason of a failed case names each entry (not called,args mismatch) and never repeats an argument value. A custom evaluator receives the same calls onEvalItem.ExpectedToolCalls, as Microsoft Agent Framework reads them. The console’s case editor takes a tool name and a JSON object of arguments for each call and refuses an argument text that is not a JSON object. Promoting a reference run writes the names of the tools the run called, without arguments. The change addseval_cases.expected_tool_calls(migration0063PostgreSQL,0051SQL Server,0050SQLite). A cell of that column that SQL Server or SQLite holds as text and that is no longer the array Tracon writes reads as an entry no run can call, so the case fails instead of passing with nothing to check, and the listing still works. A customIEvalStoremeets this inTracon.Testing.Contracts.Xunit:EvalStoreContractaddsEvalStoreContract.ReplaceCasesAsync_round_trips_an_expected_argument_of_every_json_kind,EvalStoreContract.ReplaceCasesAsync_reads_back_an_empty_list_for_a_case_that_expects_no_tool_callandEvalStoreContract.A_row_written_before_expected_tool_calls_existed_reads_back_its_names, and renamesReplaceCasesAsync_stores_expected_tools_and_contexttoEvalStoreContract.ReplaceCasesAsync_stores_expected_tool_calls_and_context. The last one needs three hooks a store on SQL overrides (SupportsRawSql,ExecuteRawAsync,QualifiedEvalCasesTable, andFormatIdForRawSqlwhen it spells an id otherwise); a store without SQL skips it. -
A host registers a tool the model provider runs, and a definition selects it by name.
AddHostedTool(name, kind, factory)registers a web search, a code interpreter or a file search (HostedToolKind); the factory receives aHostedToolContextwith the tenant, the provider of the agent’s model and the services, and builds the Microsoft.Extensions.AI tool for that tenant. A definition lists the name inToolNames, the same list as a function tool. The provider runs the tool inside the model call, so no approval, permission or timeout applies. A provider states which kinds its API surface runs through the newIHostedToolSupport(HostedToolKinds); a definition that selects a kind the provider would drop, on the primary model or on a fallback, is refused when the agent is built and byPOST /api/agents/validate(hosted_tool_unsupported, with the path of the tool name), instead of running without the tool. Measured against the request each adapter writes:openai-responsesruns all three kinds,anthropicandgooglerun the web search and the code interpreter,openai(Chat Completions) andazure-openairun none. An agent selects at most one hosted tool of each kind; the cached compiled agent follows what the factory builds, so a tenant that replaces its vector store reaches the next run.GET /api/toolslists hosted tools withToolDescriptor.HostedKind, the console shows a badge, and the sample hashosted-openai,hosted-claudeandhosted-gemini. -
A hosted tool call is priced.
Tracon:Pricing:HostedTools:{provider}:{kind}:PerCallsets the price of one call. The amount joins the run’s non-token cost, so the budget, the quota and therun.completedwebhook count it once, and a budget the call used up stops the next model turn. A kind with no price is listed on the run and not added to its cost (the run’sUnsummedItemCount); it is never priced at zero, and the host logs one warning. The code interpreter is billed by container and its price per call is an estimate. When Anthropic bills more searches than the response shows, the difference is added. The harness’s own web search is priced the same way; before, no hosted call had a cost.ToolUsageUnits.Callsnames the unit. -
An agent calls a callable agent with one tool call.
SubAgentSettings.CallMode(SubAgentCallMode.Background, the default, orTool) picks what the model sees. WithTool, each agent inCallableAgentNamesis one tool named after the agent (each run of characters other than letters and digits becomes one underscore), instead of Microsoft Agent Framework’s background task tools. The samplerouteragent asked one question took 5 model calls and about 3,600 input tokens with background tasks, and 2 model calls and about 410 input tokens withTool(real OpenAIgpt-5.4-mini, three runs each); for work that runs in parallel, background tasks stay the better choice. The depth limit, the budget of the tree, the tenant check, the wait limits and the refusal of a sub-agent that needs tool approval apply in both modes. A callable agent whose tool name another tool of the agent already has (ToolNames, a skill tool,search_knowledge, another callable agent) is refused by validation and save (sub_agent_tool_name_conflict,400) and by the compiler. The agent editor has a control for the mode and the version compare shows it. A sub-agent that fails because its model provider is down, or because it no longer exists, answers the parent with the reason as the tool result, as a background task does. A row saved before this setting reads as background tasks. -
A definition sets the model’s request fields and the tool-call loop.
ModelBindinggainsSeed,TopK,StopSequences,FrequencyPenalty,PresencePenalty,ToolMode(auto,none,requiredorrequired:<tool>),AllowMultipleToolCallsandReasoningOutput(None,SummaryorFull), andToolLoop(ToolLoopSettings:AllowConcurrentToolCalls,MaximumConsecutiveErrors,TerminateOnUnknownCalls). They are part of the definition, so a version, an experiment variant and a replay carry them; before, only an agent extension could set most of them andReasoning.Outputcould not be set at all. A provider states which of the fields its API surface sends through the newISamplingSupport(SamplingFields). A definition that sets a field the provider would drop, on the primary model or on a fallback, is refused when the agent is built and byPOST /api/agents/validate(sampling_field_unsupported, with the path of the field), instead of running without it. The shipped providers state what the model adapters send; the table is in Model providers.Tracon.Testing’sFakeModelProvideraccepts every field, and the agent editor has controls for all of them. -
Model content and workflow events Tracon has no type for are recorded, not dropped. A hosted tool call or result (web search, code interpreter, image generation, hosted MCP), a provider
ErrorContent, generated or attached data, a file reference, and any content type Tracon does not know yet are written as aCustomrun event of typetracon.content(RunEventCustomTypes.Content). AWorkflowEventthat the workflow stream does not map, such as an event of your own that an executor of a code workflow raises, is written astracon.workflow.event(RunEventCustomTypes.WorkflowEvent); its warning in the log is now a debug entry. The payload holds no data bytes, no URI, no tool inputs or outputs; its free text is the provider’s error message and the text of a string workflow payload, cut at 2 048 characters, and any other workflow payload is named by its type only. Both payloads followRecordToolPayloads. Unlike the quota and truncation notices,tracon.contentis not sent on the livePOST /api/agents/{name}/runstream; a workflow run’s stream sendstracon.workflow.eventlike every recorded event. Plain text withRecordMessageDeltasoff, reasoning withRecordReasoningDeltasoff, usage and approval content stay as they were. Read the events fromGET /api/runs/{id}/events. -
A named agent extension reaches Microsoft Agent Framework options on a compiled agent.
AddAgentExtension(name, configure),AddAgentExtension<T>(), and an instance overload register code that sets a framework option, adds anAIContextProvider, or wraps the agent withcontext.Use. A definition selects it with the newExtensionNameslist, in call order, and the agent editor offers the registered names (GET /api/agent-extensions). The agent still goes through the definition compiler, so it keeps the model rings, the tenant’s own provider key, the egress policy, and cost. The compiler refuses a change to an option the definition or a tenant and security decision owns, and names the extension and the option.AgentDefinition,AgentDescriptor, andAgentDefinitionRequestgainExtensionNames. A provider an extension adds can add tools, and those skip the tool chain; a replay that plays tools back does not run extensions. -
Any
Microsoft.Extensions.AIspeech client serves a named Tracon speech provider.AddSpeechSynthesizer(name, TextToSpeechClientBinding)andAddSpeechTranscriber(name, SpeechToTextClientBinding)take the client from the container, by service key or unkeyed. Every call passes the tenant’s egress policy against the binding’s provider name; a tenant with its own key for that provider is refused; the host stops at startup when the client is missing or has no provider name. The binding declares the character limit and the voice list, which the client cannot report. Billing is an estimate, character timing and dialogue are refused. No public member names aMicrosoft.Extensions.AIspeech type. -
UseOpenAISpeechregisters OpenAI text-to-speech and speech-to-text as the named providersopenai, withOpenAISpeechOptions(SynthesisModel,TranscriptionModel, names,MaxCharactersPerRequest,Voices,UseAsDefault). Each name is bound to one model. The clients run on the shared OpenAI client’s pipeline and not onAudioClient, so a host publishes with Native AOT. -
A factory agent built outside the model registry logs one warning. When the chat client of an
AddAgent(name, factory)agent did not come fromIModelProviderRegistry, the catalog writes a warning the first time it resolves that agent name, because its model calls skip the model rings. An agent that exposes no chat client gets no warning, and a resolve never fails because of the check. The guide “Use any Microsoft Agent Framework feature” compares the decorator, the factory agent, and the custom agent source by what each keeps, andsamples/Tracon.Samples.MafFeaturesbuilds all three. -
TraconErrorTypeslists the error codes of runs, jobs and OpenAI-compatible errors. One class holds them in one place: the type of a failed run (tool_timeout,content_blocked,run_budget_exceeded, …), the codes of a job the queue failed (tracon.job.attempts-exhausted, …) and theerror.typeof the OpenAI-compatible endpoints (model_not_found, …). The*ErrorTypeconstants of the exception classes (TraconToolTimeoutException.ToolTimeoutErrorTypeand the others) andJobErrorCodesstay, and they carry the same values; no value changed, so no stored run record and no client that matches a code is affected. The codes of the findings thatPOST /api/agents/validatereturns (unknown_tool,cycle, …) are listed the same way and are not public members. A code never changes inside a major version. -
The release check compares HTTP, SSE, telemetry, configuration keys, error codes and the CLI with the last published version. Until now only the .NET API had a check against the last release. From
1.0.0-rc.1a removed or renamed endpoint, response field, event name, instrument, attribute, configuration key, error code, CLI command or option stops the release, and so does a change that the rules do not name. A patch release does not add to any of them. See What 1.x promises for HTTP, telemetry, configuration, error codes and the CLI. -
One tenant can no longer take every workflow slot of a process, or the front of the job queue. The new
Tracon:Workflows:MaxConcurrentRunsPerTenant(TraconWorkflowOptions.MaxConcurrentRunsPerTenant, off by default, from1toMaxConcurrentRuns) limits how many workflow runs one tenant has going at the same time, in two places. In a process, a run beyond the tenant’s limit waits for a tenant slot before it takes a shared slot, so it holds nothing while it waits and never delays another tenant. In the job queue, a worker does not lease a queued workflow job (tracon.workflow,tracon.workflow-run) of a tenant that already holds that many leased, and leases the oldest job of another tenant instead; this count covers every process that shares the store, and it is read, not locked, so workers that lease at the same moment can take a tenant past the limit by up to their own number. The wait for a tenant slot counts towardRunTimeout, as the wait for a shared slot does. The setting is checked at startup, and a value that is not a number stops the host. See workflow concurrency. -
Another agent can be the stop criterion of a loop:
agentReview. ALoopCriterionwithKind = "agentReview"names a reviewer agent of the catalog in the newReviewerAgent, and optionally a version in the newReviewerAgentVersion. After each iteration the reviewer reads the request, the iteration number and the draft (the last assistant message), and answers with{"approved": true|false, "feedback": "..."}:truestops the loop,falsesends the feedback to the agent as the next input. The reviewer runs as a child run of the run under review, through the same door every sub-agent call uses, so the tenant check, the depth limit, the run and token budget, the quota, the wait limit and the refusal of a tool that waits for approval all apply. A refused call, a failed call and an answer that is not that object are failures of the criterion, andOnCriterionErrordecides what follows; an answer that cannot be read is never an approval. The shape is the reviewer agent’s own response format, not an option the loop sets on the call. Validation reportsloop_reviewer_missing,loop_reviewer_self,loop_reviewer_version_invalidandloop_reviewer_not_found, and warns withloop_reviewer_verdict_unknownwhen the reviewer has no JSON schema with a booleanapprovedand a stringfeedback. The console’s Loop panel lists the agents and takes the version.GET /api/loop-criterialists the kind. See Another agent as the reviewer. -
The
LoopIterationCompletedevent says who decided. Its payload has a newcriterialist: every criterion evaluated in that iteration, in order, with itskindanddecision(stop,continueorfailed). AnaiJudgeentry names thejudge(providerandmodelof the call, the fallback model when the judge’s own model failed over), and anagentReviewentry names thereviewer(agentName,agentVersionand therunIdof its child run). The event text names the reviewer too. The judge’s reasoning, the reviewer’s answer, the draft and the feedback are not written. Theloop.iteration-completedframe of the native stream carries the same data. A reader that ignores a field it does not know needs no change. -
The run stream of an agent with a loop says where each iteration ends.
POST /api/agents/{name}/runalready streamed every iteration, but nothing in theupdateframes marked the border, so a client that read the stream could take the first draft for the answer. After each iteration the loop evaluated, the stream now sends aloop.iteration-completedframe, between the lastupdateof that iteration and the first of the next. Its data is the recordedLoopIterationCompletedevent, byte for byte whatGET /api/runs/{id}/eventsserves. The answer is the iteration the last frame closes when it hascontinued: false; when it hasceilingReached: true, the answer is the iteration after it, because the turn at the ceiling is not evaluated. Theupdateafter a frame withcontinued: trueis the loop’s feedback to the model (roleuser), not the model’s text. The event’spayloadisnullwhenRecordToolPayloadsis off. An agent without a loop, an event the store could not record and a sub-agent’s loop send no frame. Recorded event timestamps now have whole microseconds, so the frame and/eventsagree on every store. The console playground draws a divider at each frame. A client that ignores a frame name it does not know needs no change. See Streaming.
Changed
Section titled “Changed”-
AgentDefinition.CallableAgentNamesis nowCallableAgents, a list ofCallableAgentReference(breaking). Rename the initializer; a name converts to a reference, soCallableAgentNames = ["support"]becomesCallableAgents = ["support"].AgentDefinitionRequest.CallableAgentNamesisCallableAgentstoo, and over HTTP the field iscallableAgents, a list of{ "name": ..., "version": ... }objects. A request body that still carriescallableAgentNamesis refused with400and names the new field, so it cannot drop the callable agents in silence. The validation path of a tool name that another tool already has moved fromcallableAgentNames[i]tocallableAgents[i].name.AgentDescriptor.CallableAgentNames(the catalog’s list of names) is unchanged. Upgrade note: no migration. A stored definition that holds the old key reads back as entries with no version, and takes the new shape the next time it is saved; a customIAgentDefinitionStoremust keep the version of each entry (AgentDefinitionStoreContractnow checks it). -
IAgentDefinitionStorenever gives a version number twice for one name.SaveAsyncreturns one more than the highest version the name ever had, including the versions of a definition that was deleted;DeleteAsyncstill removes the definition and every one of its versions and keeps only the number. A customIAgentDefinitionStoremust keep that number too:AgentDefinitionStoreContracthas eight more cases that a store which restarts at 1 now fails:AgentDefinitionStoreContract.SaveAsync_after_DeleteAsync_continues_the_numbering,AgentDefinitionStoreContract.The_numbering_survives_every_delete_not_only_the_first,AgentDefinitionStoreContract.DeleteAsync_of_a_name_that_never_existed_does_not_start_a_numbering,AgentDefinitionStoreContract.RollbackAsync_after_a_recreate_continues_the_numbering,AgentDefinitionStoreContract.RollbackAsync_for_a_deleted_agent_throws_and_keeps_the_numbering,AgentDefinitionStoreContract.One_tenants_delete_does_not_move_another_tenants_numbering,AgentDefinitionStoreContract.Concurrent_first_saves_of_a_new_name_get_distinct_versionsandAgentDefinitionStoreContract.Concurrent_saves_of_one_name_get_distinct_versions_across_a_delete. The PostgreSQL, SQL Server and SQLite migrations add theagent_definition_version_counterstable (0065,0053and0052), one row of name and number per agent and no agent content, and start each existing agent’s counter at the version it holds today. Upgrade note: an agent that was deleted and created again before the upgrade cannot be recovered; its earlier numbers were never kept, so its counter starts at the highest number it holds now. During a rolling upgrade an older process may still save an agent without touching the counter: the next number is one more than the larger of the counter and the agent’s own version, so such a save never collides with the history it wrote. -
A configuration value that cannot be read stops the host at startup. A key that held text its type could not read (
Tracon:Tools:DefaultMaxOutputBytes = "10MB",Tracon:RunRecording:RecordToolPayloads = "nope",Tracon:RateLimit:Enabled = "yes") was skipped and the shipped default stayed, with no message. 25 of those defaults are the loose side, so a typo left a limit open or a protection off. Now every number, flag, time span and enum name in theTracon:*sections and in the package sections (Mcp,Voice,Voice:Live,Voice:Conversation, the providers, the SQL stores) must be readable, and so must the numbers of a model catalog entry and of a price. The host refuses to start and the message names the full key and quotes the text:Tracon:Tools:DefaultMaxOutputBytes is '10MB', which is not a 32-bit whole number.Every unreadable key of the same options type is listed in one message. Empty text, or only white space, is still “not given” and keeps the default. Code that assigns the option after binding still wins, so a typo you have overridden in aConfigurecallback does not stop the host. Upgrade check: start the application once; the keys it names are the ones that were ignored until now. -
A list key that holds one value is refused.
Tracon:Skills:Scripts:EnvironmentAllowList=PATHwas ignored and the default list stayed. The message now says to write…:EnvironmentAllowList:0. A JSON[]and a key with no text keep the default list, as before; to empty a list, clear it in code. This covers every list key (Lanes,HttpSchedulableHandlerKeys,DeniedTerms,AgentNames,AllowedMediaTypes,ThresholdPercents,RetryDelays,Columns). -
An entry of a list or a map that cannot be read is left out and named by its position or name (
Quotas:ThresholdPercents:1,Scheduling:MaxConcurrentJobsPerLane:media). It used to be bound as0(or as an undefined value) so that the options validator would complain about the stand-in; the message now quotes what you wrote. -
A decimal no longer takes a thousands separator.
Tracon:Pricing:…:Input = "1,5"was read as fifteen. It is now an unreadable value; write1.5. -
Tracon:Voice:Live:DelegationModeis refused by the same rule instead of throwing while the options are bound: the host stops at startup with anOptionsValidationExceptionthat names the key and the one mode,Client. -
An address key that is not an absolute address is refused by the same rule.
Tracon:Voice:Endpoint,Tracon:Providers:OpenAI:Live:EndpointandTracon:Mcp:OAuthCallbackBaseUriignored text such asgateway.example.com/v1and kept the default, so speech and live sessions went to the provider’s public address. The host now stops and names the key:… is 'gateway.example.com/v1', which is not an absolute address such as https://example.com/. -
A map key that holds one value is refused.
Tracon:Scheduling:MaxConcurrentJobsPerLane=5,…:MaxDurationByHandlerKey=00:10:00and…:LaneByHandlerKey=medianame no lane or handler and were ignored. The message now says to write…:MaxConcurrentJobsPerLane:<name>. -
The knowledge search and the on-demand conversation memory search run through the tool chain.
search_knowledge(MemorySettings.EnableVectorSearch) andsearch_conversation_memory(ConversationMemorySearch.OnDemand) were added to a run as bare functions, so yourIToolAuthorizationHandler, yourIToolArgumentsValidator, the tool timeout and output limit, and the tree budget never saw them, and the refusal of a tenant’s egress policy reached the model as “Error: Function failed.” Now both pass the chain a tool registered in code passes. An authorization handler that deniesToolEffect.Externalcalls now also closes both searches: it is asked with the tool name andEffect = External, because the text of the query leaves the process, and the model reads your reason as the result. A validator that rejects a tool it does not know rejects them too. Neither tool is inIToolRegistry, soGET /api/toolsdoes not list them; write their names in your policy as fixed text. A spent budget stops the search before the query is embedded; the whole call, the embedding included, ends atTracon:Tools:DefaultTimeout(30 seconds by default); and a hit reaches the model as text, trimmed atTracon:Tools:DefaultMaxOutputByteslike any tool result. The call line ofsearch_knowledgeinGET /api/runs/{id}/toolsnow has ausage: unittokens, the input token count of the embedded query as the provider reported it, and nocost, so it adds to no budget, quota or webhook total; the run’snonTokenCostcounts it underunsummedItemCount. An embedding has no price in Tracon yet. See Embedding Tracon. -
A tool call does not start once the call tree’s budget is spent. The budget (
Tracon:AgentGraph:MaxTotalTokens,MaxTotalCost,MaxDuration) already refused the next model call, a new agent step and a workflow function node. A tool call was the one start it did not read, so when one model turn asked for two paid tools and the first passed the ceiling, the second still ran: its money and its side effect were gone, and the model call that would have read its result was refused. Now the second tool does not run. Its result is the limit, the setting to raise and “The tool call did not start.”; a streaming run (the HTTP run is one) records the call as failed; and the run ends at its next model call as before (Failed,run_budget_exceeded). All three limits stop a tool call,MaxTotalTokensincluded, since a model reads its result; a function node is still stopped by cost and time only. The check covers tools registered in code, session workspace tools, MCP tools, the knowledge and conversation memory searches and skill scripts, and it runs before yourIToolAuthorizationHandler. A tool that has started is never stopped, so the call that crosses the ceiling is the last one that starts; withToolLoop.AllowConcurrentToolCallson, the tools of one turn start together and all of them start. A hosted tool runs inside the model call and is not checked. A host that sets no ceiling still has the defaultMaxTotalTokensof 200,000: a tool that a turn asks for after the tree has passed it no longer runs. See Bound multi-agent trees. -
agentReviewis now a built-in loop criterion kind, so a criterion registered in code under that name is refused.AddLoopEvaluator("agentReview", ...)(in any casing) throwsTraconExceptionat startup, the way the other four built-in names do. A definition that named such a kind now means the built-in one; rename your criterion before you update. -
A workflow run that waits for a concurrency slot is
Queued, notRunning. The run’s record still opens before the wait, but it readsQueuedwhile the run waits for a slot ofMaxConcurrentRunsor ofMaxConcurrentRunsPerTenant, andRunningonce it has them. A run that finds a slot free opensRunningas before.GET /api/runs, the run’s detail and the console show it (thequeuedbadge was already there). A dashboard that countedRunningrows as “using a slot” now counts only the runs that have one.POST /api/runs/{id}/cancelreaches a waiting run (202, closedCanceled); until now it sent everyQueuedrun to the job queue. -
A
Queuedrun row can be closed by run reconciliation.IRunStore.TouchHeartbeatAsyncnow marksQueuedrows as well asRunningones, andIRunStore.ClaimOrphanedRunsAsynccloses aQueuedrow whose heartbeat is older than the threshold asFailed(orphaned). A process writes a heartbeat for the runs it executes, and for a workflow run that waits in it for a slot, so aQueuedrow with a heartbeat is a run whose process is gone. AQueuedrow that never sent one is still the job queue’s and is never closed by reconciliation. AnIRunStoreyou implement must do the same: the shared contract tests (RunStoreContract) have three new cases,RunStoreContract.ClaimOrphanedRunsAsync_closes_a_Queued_row_whose_heartbeat_is_stale,RunStoreContract.ClaimOrphanedRunsAsync_does_not_touch_a_Queued_row_with_a_fresh_heartbeatandRunStoreContract.A_run_promoted_from_Queued_to_Running_keeps_the_heartbeat_it_sent_while_it_waited, andClaimOrphanedRunsAsync_never_touches_a_Queued_rowis nowRunStoreContract.ClaimOrphanedRunsAsync_never_touches_a_Queued_row_that_has_no_heartbeat. -
Breaking for implementers (preview):
IJobStore.LeaseAsync(owner, leaseDuration, lanes, cancellationToken)is nowIJobStore.LeaseAsync(JobLeaseRequest request, cancellationToken).JobLeaseRequestcarriesOwner,LeaseDuration,Lanesand the newTenantCap(JobTenantCap: the handler keys andMaxLeasedPerTenant); a new option is a new property, not a new parameter. AnIJobStoreyou implement takes the request and, whenTenantCapis set, skips a ready job of a handler key in the cap while its tenant holds that many live leases on those keys (an expired lease does not count). A caller readsrequest.Laneswhere it readlanes.JobStoreContracthas eleven new cases for the cap:JobStoreContract.Lease_without_a_tenant_cap_gives_one_tenant_the_whole_queue_in_age_order,JobStoreContract.Lease_with_a_tenant_cap_leases_up_to_the_cap_for_one_tenant,JobStoreContract.Lease_with_a_tenant_cap_skips_a_tenant_at_the_cap_and_leases_the_oldest_job_of_another,JobStoreContract.Lease_with_a_tenant_cap_counts_a_running_job,JobStoreContract.Lease_with_a_tenant_cap_counts_both_workflow_handler_keys_together,JobStoreContract.Lease_with_a_tenant_cap_does_not_count_a_job_of_another_handler_key,JobStoreContract.Lease_with_a_tenant_cap_does_not_count_a_lease_that_has_expired,JobStoreContract.Lease_with_a_tenant_cap_does_not_count_the_leases_of_another_tenant,JobStoreContract.Lease_with_a_tenant_cap_still_honors_the_lane_filter,JobStoreContract.Lease_with_a_tenant_cap_still_leases_a_job_of_another_handler_key_of_a_tenant_at_the_capandJobStoreContract.Lease_with_a_tenant_cap_that_names_no_handler_key_or_no_room_is_refused. -
tracon.tenant.idis no longer on metrics by default. The metricstracon.runs,tracon.run.cost,tracon.judge.cost,tracon.judge.score,tracon.model.cache,tracon.audit.write_failures,tracon.run.recording_failuresandtracon.job.executionscarried the tenant on every series, which multiplies each series by the number of tenants. They now carry it only whenTracon:Observability:IncludeTenantTag(TraconObservabilityOptions.IncludeTenantTag) istrue; it isfalseby default. Spans and the quota gauges (tracon.quota.usage,tracon.quota.limit) carrytracon.tenant.idas before. A dashboard or an alert rule that filters or groups one of the eight metrics bytracon.tenant.idshows no series until you set the key. A value that is nottrueorfalsestops the application at startup. See the tenant tag on metrics. -
A workflow run that continues an earlier run no longer starts with a budget of zero. A person’s answer to a request, an operator’s resume (
POST /api/workflows/runs/{runId}/resume) and a recovery after an interruption each start a new run row, and each used to build a new call-tree budget: a workflow with an approval loop got the wholeTracon:AgentGraphceiling again with every answer, and a run that failed for its budget was resumed into the node that had been refused. The ceiling is now the logical run’s. The new run starts from what the runs before it spent (tokens, cost and child runs), and the time limit counts the time the chain’s runs were executing, not the time the workflow waited for a person. A workflow that stayed under its ceiling only because every answer got a new one now fails at the node that crosses it, withrun_budget_exceeded: raiseMaxTotalCost,MaxTotalTokens,MaxTotalRunsorMaxDurationto the size of the whole review, not of one round. A chain that cannot be read (the run store does not answer, or it is longer than 128 runs) ends the runFailedbefore its first node. A run that an interruption closed holds what its function nodes reported only in its items, and Tracon reads them from there. A nested sub-agent call is spent but not counted as a started run. See the ceiling of a workflow. -
An operator’s resume of a workflow run records the run it resumes in
continuedFromRunId. Until now only a recovery wrote the link, so the resume run looked unrelated to its source andGET /api/runs?continuedFromRunId=did not list it. A run that answers a person’s request still does not carry it; the answer names the run that waited in its own history. A continuation that a recovery opened keeps the link the recovery wrote. The resume run joins the chain the recovery and the response gate read, so a repeated answer follows it, and a resume of a run whose queued job may still recover it is recovered with that job: resume a run only after its job has ended. -
A workflow function node or an agent step is refused once the tenant’s cost quota is spent. The quota was read at the door only (
429), so a workflow that waited for a person, or ran for hours, passed a quota that was spent in its first minutes. Before a function node or an agent step starts, the tree asks the tenant’s quota: the cost limit stops a function node, and the token and cost limits stop an agent step or one agent’s call to another. The run endsFailedwith the new error typetenant_quota_exceeded(TraconTenantQuotaExceededException, classQuotaExceeded); when one agent calls another, the refusal comes back to the model as a tool result, as a spent call-tree budget does. Only a start is refused, and the check counts what the run has spent that its counter has not seen yet, so the nodes it already finished count. The run limit stays at the door. A node registered withoptions.StartsWhenBudgetIsSpent = trueis exempt from the quota as it is from the budget, and a refusal is never retried by a node’s retry policy. The check reads the tenant’s rules (one read; two when the tenant has a rule) each time a node or step starts, and followsQuotas:AllowOnStoreFailurewhen the store cannot be read. WithQuotas:Enabled = falsenothing is read. Quotas are on by default, so a model call is priced in every installation that has not turned them off:AgentRunBudget.ConsumedCostnow holds the cost of the model calls even whenAgentGraph.MaxTotalCostis not set. The price lookup costs about 1 microsecond per model call (24 models in the catalog). See A quota also stops work that has not started. -
A data subject erasure deletes the copies of a session outside the SQL rows before it opens its transaction. The copies are the records of conversation memory in your vector store and the files of a session workspace. They used to go after the rows were deleted and before the commit. The files live in the same database, and SQLite lets one connection write at a time: a delete on a second connection, while the erasure held its writes, waited for the busy timeout and failed. A copy that cannot be deleted still fails the call with every row in place, and repeating the call finishes the work. If a later step fails after the copies are gone, the repeat call deletes the rest and its audit entry counts only what was left. The
sessionsretention target now always reads the expired batch and deletes the files of each session before the rows (an installation with no workspace deletes nothing extra, and the delete is answered from the new index). -
Model Context Protocol SDK
2.3.0(ModelContextProtocol.Core,.AspNetCore,.Extensions.Tasks). No public member was removed. A published agent’stools/callfrom a client on the2026-07-28revision now gets its response headers with the result: a proxy with a time-to-first-byte limit cuts a long call. UseEnableTasksfor long calls, or raise that limit. -
An A2A message is refused before the model is called when session ownership is strict and the caller has no identity to own the conversation. With
Tracon:SessionOwnership:EnabledandRequireAuthenticatedOwneron, a conversation needs an owner. A2A wrote no session until now, so the rule never reached it. The caller receives a JSON-RPC internal error (-32603) and the cause is in the server log atErrorlevel. A message that resumes a conversation another user owns is refused the same way, which matters when one API key speaks for several users. BindIRunAttributionContextto the identity pipeline, or turnRequireAuthenticatedOwneroff, before you upgrade an installation that publishes an agent over A2A. -
EvalCase.ExpectedToolsandEvalCaseDraft.ExpectedToolsare replaced byExpectedToolCalls. This breaks source and the JSON of a case. The old field held tool names and no check read it, so a case that filled it asserted nothing; a name-only entry of the new field is checked.PUT /api/evals/{name}/casesanswers400(Removed field) to a body that still carriesexpectedToolsand namesexpectedToolCalls; theexpectedToolsof the case list is gone. A case stored by an earlier build keeps its names: they read back as entries without arguments, the old column stays in the table and is never written, and the next save of the case writes the new column. A custom check can no longer be registered under the nametoolCallArgsMatch; it is a built-in kind now. -
A call of a registered hosted tool is a pair of tool events, not a
tracon.contentevent. The calls of a tool added withAddHostedToolareToolInvokingandToolInvokedevents under the registered name, with the provider’s call identifier and no payload, and a tool invocation record that carries the call’s measurement (unitcalls, quantity1). A hosted call that no registered tool owns, such as the harness’s web search, stays atracon.contentevent as before. A streamed call that the provider shows in several pieces is written once. A replay that plays tools back (ReplayTools) or runs none (NoTools) removes a hosted tool, so it does not search and bill again. -
ModelBinding.AllowConcurrentToolCallsmoved toModelBinding.ToolLoop.AllowConcurrentToolCalls. This breaks source and the JSON of a definition. A definition stored by an earlier build is read with the old key moved, so no migration runs and the setting is kept; the next save writes the new place only, and so does a rollback.POST /api/agents,PUT /api/agents/{name}andPOST /api/agents/validaterefuse the old key with400(Removed field) and name the new path. A fallback link now takes the wholeToolLoopof the primary model. -
An agent extension and the options of one run can no longer set
Seed,TopK,StopSequences,FrequencyPenalty,PresencePenalty,ToolModeorAllowMultipleToolCalls. The definition owns them now; an extension that changes one fails the compilation and names the option, andChatClientAgentRunOptionsthat sets one throwsArgumentException. One exception: a run may setAllowMultipleToolCallstofalse, which a handoff step does to hand off to one agent. -
/v1/chat/completionsrefuses a model setting the agent owns with400.temperature,top_p,max_tokens,max_completion_tokens,frequency_penalty,presence_penalty,seed,stop,logit_bias,reasoning_effort,response_format,tools,tool_choice,functions,function_call,n,logprobs,top_logprobs,modalities,audio, andweb_search_optionswere read by nothing: a client that asked for JSON, three choices, or its own tool got none of them and no error. A value that asks for the default passes./v1/responsesalready refused its own setting fields.POST /v1/conversationsrefuses initialitems, which it used to drop. -
Agents inside run recording receive a plain
AgentRunOptions. AnIAgentDecoratorwith anOrderabove 0 no longer seesTraconRunOptions; read the run throughTraconRunContext. Microsoft Agent Framework’s function-invocation middleware (AIAgentBuilder.Usewith aFunctionInvocationContextcallback) rejects any other options type, so it threwNotSupportedExceptionon every run started by a Tracon endpoint. It now runs. -
/v1/chat/completionsrefuses tool-call history and unknown roles with400. Atoolorfunctionmessage, an assistant message withtool_callsorfunction_call, and a role other thansystem,developer,userorassistantused to pass: the assistant turn was dropped, the tool result went on as plain text without its call id, and an unknown role becameuser. Tracon runs the agent’s tools on the server, and/v1/responsesrefusesfunction_callitems the same way. A content part Tracon cannot hand to the model is refused by name, too. -
AddSpeechSynthesizer(name, null)andAddSpeechTranscriber(name, null)with a literalnullno longer compile: the call is ambiguous between the factory and the binding overloads. Cast thenull, or pass the factory. -
A harness agent that names skills and also calls other agents lists the skill tools after the background-agent tools, and puts the skill section of its instructions after the background-agents section. They were in the other order. A plain agent, and a harness agent that uses only one of the two, does not change.
-
A run’s events and its end are stamped with the
TimeProviderits start came from. A run’s start came from the registeredTimeProviderand its end and events from the system clock. With your ownTimeProviderregistered (a test clock, for example) a run seemed to last from the fake start to today, and the time a workflow continuation inherited from the run it continues was that gap. With the default clock nothing changes. -
A version number of a stored agent could name another content after the agent was deleted and created again. The numbering restarted at 1, so a pin to version 3 (a workflow step’s
agentVersion, a loop reviewer, an experiment arm) or a tool approval that waited on version 2 reached the new agent’s third or second save a few edits later, and ran its text instead of failing. A run on an approval, for example, was resumed with the new agent’s instructions and completed. The numbering now continues, so the old number names no version and the run stops before any step withpins agent 'x' to version N, but the agent has no version N(a workflow) or fails (an approval resume). Version numbers a run recorded before the delete are not reachable any more, as they were not before. Needs the migration of the Changed entry above. -
A data subject erasure (
DELETE /api/data-subjects/{id}?dryRun=false) did not return on SQLite. The audit entry is written before the erasure commits, from inside its transaction. On SQLite that transaction holds the database’s single write lock, so the audit write from a second connection waited until its command timed out, and the audit log retried the wait up to 50 times. On SQLite the audit entry is now written on the erasure’s own transaction, and the two commit or roll back together. PostgreSQL and SQL Server are not changed. AnIAuditLogyou register in place of Tracon’s own still writes on its own connection. -
OpenAI and Azure OpenAI never received the response schema as written, and nothing showed it. The Microsoft.Extensions.AI client rewrites the schema on every request, strict or not: it closes every object, lists every property in
required(an optional property becomes required and its type does not gainnull), and moves 22 keywords (maxLength,pattern,minimum,minItemsand others) out of the schema into the property’sdescription. An optionalnicknamethe model did not know came back as"". The behavior is not changed; the validation report now shows it (see above). -
The A2A and MCP exposure check refuses an agent whose skill tools ask for approval. The check read only the tool registry, and the skill tools are not in it, so an agent with skills was exposed and its first
load_skillcall closed the runFailedwithapproval_unanswerable(-32603over A2A). The host now refuses the agent when it starts, names the agent and the way out (Skills.DisableReadApproval, or remove the skills), and opens it when the definition turns the read approval off. A host that exposed such an agent and relied on every call failing must setSkills.DisableReadApprovalon the definition, or take the agent off the allow list. The check reads the catalog once, when the host starts: an agent saved later is not checked, and a model that callsrun_skill_scriptstill ends the run at the call unless a standing approval rule covers it. -
A tool that Microsoft Agent Framework already wrapped for approval reports that it needs approval.
AddTool(new ApprovalRequiredAIFunction(fn))— the default shape ofDockerShellExecutor.AsAIFunction()— asked for approval at run time, but/api/toolssaidrequiresApproval: false. The A2A and MCP exposure check read that flag, so such an agent was exposed and every call failed with-32603. The startup check now refuses it, and replay sees the approval. -
The .NET client reads a nullable enum property.
ToolDescriptor.HostedKind,RunComparisonSide.ErrorClass,RunError.Class,WorkflowDescriptor.Kind,EvalCase.SourceKindandVoiceSession.EndReasonwere generated as an empty class named after the property in place of the enum, so the client threwJsonExceptionwhen the host sent a value (The JSON value could not be converted to ...ErrorClass). They are the enum types now. A property the host always sends (WorkflowDefinition.Kind,WorkflowSaveRequest.Kind,RunErrorStatistics.Class) is no longer nullable in the client. The TypeScript client was not affected. -
A stored definition written before a list member existed no longer breaks the catalog. The SQL payload read
mcpResourceUris,parameters, and the newextensionNamesasnullwhen a row had no such key, and the catalog listing failed withArgumentNullExceptionfor every agent. The three lists now read as empty. -
A harness agent completes the turn that answers a pending approval. The approved (or rejected) tool ran, and then the next model call failed with
InvalidOperationException: the harness reloads the session history on every model call, and the history still held the approval request while the response had been consumed. A plain agent was not affected. -
UseOpenAIImagesandUseAzureOpenAIImagespublish with Native AOT. The image generator now runs on the shared client’s pipeline instead ofImageClient, whose members name aSystem.ClientModeltype that version 1.16 renamed;ilcstopped with “Failed to load type”. -
/v1/chat/completionshands image, audio and file parts to the model. It kept only thetextparts, so the model answered as if the user had sent no image, and a message with only an image answered400“‘messages’ cannot be empty.” Embedded data passes the attachment type check and is stored as an attachment, as on/v1/responses. -
An image’s
detailand a file’s name now reach the model on/v1/responsesand onPOST /api/agents/{name}/runattachments. The attachment reference dropped both.
1.0.0-preview.5 — 2026-10-17
Section titled “1.0.0-preview.5 — 2026-10-17”The public API gets smaller before it freezes, and every dependency moves to its
newest line. Twelve interfaces, 23 types that only they used, 81 HTTP request and
response types and TraconAgentSessionStore leave the public surface; five service
types lose their public constructors; AuthorizationConfigurationKey is removed from
the MCP server types, and a migration moves the stored data. The runtime moves to
Microsoft Agent Framework 1.25.0 and Microsoft.Extensions.AI 10.11.0.
Tracon.Azure calls the Azure OpenAI v1 API, Anthropic ReasoningEffort becomes
adaptive thinking, and a configuration value Tracon cannot read now stops startup. The
release adds one migration to each SQL provider and breaks source in several places:
read Upgrading from 1.0.0-preview.4 first.
Upgrading from 1.0.0-preview.4
Section titled “Upgrading from 1.0.0-preview.4”- Migrations. PostgreSQL
0062, SQL Server0050and SQLite0049(remove_authorization_configuration_key). WithAutoApplyMigrations(the default) they run at startup. Each one moves the storedauthorization_configuration_keyof an MCP server into itsheader_configuration_keysmap as theAuthorizationentry, then drops the column. A row with OAuth on, or whose map already namesAuthorizationin any letter case, keeps its map: the connection never read the old key there. An empty key moves nothing. Only the key name moves; no secret value is stored or touched. - The column is dropped in the same migration, so there is no way back. A build from before the migration cannot read the table. Back up the database before the first start on this version.
- Every MCP connection reconnects once after the upgrade (its definition fingerprint changed).
- Public types changed or left the surface. Each entry below (Changed, Removed) says what to do:
McpServerDefinition.AuthorizationConfigurationKeyandMcpServerRequest.AuthorizationConfigurationKeyare removed; declare the key name asHeaderConfigurationKeys["Authorization"].PUT /api/mcp-servers/{name}answers400to a body that still carriesauthorizationConfigurationKey.- Twelve interfaces, 23 types, 81 HTTP request and response types and
TraconAgentSessionStoreareinternal. A host that named one declares its own type, or resolves a service from DI. IRunStore.UpdateRunCostAsynchas no default fortenantId; pass the run’s tenant.SingleTenantContext,HttpTenantContext,TimeoutAIFunction,CompiledAgentCacheandDefaultRunAttributionContexthave no public constructor; resolve them from DI.OpenAILiveOptions.BackendModel,LiveVoiceSessionOptions.BackendModel,QuotaUsageQuery.AsOfandLiveVoiceDelegationMode.Responsesare gone.Tracon.Client:IMcpToolRefresherand thebodyparameter ofTraconRefreshMcpToolsAsyncare gone.JobStoreContractgains a test; a third-partyIJobStorethat keeps a time limit in a 32-bit millisecond column fails it.
- Behavior changes most likely to reach an upgrade, each described in its entry below: a
Tracon:Voice:Live:DelegationModeorTracon:UtilityModelvalue Tracon cannot read stops startup;Tracon.Azureuses the Azure OpenAI v1 API and refuses an Entra token over plain HTTP; AnthropicReasoningEffortis adaptive thinking;Tracon.AnthropicandTracon.Googleno longer promise Native AOT compatibility. - A project that pins Agent Framework hosting packages next to Tracon moves them to the versions in
the dependencies entry (Changed), or restore fails with
NU1608orNU1107. - Pin every Tracon package to the same exact version. The pre-release Microsoft Agent Framework
hosting packages are pinned at
1.25.0-preview.261014.1,Microsoft.Agents.AI.Hosting.OpenAIat1.25.0-alpha.261014.1andA2A.AspNetCoreat1.0.0-preview3.
Changed
Section titled “Changed”-
Tracon.Azurecalls the Azure OpenAI v1 API. Requests go to{endpoint}/openai/v1/through the plainOpenAIclient, with the deployment name in the request’smodelfield and noapi-versionquery string;Azure.AI.OpenAIis no longer a dependency (its stable line stopped at 2.1.0 in December 2024). The resource must support the v1 API.UseAzureOpenAI,AzureOpenAIProviderOptionsand the registered provider name do not change; an address that already ends in/openai/v1is used as it is. An API key is sent asAuthorization: Bearer. An Entra credential now goes only over HTTPS: aCredentialFactorywith a plainhttp://address is refused at the first call instead of sending the token. The health check callsGET /openai/v1/models, sends the key asAuthorization: Bearerlike the chat call (it sent anapi-keyheader), and reports unhealthy instead of sending an Entra token over plain HTTP. Image generation uses the same v1 endpoint (/openai/v1/images/generations). The Azure-only request fields (On Your Data) never worked with the OpenAI version in use and have no counterpart on the plain client;SupportedSettingsstays empty. -
ModelBinding.ReasoningEffortbecomes adaptive thinking on Anthropic. The request now carriesthinking: {"type":"adaptive"}andoutput_config.effort(low,medium,high,xhigh). The budget form Tracon sent before (thinking.type = enabledwithbudget_tokens) is rejected with400by every current Claude model; the adaptive form is accepted by all of them (measured on the Claude 5 family). A model that predates adaptive thinking,claude-haiku-4-5, is the reverse: setanthropic.thinking.mode = enabledandanthropic.thinking.budgetTokensfor it. A binding that names noReasoningEffortsends the same request as before. -
Tracon.AnthropicandTracon.Googleno longer promise Native AOT compatibility. The promise is now a measured result: a Native AOT app that calls the provider through its real request and response path must publish with no trim or AOT warning and run.Tracon.OpenAI,Tracon.AzureandTracon.Voicepass. The Anthropic SDK’s chat adapter fails on the first request under Native AOT, and the Gemini credential library publishes with about a hundred warnings. The packages’ own code is unchanged and stays analyzer-clean. The release rehearsal now publishes and runs a provider smoke, and fails when the compatibility page promises AOT for a provider package the smoke does not reference. -
IRunStore.UpdateRunCostAsyncno longer defaultstenantIdtonull. The parameter staysstring?; a caller now states the tenant on purpose.nullskipped the tenant check, so a caller that left the argument out silently wrote a cost onto another tenant’s run. A callUpdateRunCostAsync(runId, cost)stops compiling: pass the run’s tenant. A store implementation drops= nullfrom its own signature; the call shape does not change otherwise. The shippedRunStoreContractpasses the tenant explicitly. -
Five service types keep a public type and lose the public constructor. The container, or the tool pipeline, builds them, and the guides and samples never call
newon them:SingleTenantContext,HttpTenantContext,TimeoutAIFunction(both constructors),CompiledAgentCacheandDefaultRunAttributionContext. Resolve them from DI. Their registrations are factories now;TryAddandReplacebehave as before, so anITenantContextregistered beforeAddTracon()still wins. A new test pins every public constructor of a concrete class, so the next service type cannot reach the surface unnoticed. -
Tracon:Voice:Live:DelegationModerefuses a name that is not a delegation mode. The host stops at startup with a message that names the setting. A misspelled name used to fall back toClientwithout a word. -
Tracon:UtilityModelrefuses a number it cannot read.Temperature,TopPandMaxOutputTokensthat do not parse ("abc","1.5"for the token limit,"0,7") stop the host at startup with a message that names the key and the text as written. Such a value used to bind as0, so a typo ran the context-compaction summarizer at temperature zero without a word. An empty or blank value still means “not set”: the provider default applies. Code that assignsTraconOptions.UtilityModelafter binding still wins. -
Tracon.Clientis regenerated with NSwag14.7.1. Its public surface does not change. The client now passes the cancellation token when it reads the body of an unexpected response, so a cancelled call no longer waits for that body. -
Tracon.Testing.Contracts.Xunitkeeps asking forxunit.v3.extensibility.coreandxunit.v3.assert>= 3.2.2. The package was measured on xunit.v34.0.2withMicrosoft.Testing.Extensions.TrxReport2.5.1; a project on either line restores withoutNU1608. -
Runtime dependencies move to the newest lines. Microsoft Agent Framework
1.25.0(the hosting packages1.25.0-preview.261014.1,Microsoft.Agents.AI.Hosting.OpenAI1.25.0-alpha.261014.1,A2A.AspNetCore1.0.0-preview3, all still exact pins), Microsoft.Extensions.AI10.11.0, OpenAI2.14.0, the .NET platform packages10.0.13, Anthropic12.58.0, Google.GenAI1.26.1, Azure.Core1.63.0and Microsoft.Data.SqlClient7.1.1. A project that pins Agent Framework hosting packages next to Tracon now getsNU1608orNU1107at restore, as before; move it to these versions. Behavior does not change: the Anthropic provider still sendsReasoningEffortas an extended-thinking budget, although the SDK’s own default is now adaptive thinking.Microsoft.Bcl.AsyncInterfaces,Microsoft.Bcl.HashCodeandMicrosoft.Bcl.Memoryleave the dependency graph ofTracon.Core(and so ofTraconandTracon.Google): the token-counting data package now ships anet8.0build. -
The console build tools move to their newest lines; building the console needs Node.js 22.22.2 or later on the 22 line, 24.15 or later on the 24 line, or 26 and above. TypeScript
7, Vite8(Rolldown), Vitest5, jsdom30and React19.3.1. The embedded console grows from 211.9 KB to 215.4 KB gzip (budget 250 KB), and the embeddable widget (embed/embed.js) still runs in strict mode: Vite 8 dropped the"use strict"directive and the build now asks for it. The embedded file set, the public API and the HTTP contract do not change. A contributor on Node.js 20 now gets annpm ciengine error that names the required version, not a wall of failing tests. The NuGet packages are not affected: a consumer does not build the console. -
The compiled
dist/of@tracon/clientis byte for byte the same, in the same 23 files. Only the development dependencies in itspackage.jsonmove (Vitest5,@types/node26.6); a consumer installs the same code and the same runtime dependency. It still builds with TypeScript5.9, becauseopenapi-typescript7needs the TypeScript 5 compiler API that TypeScript 7 does not ship.
Removed
Section titled “Removed”-
McpServerDefinition.AuthorizationConfigurationKeyandMcpServerRequest.AuthorizationConfigurationKeyare gone (authorizationConfigurationKeyon the wire; deprecated since1.0.0-preview.3). TheAuthorizationheader is declared like any other credential header:HeaderConfigurationKeys["Authorization"](headerConfigurationKeyson the wire). Code that sets or reads the member stops compiling. The same members leave the generatedTracon.ClienttypesMcpServerDefinitionandMcpServerRequestand the@tracon/clientschema, and the field leaves both HTTP schemas. A third-partyIMcpServerStorethat persisted the field drops it and must keepHeaderConfigurationKeys(the shippedMcpServerStoreContractgainsEvery_field_of_a_server_survives_a_round_trip).PUT /api/mcp-servers/{name}refuses a body that still carriesauthorizationConfigurationKey, with any value and in any letter case, with400(titleRemoved field) and a detail that namesheaderConfigurationKeys["Authorization"]. Without the rule the field would be dropped silently and the first tool call would fail with401. Other unknown fields are still ignored. The console form writes the key name intoheaderConfigurationKeys.Authorization; an empty input sends no map, so a stored map is kept. The migration (see Upgrading) moves the stored data first. -
Twelve interfaces and the 23 types that only they used are
internal. They are plumbing between Tracon’s own packages. No guide, sample or contract suite asks a consumer to implement one, and the shipped implementations are the only ones.IToolRegistry,IVoiceHealthCheckandIWorkflowRunnerstay public: a guide names the first two, andIWorkflowRunneris the one public way to run a workflow in process. A host that registered its own implementation of one of these interfaces, or named one of the types, stops compiling. The types, by package:Tracon.Abstractions(34):IAuditActorResolver,IAuditDecorated,IMcpOAuthCoordinator,IMcpPromptClient,IMcpResourceClient,IMcpResourceContextProviderFactory,IMcpToolRefresher,IStatePreflightReader,ITraconDrainState,IWebhookPublisher,IWorkflowFunctionCatalog,McpOAuthCompleteResult,McpOAuthOperationStatus,McpOAuthStartResult,McpOperationStatus,McpPromptArgumentSummary,McpPromptContent,McpPromptListResult,McpPromptSummary,McpRefreshOutcome,McpResourceContent,McpResourceListResult,McpResourceSummary,StateGenerationTally,StatePreflightTarget,StateSample,WebhookApprovalSummary,WebhookEvalSummary,WebhookEventPayload,WebhookJobSummary,WebhookQuotaSummary,WebhookRunSummary,WebhookScoreSummary,WorkflowFunctionDescriptor.Tracon.AspNetCore(1):ITraconUiProvider.
-
TraconAgentSessionStoreisinternal. Its base type,AgentSessionStore, has been[Experimental]since Agent Framework1.22, and a public type cannot freeze a base its owner may still change./v1/responseskeeps using it unless the host registers its ownAgentSessionStore; that registration always wins. No guide or sample built the class. A host that did, to wrap it inIsolationKeyScopedAgentSessionStore, supplies its own store instead. A store the host supplies replaces the Tracon-backed one for/v1/responsesentirely: the tenant filter, the session ownership check and the sessions API of Tracon do not see those conversations. The store keeps session identities as before: theisolationpartition of a key is folded into the identity as<escaped key>::<session id>, and a partition it does not know is refused. -
Four members that never worked or never did anything are gone.
OpenAILiveOptions.BackendModelandLiveVoiceSessionOptions.BackendModelwere read by nothing.QuotaUsageQuery.AsOfwas[Obsolete]since1.0.0-preview.3and no store applied it; usePeriodStarts.LiveVoiceDelegationMode.Responsesmade every session that chose it fail at the provider with a502; the enum keepsClient, the only mode. ABackendModelkey left in configuration is ignored and the host starts. -
81 HTTP request and response types are
internal. They were public only so that the OpenAPI document could carry their descriptions. A .NET client does not use them:Tracon.Clientgenerates its own types, and the HTTP contract does not change. Paths, methods, schema names,required, property types and$refare identical, and the document keeps every description (see Fixed for the five that changed). A host that referenced one of these types declares its own record for the body or uses the generated client type of the same name. The descriptions now come from[Description], so a new HTTP type must carry it; a test fails the build when a schema or property loses its description. The types, by package:Tracon.Abstractions(17):AgentValidationReport,CanaryDecisionKind,CanaryEvaluation,ContextWindowEstimate,DataSubjectErasureResult,ImageGenerationOperatorRequest,ImageGenerationOperatorResponse,JudgeFailure,JudgeRunResponse,RetentionPreview,RunCostRecalculationResult,SessionBranchRequest,SessionBranchResult,SpeakRequest,SpeakResponse,ValidationMessage,ValidationSeverity.Tracon.AspNetCore(63):AcceptedRunResponse,AgentDetailResponse,AgentRollbackRequest,AgentSkillRequest,AgentVersionDiffResponse,ApiKeyCreateRequest,ApprovalDecisionRequest,CurrentTenantResponse,EvalCaseInput,EvalCasePromotionRequest,EvalRunDetailResponse,EvalRunTriggerRequest,EvalSuiteSaveRequest,ExperimentCanaryResponse,ExperimentResultsResponse,ExperimentSaveRequest,InboundTriggerAcceptedResponse,InboundTriggerResponse,InboundTriggerSaveRequest,JobDetailResponse,JobScheduleSaveRequest,JobTriggerRequest,LiveVoiceSessionCreateRequest,LiveVoiceSessionCreateResponse,LiveVoiceSessionStatusResponse,LoopCriterionCatalogResponse,LoopCriterionKindResponse,LoopJudgeModelResponse,McpOAuthStartResponse,McpPromptArgumentsRequest,McpRefreshResponse,QuotaSaveRequest,QuotaUsageResponse,RetentionPolicySaveRequest,RetentionRunTriggerResponse,RunComparisonResponse,RunComparisonSide,RunFeedbackRequest,RunInputResponse,RunReplayResponse,SearchKnowledgeHit,SessionDetailResponse,SkillScriptGrantRequest,TenantEgressPolicyRequest,TenantEgressPolicyResponse,TenantProviderBindingRequest,TenantProviderBindingResponse,TenantRequest,ToolApprovalRuleRequest,TraconAuthenticationMeta,TraconMetaResponse,TraconRoleMeta,TraconStorageMeta,UploadDocumentResponse,WebhookSaveRequest,WebhookTestResponse,WorkflowFunctionResponse,WorkflowRespondHttpRequest,WorkflowResumeHttpRequest,WorkflowRollbackRequest,WorkflowRunHttpRequest,WorkflowSaveRequest,WorkflowVersionDiffResponse.Tracon.Core(1):OnlineEvaluationSummary.
-
Tracon.Client.Generated.IMcpToolRefresherand thebodyparameter ofTraconApiClient.TraconRefreshMcpToolsAsync. The class was never a request body: it was the server’s own refresher service, published by mistake as a schema. A call that passednullor nothing as the body drops the argument;TraconRefreshMcpToolsAsync(cancellationToken)sends the same request.
WorkflowRunRequest.DefinitionVersion. The version of a stored workflow’s definition to run, the same rule asWorkflowResumeRequest.DefinitionVersion: the run compiles that version and records it on its row.null(the default) runs the current definition; a version the workflow does not have fails the run and never falls back; a workflow defined in code ignores the value. The HTTP API does not set it.JobStoreContractchecks a job time limit beyond 24.8 days.A_jobs_time_limit_beyond_an_int_of_milliseconds_survives_the_round_trip_and_the_leasewrites a 30-day limit and reads it back from the enqueue, the read and the lease. A third-partyIJobStorethat keeps the limit in a 32-bit millisecond column now fails the contract.
-
A queued workflow run that recovery starts again, because the interrupted attempt wrote no checkpoint, now compiles the definition version that attempt recorded. It compiled the current definition, so a save made while the job waited changed what ran in the same session. Batch items restart the same way.
-
Cancelling a run at the moment it finishes no longer fails with
ObjectDisposedException.POST /api/runs/{id}/cancelanswers409instead of500, and cancelling a run tree still reaches every sibling. Closing a live voice session while a delegation finishes no longer ends the close with the same exception. -
UseLiveVoice(options => options.DelegationMode = …)with a value outside the enum stops the host at startup. It reached the provider on the first session and ended as500. -
Anthropic prompt caching with
anthropic.promptCaching.placement = systemkeeps the cached instructions in the top-levelsystemarray under Anthropic SDK12.58. The SDK adapter now moves only the system messages in front of the first user or assistant message there; the provider appended the instructions block after the conversation, so it went out as arole: systementry insidemessagesand the cache breakpoint sat in the conversation. The block is now placed in front of the first non-system message. -
Five property descriptions in the OpenAPI document no longer carry an unresolved XML reference.
WorkflowSaveRequest.nodesreadIReadOnlyList<string>? WorkflowSaveRequest.AgentNameswhere the text meansAgentNames;DataSubjectErasureResult.dryRun,JobScheduleSaveRequest.lane,InboundTriggerSaveRequest.payloadPathandInboundTriggerResponse.payloadPathhad the same fault.@tracon/clientandTracon.Clientcarry the corrected text. -
POST /api/mcp-servers/refreshno longer declares a request body. The endpoint took its refresher service without[FromServices]; in a host withoutTracon.Mcpthe framework read the service from the body instead, so the OpenAPI document,@tracon/clientandTracon.Clientcarried a body schema (IMcpToolRefresher) for four preview releases. The request itself is unchanged. A test now fails the build when any endpoint reads a service interface from its body.
1.0.0-preview.4 — 2026-10-15
Section titled “1.0.0-preview.4 — 2026-10-15”Workflows become a control plane for long-running pipelines that a person reviews. A run can be queued and resumes after a restart; a waiting request has one answer, an inbox and a time limit; every step has a time limit, cancellation, a cost record and a budget gate; and a graph written in code gets the same node capabilities as a saved chain, with a revision loop that is safe on a back edge. Speech gains multi-speaker dialogue and named providers chosen per tenant. The release changes the public API and adds seven migrations to each SQL provider: read Upgrading from 1.0.0-preview.3 first.
Upgrading from 1.0.0-preview.3
Section titled “Upgrading from 1.0.0-preview.3”- Migrations. PostgreSQL
0055–0061, SQL Server0043–0049and SQLite0042–0048. WithAutoApplyMigrations(the default) they run at startup. - Five public types changed. Each entry below says what to do:
IRunStoregainsRecordNonTokenUsageAsyncandListNonTokenUsageAsync; a custom store adds both (Changed).IWorkflowDefinitionStoregainsGetVersionAsync,ListVersionsAsyncandRollbackAsync; a custom store keeps the version history (Changed).IWorkflowFunctionCataloggainsValidateSettings(Changed).TraconApiClient: three methods gain optional parameters; a call that passesskip,takeor the cancellation token by position names the argument.ModelProviderSettingsContract.SupportedSettingis nowvirtualand nullable; an existing override adds?to its return type.
- One source break:
AddWorkflowFunction<TInput, TOutput>(name, factory, null)is ambiguous (CS0121); pass a description or use the overload that takes options. - Behavior changes most likely to reach an upgrade, each described in its
entry below: a configuration value Tracon cannot read stops startup; answering
a workflow request is audited before it runs, and an audit store that cannot
write refuses the answer with
500; knowledge-base embedding applies the tenant’s egress policy; every run of a tree adds its spend to the quota; a harness-loop agent hands only its final iteration to a workflow step, the OpenAI-compatible and A2A streams and voice;functionsandrequestsare reserved workflow names. - Pin every Tracon package to the same exact version. The pre-release Microsoft Agent Framework hosting packages stay pinned at their 1.20.0 previews.
Known limitations
Section titled “Known limitations”- The native agent stream (
POST /api/agents/{name}/run) streams every harness-loop iteration, the loop’s feedback included, with no boundary between iterations. A client that shows that stream as the answer shows the drafts. Read the run’sloop.iteration-completedevents, or use a surface that returns the final iteration (the non-streaming run, the OpenAI-compatible endpoints, a workflow step). - In a graph written in code, an agent step’s version pin is fixed when the graph is built, and the graph is built for every run; there is no pin per production. A revision that runs after a new agent version is saved uses that version.
Security
Section titled “Security”-
Answering a workflow’s pending human request is now the seventh fail-closed audit operation. Every
POST /api/workflows/runs/{runId}/respondthat starts a run writesworkflow.respond(the source run, the request id, the run the answer starts, the response mode and a SHA-256 of the answer — never the answer itself) before the run starts, queued or streamed, in every response mode. Behavior change: when the audit store cannot write the entry, the answer is refused with500and no run, record or job is created; before, an answer was never audited.IAuditLog’s documented set grows from six operations to seven. The hash tells two answers apart; it does not hide an answer with few possible values, such as a yes or no. -
A refused provider setting no longer repeats its value. The Anthropic choice settings (
anthropic.effort,anthropic.thinking.mode, the prompt-caching placement and TTL) and thinking budget, and the Google safety thresholds and thinking budget, wrote the rejected value into the message, which reaches logs and run records; a value can be a credential pasted into the wrong field. The message now names the key and the accepted values only. -
The built-in ElevenLabs client refuses a tenant credential with
NotSupportedExceptionon synthesis, streaming synthesis, dialogue and transcription. It does not take a tenant key; a caller that passed one directly was served on the configured key, so the host’s account paid for the tenant’s speech. The resolver never sends one, so calls made through Tracon are not affected. -
Knowledge base embedding calls now apply the tenant’s provider checks before the tenant’s text leaves the process. A document upload,
POST /api/knowledge/{collection}/searchand thesearch_knowledgetool called the registered embedding generator and read neither the tenant’s egress policy nor its own provider key. Name the provider in the newTraconKnowledgeOptions.EmbeddingProvider(Tracon:Knowledge:EmbeddingProvider); without it, the name the generator reports is used. Behavior change: a tenant with an egress policy that does not name the embedding provider, or with its own key for it, gets403on upload and search (new in the OpenAPI document and both generated clients) and a failed tool call. When no provider name is known, a tenant with a policy is refused; a tenant without one is not affected. -
A data subject export now returns protected columns as plaintext. With
AddContentProtection(...)on, the export wrote the stored envelope of session state, run inputs and chat history, so the subject received ciphertext. -
Workflow checkpoint state is now a protected column (
ProtectedColumn.WorkflowCheckpointState). It carries a workflow run’s messages and was stored in the clear even with content protection on. New checkpoints are encrypted when the column is inColumns(the default takes every column); existing rows stay readable. No migration. -
Data subject export and erasure now reach four more copies of a subject’s run: an eval case promoted from the run, the stored response of an idempotent request that started it, the job of a queued run (its id is the run id) and the batch item that started it. Behavior change: erasing a subject now deletes those rows, so an eval suite loses the cases promoted from the subject’s runs; a batch job keeps its other items. The erasure’s audit entry counts each target.
-
A refused configuration key name is no longer echoed. An operator who typed a value, such as an API key, into a field that takes a configuration key name got that value back in the
400body, and a webhook delivery wrote it towebhook_deliveries.error. The message now names the field and the allowed key space only. This covers every field that takes a key name: MCP server headers and OAuth secret, tenant provider bindings, webhook headers and signing secret, and inbound trigger secrets. -
A retention archive now hands protected columns to the
IArchiveSinkas plaintext. WithAddContentProtection(...)on, the archive wrote the stored envelope of run event text and payload, tool call arguments and results, workflow checkpoint state, attachment content, session state and run inputs. Behavior change: a sink that stored those rows now receives the readable content; encrypt it in the sink if the archive must be encrypted at rest. -
A real data subject erasure now refuses with
409while one of the subject’s runs isQueuedorRunning. Such a run could write its session and workflow checkpoint back after the erasure committed. The problem carries the run ids inactiveRunIds; a dry run lists them in the newDataSubjectErasureResult.ActiveRunIdsand erases nothing. Cancel the runs, or passforce=trueto erase anyway.409andforceare new in the OpenAPI document and both generated clients. -
Text-to-speech, speech-to-text and the voice list now apply the tenant’s provider checks before any text or audio leaves the process. The
speak,transcribeandlist_voicestools,POST /api/voice/speak,GET /api/voice/voicesand the conversation socket called the setup-wide speech provider and read neither the tenant’s egress policy nor its own provider key. Behavior change: a tenant with a saved egress policy that does not name its speech provider (elevenlabsfor the built-in client) can no longer synthesize, transcribe or list voices: the endpoints answer403, the conversation handshake answers403before a connection slot is taken (an open conversation keeps the providers it was opened with until it closes), and a tool call fails. Add the provider name to the policy to keep speech working. A tenant with its own binding for the speech provider is refused unless the provider declaresSupportsTenantCredentials; the built-in ElevenLabs client does not, so such a tenant is no longer billed to the host’s key. A binding with an endpoint override, or whose key has no value, is refused too. The policy is judged against the provider name the engine reports, never the name it is registered under. -
Image generation now applies the tenant’s provider checks before the prompt leaves the process.
generate_imageandPOST /api/images/generatecalled the setup-wide generator and read neither the tenant’s egress policy nor its own provider key: a tenant whose policy did not allow the image provider still had its prompts sent there, and a tenant with its own key for that provider was billed to the host’s account. A policy that does not nameImages:Providernow refuses the call, and so does a tenant binding for that provider, because the generator is built with the setup key and cannot use the tenant’s. The endpoint answers403(new in the OpenAPI document and both generated clients); the tool call fails with the same message. A run without a tenant scope now fails before the provider call instead of after it. -
A root agent run now counts its whole call tree in the tenant’s quota counters and in its
run.completed/run.failedwebhook. A child agent run never accounts on its own, and the root counted only its own tokens and cost, so the children’s spend reached no counter and a token or cost quota was exceeded through sub-agents. The root now adds what its child runs recorded; a workflow root already counted its tree. The webhook’sinputTokens,outputTokensandcostof an agent root are now the tree’s totals, the same meaning a workflow root’s event has. Costs in two currencies are not added; the root’s own is kept. -
A tenant endpoint on a provider added with
UseOpenAICompatible()now goes through the outbound address guard, as on every other provider. The factory built the provider without the guard, so a tenant binding whose endpoint host name resolved to a private or metadata address received the tenant’s prompts and key; the binding save path checks only IP literals. -
One tenant can no longer stop a provider for every other tenant on the shared setup-time key. The circuit breaker counted every exception, so a tenant’s repeated
400s, an exhausted run budget, a history that names a deleted attachment, or a tool failure that the tool-call loop rethrows opened the provider’s circuit for everyone for the break duration. It now counts only a failure the provider client threw that shows the provider is unhealthy: no connection, a timeout, a5xx,408or429, or a401/403, because a rejected key fails every caller in the same way. Any other permanent4xxdoes not count, and a failure that does not count leaves the counter and a half-open circuit as they were. A test or runbook that opens the circuit with a400or a404now needs a401or a connection failure. -
Data subject export and erasure now include workflow checkpoints and the scores of the runs found through a subject’s sessions. Neither table has a foreign key to
runs, so nothing cascaded to them: an erasure left the checkpoint state, which can hold the subject’s own messages, and the scores with their comments, and the export did not show them. A checkpoint’sstateis exported as the stored JSON text. Erasure also removes the checkpoints of a run that waits for input: retention keeps them, erasure does not. -
A job that resumes an approved tool call (
ApprovalResume, a built-in key a schedule can also enqueue) now fails when its payload names a different agent than the run that asked for approval, or when that run no longer exists. A hand-written payload could run another agent with the approved tool call. -
A failed workflow node no longer writes its exception’s own message to the run’s events. The run row already carried only the exception’s type and a correlation id, but the
ExecutorFailedevent of the same failure carried the raw message - a host and port, a request detail - to the event table, the SSE stream and every run event sink. The event now carries the same display-safe text as the row (<Type> failed. (ref: <id>), or a Tracon exception’s own message), and both name the same log entry. -
A harness agent no longer reads skills from, or writes memory files to, the server’s working directory. Microsoft Agent Framework’s harness falls back to the process’s working directory when it is not given a source: with no skill source it loaded every
SKILL.mdit found there, past the agent’sSkillNames, the skill’s enabled flag and the tenant’s scope; with no file store it wrote what the model chose under./agent-file-memory, outside the tenant stores, retention and erasure. Every harness agent got both, including one saved from the console. Skills are now offered only when the definition names them, and file memory only whenMemory.EnableFileMemoryis true, on the registeredAgentFileStore. A definition that names skills whileHarness.DisableAgentSkillsProvideris true is refused when it is compiled; its skills were dropped without a word. -
A tenant’s egress policy and its own provider key now reach every model an agent calls, not only the agent’s own model. The model that summarizes the conversation during compaction (
Compaction.SummarizationModel, orUtilityModelwhen the agent names none) and the judge of anaiJudgeloop criterion were built without either. A definition that named a provider the tenant may not call compiled and ran, and each compaction sent the tenant’s conversation to that provider. Such a definition is now refused when it is compiled or validated, and the refusal names the provider. The summarizer uses the tenant’s own key when the tenant has one for its provider, so the tenant pays for it, and such an agent is not cached: a rotated or deleted key takes effect on the next run. The loop judge keeps the setup-time key: it is the host’s one judge model, the same one the run judge calls. -
A saved egress policy now reaches agents compiled before it. A compiled agent kept the policy decision of the moment it was compiled, so after a tenant forbade a provider, an agent already in memory kept calling that provider until its definition changed or the process restarted. The next run is now compiled under the saved policy and refused before any request leaves; a run already in progress is not stopped. Each run reads the tenant’s policy once to build the cache key.
-
A standing approval rule for a tool of an MCP server is now bound to the server’s endpoint. An MCP tool is named
{server}_{tool}, so when a key with theAgentsAdminscope pointed a server at another endpoint (or deleted it and added it back under the same name), a rule a security administrator wrote for the old endpoint approved the new endpoint’s tool. A rule now records a fingerprint of the endpoint (sourceFingerprint, a SHA-256, never the address) when it is written, by an administrator or by the “don’t ask again” flow, and approves the tool only while the server keeps that endpoint. While a server’s tools are not discovered (its last refresh failed), its tools always ask, andPOST /api/approvals/rulesfor one of them answers409. Rules stored before this release carry no fingerprint: each standing rule for an MCP tool asks once more after the upgrade.ToolApprovalRule.SourceFingerprintis new and part of a rule’s identity; a customIToolApprovalRuleStorehas to store and compare it (see Changed). -
Saving an MCP server with
requiresApproval: falsenow requires theSecurityAdminscope as well asAgentsAdminwhen the call uses an API key: for a new server, for a server that required approval, and for a server whose endpoint changes while approval stays off. The flag waives approval for every tool of the server, which is the decision onlySecurityAdmincould make for a single tool; anAgentsAdminkey alone could switch it off and answers403now. -
A provider-hosted live voice session now obeys the tenant’s egress policy and its own provider key. The session is bound to a tenant, but the OpenAI live provider used the shared key and read no policy, so a tenant whose policy excluded
openaistill had its audio sent there, and a tenant with its own key was billed to the installation’s.POST /api/voice/live/sessionsnow answers403when the policy does not allow the live provider, when the tenant’s binding for it has no value, or when the provider cannot use the tenant’s key; a tenant with its own binding gets its own key on both the session creation and the sideband attach. A binding with an endpoint override is refused for a live session (403). -
Tracon.SqlServerno longer restores aSystem.Security.Cryptography.Pkcswith known vulnerabilities (GHSA-846f-f764-76mx, high; GHSA-2fmv-rv2c-59x7, moderate).Microsoft.Data.SqlClientpulls 8.0.1 onnet8.0and 9.0.13 onnet9.0andnet10.0, and every SqlClient release up to 7.1.1 does the same, so a new project that used--persistence sqlserverrestored withNU1902andNU1903. The package now depends on the patched version for each target framework (8.0.2, 9.0.21, 10.0.13), which wins over SqlClient’s own. It adds no further packages to the graph.
-
A workflow run that reaches its time limit no longer ends its stream with an exception. When the limit fell between the moment an event arrived and the moment it was recorded, recording it on the run’s cancelled token threw
TaskCanceledExceptionout of the stream: the run was closedCanceled, but a streaming caller got the exception instead of the closing event. The run now stops there as it does between two steps. -
A job released for another attempt no longer loses that attempt. A failed attempt with attempts left makes the job leasable at once, and the worker could lease it again while the attempt that released it was still finishing on the same worker. It logged “already executing” and dropped the new lease, but the store had already counted the attempt: the job stayed
Leaseduntil the lease ran out and then failed as out of attempts without running again. The new lease now waits for the previous attempt and runs, unless that attempt finished the job. -
A pending workflow request whose type is a record of the graph’s own is answered correctly. The run that answers reads the request back from a checkpoint, where its data is JSON, and the plan-review check asked the framework to read that JSON as a Magentic plan review, every member of which is optional: a record such as
{"text": "...", "violation": null}was taken for one, and the answer was built the wrong response type (Message type MagenticPlanReviewResponse does not match expected response type). The check now reads the port’s declared request type. The run that published the request held the live object and never showed the defect. -
A harness loop agent no longer hands its drafts to the caller that acts on its answer. A streaming loop yields every iteration, its drafts and its feedback included, and six surfaces read that stream as the answer: the next node of a workflow (a text-to-speech node spoke the rejected version), the next agent of a ready-made Sequential workflow, the streaming
/v1/chat/completionsand/v1/responses, the streaming A2A call, and the two voice surfaces, which spoke the drafts aloud. These surfaces now run a loop agent through the non-streaming path and stream its final iteration only; its usage stays the sum of every iteration. Behavior change: on these surfaces a loop agent’s answer arrives in one piece when the loop ends, not token by token. The nativePOST /api/agents/{name}/runstream is unchanged and still streams every iteration. A customIAgentDecoratorthat does not delegateGetServicehides the loop, and the agent then streams as before. -
An approval that ran out of time no longer erases what the run that asked had recorded. The expiry closed the run
Failedwith a completion built from nothing, andIRunStore.CompleteRunAsyncreplaces the event count, usage and cost of the row with what the completion carries, so a run that had spent tokens before it paused lost its usage, its cost and its event count. The expiry now reads the run first, closes it only while it is stillAwaitingApproval(a run something else closed keeps its outcome), and writes those values back. -
Tracon:Retention:IdempotencyKeysis now bound from configuration. ItsMaxAgeDaysandArchivekeys were ignored and the target kept its one-day default; only code (Configure<TraconRetentionOptions>) could change it. A configuration that sets them now takes effect, andMaxAgeDaysbelow 1 fails startup like every other target. -
/v1/chat/completionsnow reports why the answer ended.finish_reasonwas always"stop", so an OpenAI SDK caller could not tell a cut-off answer from a complete one. Behavior change: an answer the model stopped at its output token limit or at its context window is"length", and an answer a content filter cut off after it produced text is"content_filter", on the JSON response and on the last chunk of the stream (data: [DONE]still ends it). The value comes from the answer’s final model call; a tool turn’s reason is never reported, and run recording does not need to be on. A filtered answer with no content still fails with502(erroron the stream), and a cut-off structured answer still fails as before. -
A failed read of the quota threshold notice no longer fails the agent run’s stream. With
TraconQuotaOptions.PublishThresholdToRunStreamon, a run store error while the stream read the notice back ended the stream of a completed run with anerrorframe. Behavior change: the stream now logs a warning and ends withdone, without the notice; the notice stays onGET /api/runs/{id}/events. -
A tenant’s quota now counts what a call tree spends after its root run ended. The root read its tree from the run store once, while its own row closed, so a child agent still running in the background at that moment (a root that starts a task and answers without waiting) never reached a counter, and neither did the amount a tool reported after its call timed out and its run closed. Behavior change: every run of a tree now adds its own tokens and cost to the counter when it ends, under the root’s tenant and name (a child adds no run: the tree is still one run), and a tool amount that arrives after its run closed is added to the counter once, in
Tracon:Pricing:Currency. The counter andGET /api/runs/{runId}give the exact total; therun.completedwebhook is unchanged and shows the tree as it stands when the root closes. A threshold crossed by a child run is written to that child’s own event stream (withQuotas:PublishThresholdToRunStream; read it fromGET /api/runs/{runId}/events) and itsquota.thresholdwebhook names that run; a tool amount that crosses one is announced by the webhook only. A child priced in another currency thanTracon:Pricing:Currencyadds its tokens and the amounts it reported beyond tokens, not its token cost. A store of your own needs no change;QuotaStoreContracthas two cases for an entry that adds tokens and cost and no run. -
A job no longer repeats a failure the next attempt would get again. The worker retried every handler failure up to the attempt limit: an eval whose judge was cut off at its output limit ran its unfinished case and paid for it on every attempt, and a queued run whose model answered a permanent
4xxran again too. Behavior change: a failure whose stable type isoutput_truncatedortenant_provider_refused, or a model provider failure (upstream_error) with a4xxother than408and429, fails the job on that attempt. The decision reads the type, never the message; an exception a consumer handler throws itself keeps its retries, and so doesJobRetryException. A workflow node retry and the online evaluation read the same decision. -
A tenant’s refusal of a provider is no longer classified by its text. Its error type was the .NET type name, and a tenant or provider name with “timeout” or “429” in it made the refusal a transient fault that a workflow node retried. Behavior change: every refusal (model, fallback, summarizer, loop judge, image, speech, embeddings, live voice) is the new
TraconTenantProviderRefusedException, error typetenant_provider_refused, with the new classRunErrorClass.TenantProviderRefused(in the OpenAPI document and both generated clients). A compilation no longer wraps it asTraconCompilationException(compilation_failed); the message still names the agent. A binding that names a configuration key the tenant may not read is a refusal too: the image, speech and knowledge endpoints answer403for it, not500. The model path’s refusal of a tenant credential its provider cannot use wasprovider_credential_unsupportedand is nowtenant_provider_refusedtoo. The image and live voice refusals name their kind of provider instead of “model provider”. -
A provider failure masked as
upstream_errorthat answered429is nowRateLimited, notProviderError, and a status .NET carries only onHttpRequestException.StatusCodenow reaches the record ((HTTP 401)). -
An Anthropic answer cut off by the context window (
model_context_window_exceeded) and a turn the API paused (pause_turn) no longer complete the run as finished. The SDK’s adapter reported both as a plain stop. A cut-off structured answer now fails withoutput_truncatedand a plain one writestracon.output.truncated, both naming the context window; the event payload gains acausefield (output_limitorcontext_window). A paused turn is continued, its blocks sent back as the API returned them, up to five times; a turn still paused fails the call. New:TraconFinishReasons.ContextWindowExceeded, a finish reason any provider can report. -
A SQL store write that the server picks as a deadlock victim is run again. The SQL Server upsert gave error 1205 to its caller under concurrent writes. Every store transaction now opens at one boundary that runs the whole transaction again, up to five times, on a new connection.
-
A failed webhook send whose result could not be recorded kept its retry delay. The store error used to replace the send’s failure: the job was released at once, and on the last attempt the delivery stayed
Pending. It now endsFailed. -
TraconToolUsage.Reportrefuses a measurement the record cannot hold, as a function node’s item is refused. An oversize unit, provider or model reached the insert, and on PostgreSQL and SQL Server the failed insert lost the call’s whole record. -
GET /api/voice/voicesanswers502when the provider fails, asPOST /api/voice/speakdoes; it answered500(new in the OpenAPI document and both generated clients). -
The first function node of a workflow node list runs once per run. A run sends the first node the user’s message and then Microsoft Agent Framework’s turn token, on every start and resume, and a first node that takes
objectreceived both and ran twice — a node with an outside side effect did it twice. Behavior change: saving a node list whose first node takesobjectanswers400, and a stored one fails before its first node runs; declareList<ChatMessage>orIEnumerable<ChatMessage>. -
No money total adds amounts in two currencies. A run tree, the totals and breakdowns of
GET /api/stats, the points ofGET /api/stats/timeseries, an experiment arm and the online evaluation’s judge cost added the runs’ amounts and labelled the sum with one of their currencies (the in-memory store the first, the SQL stores the alphabetically largest). When the runs are in more than one currency, the total now keeps no amount and no currency, and the newCurrencyCountsays how many there are; letter case is not a second currency.RunSpend.Totalfor a tree returnsnullfor such a tree, so the quota and therun.completedwebhook no longer count a mixed sum: an agent root counts its own cost. The console shows mixed currencies. -
A run whose caller can never answer its tool approval no longer waits forever.
/v1/chat/completionsanswered200with empty content; it now answers409with the error typeapproval_required(or, on a stream, an error frame of that type and no[DONE]).POST /api/agents/{name}/runwithout asessionIdanswered200; it now answers409witherrorTypeapproval_unanswerable(or anerrorevent instead ofdone). In both, and for an agent exposed over MCP or A2A, the run closesFailedwith the error typeapproval_unanswerable; an MCP task poller still sees the refusal. The409responses are new in the OpenAPI document and both generated clients. -
A queued turn that ends at a tool approval no longer calls the model again when the approval cannot be written. The write is tried again within the lease, and a write the store committed but reported as failed is not written twice; past the lease the job fails with
tracon.job.approval-save-failedand the run closesFailed. -
A structured-output agent (
JsonorJsonSchemawith validation on) whose turn stops at a tool approval or at a client-side tool call no longer fails with “The response is empty.” Such a pause is not validated; the turn that carries the decision or the tool result is. -
A workflow run stopped by a graceful shutdown keeps its checkpoints while
ResumeInterruptedRunsis on, also withKeepCheckpointsAfterCompletionoff. They were deleted when the stopped run closed, and its continuation ran every finished step again; the continuation now removes them when it finishes. -
An agent can name a skill that exists only under a skill root (
SkillRoots). The catalog and the definition check looked in code and in the store only, so such an agent failed with “the skill was not found” and no HTTP run reached a script on disk. A disk skill is installation-wide like a code skill; a stored skill under the same name shadows it for its tenant. An edit to itsSKILL.mdreaches the next run. -
The
Tracon:Knowledgeconfiguration section is now read.TraconKnowledgeOptionswas documented as bound from it, but every value there was ignored; only code that calledConfigure<TraconKnowledgeOptions>changed a setting. ADimensionsvalue in configuration now takes effect: it must match the migrated vector column. -
A draining process (
Drain.Enabled) now refuses every new run it would start itself, not only the agent run endpoint. The workflow run, resume and respond endpoints and run replay started a run during the drain; they now answer503(new in the OpenAPI document and both generated clients)./v1/chat/completionsand/v1/responsesanswer503in the OpenAI error shape withtypeservice_unavailable, the voice conversation handshake answers503before the upgrade, an MCPtools/callreturns an error result and an A2A message fails. A trigger, eval, schedule or approval still writes its job, which waits for a worker that is not draining. -
A provider error with an empty or non-OpenAI body is now read with its HTTP status. System.ClientModel (OpenAI, Azure OpenAI) writes
Status: 404, the Anthropic SDKStatus Code: NotFoundand Google.GenAIstatus code 404; onlyHTTP 404was read. Such a401,403or404moved to the next fallback link, and such a400or404counted against the circuit that every tenant on the setup key shares. The stored provider error now names the status for these failures too, so their error fingerprint changes once. A cancellation that a tool or a Tracon ring raised no longer counts against the provider’s circuit; the provider client’s own timeout still does. -
A queued agent run, or the agent run of an inbound trigger, no longer stays
Queuedforever when the queue refuses its job. The placeholder row was written first and never closed; it is now closed asFailed, as the queued workflow run already did, and the request still fails. -
A batch item or an eval case whose run stops at a tool approval now fails, and its run is closed as
Failed. Both run without a session, so the approval could never be answered: the run stayedAwaitingApprovalwith no inbox entry, and the batch reported the itemCompleted. The queued agent run already had this rule. -
A document whose text the content guard masks stays a document in the run record. The masked message was rebuilt as plain text, so
RunStarted.Textheld the document’s text and noDocumentAttachedevent was written. The record now takes the name and the text as they read after masking, or the name[masked document]when the masked text no longer reads as one document. The original name and summary are not recorded. -
The model calls of an evaluator added with
AddEvaluatorJudgeare now recorded. Each judgment writes onejudge:{name}run of kindEvalwith the calls’ usage and cost, and the cost reaches the judge cost metric, as the built-in model judge already did. Before, nothing wrote this spend down. A failing evaluator closes its run asFailedand still throws. -
The in-memory job store now changes a job item only while it is pending, as the SQL stores do. It overwrote a finished item, so a late report from a worker whose lease had expired rewrote the result the job already gave.
-
Scheduling:Lanes,Scheduling:MaxConcurrentJobsPerLane,Scheduling:LaneByHandlerKeyandScheduling:HttpSchedulableHandlerKeysare now read from configuration. The four keys were documented and bound nowhere, so a lane budget inappsettings.jsonwas ignored and the worker leased every lane. A list that is present replaces the default. A per-lane budget that is not a number now fails at startup with the lane named, instead of being skipped. -
A webhook delivery now honors
Egress:AllowPrivateNetworkTargets. The subscription was saved under that setting, but the delivery job checked the target against the default egress options and dropped every delivery to a private address; onlyWebhooks:AllowPrivateNetworkTargetsworked. -
A queued run no longer fails, or runs a second time, when its session cannot be saved once after the model has answered. The handlers that end a queued turn (a queued run, the continuation of an approved tool call, the continuation of an interrupted run) saved the session inside their own failure path: one store error failed the job while its run said
Completed, or released the job and called the model again. The save is now tried again with a growing pause while the worker holds the lease. If it still fails, the job fails withtracon.job.session-save-failed, whatever its attempt limit, and the model is not called again. A run that asked for approval closesFailedwith the same code, because its approval cannot be answered without the session. -
A run that resumes after an approval decision and asks for another approval now puts that request in the approval mailbox. Only the queued-run handler wrote pending approvals, so the resumed run closed
AwaitingApproval,GET /api/approvals/pendinganswered an empty list, and the chain stopped. A run that continues an interrupted run had the same gap. Both now save the session first, list the approval second and announce it withapproval.pending, as a queued run does. -
The sub-agents and tools of a queued run now see the run’s labels, and those of a resumed or continued run see its user and labels. The job payload now carries
labelsnext touserId, and a resumed or continued run runs under the user and labels of the run it continues, not the approver’s. A workflow or agent batch that runs from the queue now carries the user and labels of whoever queued it:IJobDispatchercaptures the attributionIRunAttributionContextresolves at the time of the call, and an inbound trigger keeps the accept request’s (an agent run it starts recorded none either). A schedule’s payload can name one in an envelope,{"items": [...], "userId": "...", "labels": {...}}; an envelope that breaks a label limit is refused with400. A context you register yourself sees the replayed attribution only if it reportsAmbientRunAttributionScopeoutside a request; the reference sample’s demo context now does. -
An evaluation run whose cases have all run is no longer marked
Failedwhen the write that closes it fails. The handler now tries the write again for a third of the job lease; if the store still refuses it, the next attempt runs no case again and only repeats the write. -
A webhook delivery the receiver accepted is no longer sent again because Tracon could not record the result. The result writes are now tried again for a third of the job lease, a write that still fails is logged with a reference, and a retry that finds the delivery already settled sends nothing. Delivery stays at-least-once: deduplicate on the
X-Tracon-Deliveryheader. -
An eval case’s
contextnow reaches the agent, through the document channel ahead of the query, as a run withdocumentssends it. A case ran as its query alone and the context went to the evaluator only. The evaluator still receives it, and the case run now records the parameter values it was bound with. -
Promoting a run to an eval case now copies the run’s recorded input: the whole query, the documents it carried (into
context, one block per document) and the parameter values it was bound with (intoparameters). It read only theRunStartedevent, which is cut atMaxPayloadLength(8 KB by default), and dropped the documents and the values. Without a recorded input the query still comes from that event and keeps its…[truncated]marker; a run that carried documents with no recorded input, an attachment, or a message besides its documents and its request is now refused with422. Requirement: anIEvalStorestores the newEvalCaseDraft.ParametersinAddCaseAsync. -
On a SQL provider, a replay, a continuation or an approval resume of a run that carried documents now records them as documents. The stored document marker came back as JSON values the recorder did not recognize. The console’s case editor also keeps the line breaks of a case’s context.
-
A fallback on the binding of the compaction summarizer or of an
aiJudgeloop judge no longer rewrites the run.runs.model_idnamed the judge’s fallback model, the agent’s own tokens were priced at that model, and the run got aModelFallbackUsedevent its own model never caused. The run now keeps the agent’s model and gets no event, the side call’s tokens are priced at the fallback model that answered, and the switch is logged. -
POST /api/workflows/runs/{runId}/respondnow refuses an answer that does not fit the pending request’s port with400before the event stream opens, as its contract said, and opens no run. The answer was translated only inside the resumed run, after the200, and the new run endedFailed. A request recorded by 1.0.0-preview.3 or earlier does not carry its response type’s assembly; for it adatavalue that does not read into the type is still reported on the stream. -
Saving a
Sequentialnode list now checks that each node takes what the node before it passes on, and that the first node takes the run’sList<ChatMessage>. Microsoft Agent Framework drops a message no handler takes without an error, so such a chain was saved with200and every run endedCompletedwith no output. The save answers400naming both nodes and both types, and a stored chain that does not fit now fails its run before the first node. The site, theTracon.WorkflowsREADME and theAddWorkflowFunctionexamples showed astringfunction after an agent; they now takeList<ChatMessage>. -
Automatic run continuation no longer treats a workflow run or a sub-run as an agent turn. An orphaned workflow root was queued as an agent continuation under the workflow’s name, and a workflow step or a callable agent’s run was continued on its own, repeating its tools in its root’s session. Such a run now stays
Failedwith aRunContinuationBlockedevent; for a workflow run the event namesPOST /api/workflows/runs/{runId}/resume. A continuation queued before the fix is refused the same way. The refusal event is now written for a run of any tenant, andMaxAttemptsabove 1 counts every earlier continuation of another tenant’s chain. -
A retention policy with
archive: trueno longer deletes rows it never archived. Each batch was read in time order for theIArchiveSinkand then deleted as a separate batch in no order; with more matching rows than one batch, rows that never reached the sink were deleted (measured on PostgreSQL, SQL Server and SQLite). A run now deletes exactly the rows the sink received, by key, after the sink returns. A sink can receive a row more than once. -
Documentation now matches the code: a failed workflow node’s message rule and the causes of a
Canceledworkflow run, the request options a fallback link inherits (temperature,TopP,MaxOutputTokens,ResponseFormatand tools carry over; only provider settings do not), the five gates and the waiting quota of a tool call, the console editor keeping a harnessloop, and how a job handler learns about cancellation (JobContext.IsCancelledAsync). The XML ofJobHandlerKeys.Workflow,TraconOptions.SkillRoots,MaxOutputBytes(per stream),ToolCallUsage,ModelFallbackandModelBindingwas corrected. A resume from a checkpoint whose graph changed now says the graph’s structure differs instead of naming one cause. -
Every documented webhook event is now published.
workflow.request.pending,job.completed,job.failedandeval.completedwere defined, accepted on subscribe and listed on the site, but no code published them, and a workflow run produced norun.*event at all (its agent steps are child runs). A workflow root now publishesrun.completed/run.failedwith the run tree’s tokens and cost, and oneworkflow.request.pendingper request it waits on (ids only, no prompt). A job publishesjob.completedorjob.failedwhen it reaches that status (a cancelled job and a webhook delivery job publish nothing). An evaluation run publisheseval.completedwhen it completes, is cancelled, or fails on its last attempt. The console’s subscription form now also offersrun.score.low. -
Retention and data subject erasure now delete an attachment’s bytes in a registered
IAttachmentStorage. Retention’sattachmentstarget never readexternal_uri, so the bytes stayed in object storage for ever; it now deletes the bytes before the row, and keeps the row (and tries again on the next run) when the byte delete fails. Erasure deletes the bytes after the row deletes and before the audit write and the commit, and rolls back when a byte delete fails.IAttachmentStore.DeleteAsyncandDeleteBySessionAsyncalso delete the bytes first: a storage failure after the row was gone lost the location for good. Requirement: anIAttachmentStorage.DeleteAsyncmust succeed for a location that no longer exists. -
Retention no longer treats a workflow’s attachments as orphaned while a run of that session waits for input, is still running, or finished after the cutoff. A workflow session has no session row, so an image or a speech clip an agent step produced was deleted seven days after it was made, even while the run waited in
AwaitingInput. Ownership also requires the same tenant. -
The continuation that a tool-approval decision queues now runs on the agent version of the run that asked for approval, and counts in the same experiment arm. It ran the current version, so a version saved while the approval waited changed the instructions halfway through the turn. Code agents still run their current definition. An automatic continuation after a crash now keeps the interrupted run’s experiment arm, not only its version.
-
A queued run (
Prefer: respond-async) now uses the request’scultureand takes part in a running experiment. The worker resolved the agent with no culture and the current version, so a queued run always got the default instructions, ignored the experiment and counted in no arm. The arm is now assigned when the request is accepted, with the same key as a synchronous run (sessionId, else the run id), and the worker runs that arm’s version. The worker honors an arm in the job payload only when it matches a started experiment of the job’s tenant and agent with that exact version; otherwise the job fails and no model call is made. -
A store error after a job’s handler has finished no longer counts as a handler failure. The worker wrote the outcome inside the handler’s own
try, so one failed database call either failed a job whose work was done — a queued agent run (one attempt by default) becameFailedwhile its run wasCompleted— or, with attempts left, ran the handler a second time. The worker now retries the write with a growing pause while it can still prove its lease, and stops a third of the lease before that lease could run out; a write that keeps failing is logged as an error and the job takes the lease-expiry path. A store error while releasing a failed attempt, or while failing its last one, no longer leaves the jobRunninguntil its lease expires. In a batch, a store error while recording a finished item no longer records that item asFailed. -
A workflow run now carries its attribution the way an agent run does. The root built its run scope before it read
IRunAttributionContext, so every node and tool of a workflow readTraconRunContext.Current.UserIdandLabelsasnullwhile the run row carried them, and its quota record named no run and no user, so aquota.thresholdwebhook for a workflow carriedrunIdanduserIdasnull. -
A harness loop with an
aiJudgecriterion now records the judge’s tokens in the run’susage, cost and quota consumption. The judge calls its own model on every iteration it reaches, and the framework drops that response: the tree budget counted the judge, but the run record, the run’s cost and the tenant’s quota did not. -
The tokens of the context compaction summarizer are now priced at the summarizer’s own model. They were priced at the agent’s model, while the tree budget priced them at the summarizer’s.
-
The tree budget now charges a streamed model call with the sum of its usage reports. It kept only the last report, while the run record sums them, so a call that reports usage in more than one part was undercounted against
MaxTotalTokensandMaxTotalCost. -
A consumer’s own speech provider now works with
UseVoice()as documented. WithISpeechSynthesizerandISpeechTranscriberregistered beforeUseVoice()and no voice key, the first read of the voice settings threw:POST /api/voice/speakcalled the provider, saved the attachment, then answered500and left the attachment behind.Provider,ApiKeyandEndpointare now validated only while the built-in ElevenLabs client serves at least one of the two contracts; a registration added afterUseVoice()fails at startup with a message that says to register first. Three behaviors now follow the bound implementation instead of the ElevenLabs settings: prices are looked up under itsProviderName(Tracon:Pricing:Voice:{ProviderName}:{model}, and only the built-in client falls back to its default model), thespeaktool appliesISpeechSynthesizer.MaxCharactersPerRequestas the HTTP endpoint already did, andGET /api/voice/healthuses the synthesizer’s ownIVoiceHealthCheckor a voice-list call that reports itsProviderName. -
Speech audio saved by
POST /api/voice/speakis now named after its detected type (.ogg,.wav,.mp3) instead of always.mp3, and audio a live conversation saves withPersistAudiois no longer named.bin. Speech, conversation and image attachments now share one naming rule, and their timestamps carry ticks. -
The console can now answer a workflow request that it did not start. The run page sent the operator to “the workflow screen”, but that screen showed a request card only for a run it had started itself, so a run started over HTTP, by a job or in another tab could not be answered from the console at all. The workflow screen now opens on the run named by
?runId=. Both links on a run page carry it, and the screen writes its own run there, so a reload or a copied link keeps the run. ArunIdof another workflow, or of a run that is not waiting, shows a note instead of a card. The run page now also says that answering an already answered run opens a separate branch. -
A workflow node retry, the model fallback chain and an online evaluation judge no longer repeat a permanent client error.
RunErrorClass.ProviderErrorcovers every provider HTTP error,4xxincluded, and the node retry read only that class: a node that got a401fromEnsureSuccessStatusCode(), or a model call that got a404 (model_not_found), ranWorkflowNodeRetryPolicy.MaxAttemptstimes, and a retried409on a non-idempotent call could repeat its side effect. The fallback chain moved to the next provider on any4xxother than401/403that an SDK exception type carried, so the next provider got the same bad request, and an online evaluation job retried a judge whose model answered401or404. A failure that carries a permanent HTTP status (every4xxexcept408and429, read fromHttpRequestException.StatusCodeor fromHTTP nnntext in the exception chain) is now thrown on the first attempt, never moves to the next link, and fails the judge’s item without a retry. Behavior change: a registeredIRunErrorClassifiercan no longer make such a failure retry a workflow node; anIProviderRetryClassifierthat returnsRetrystill moves to the next link.runs.error_classvalues are unchanged. -
A harness agent on the OpenAI Chat Completions provider (
openai,azure-openaiand a compatible registration) no longer fails with400. The harness sent the provider’s hosted web search on every request (unknown_parameter: web_search_optionsfor a model that does not search; it is now off by default, see Changed), and a streamed run lost the conversation after its first tool call: the next request carried only the system message and the tool result (messages.[1].role). The harness’s history layer wrote a marker onto each streamed update, and the tool loop under it, holding the same object, took the conversation for one the service stores. Each update now reaches the harness as a copy. -
HarnessSettings.MaximumIterationsPerRequestnow bounds the tool-call loop. It reached the harness’s own loop, which never sees a tool call: the loop that runs the tools kept its default of 40, so a limit of 2 still made 7 model calls. The limit now reaches that loop and the loop of every fallback model, and at the limit the last call offers the model no function tools. A value below 1 is refused when the agent is compiled. -
A fallback model runs the agent’s tools with the agent’s
ModelBinding.AllowConcurrentToolCalls. The fallback link built its client from the provider and model alone, so concurrent tool calls turned off when the chain moved to a fallback. -
A workflow run whose node works quietly for a while is no longer cancelled by its own silence. The workflow stream wrote nothing between events, the console stops reading a stream after 30 seconds of silence, and a closed connection cancels the run - so any node that took longer ended the run
Canceled. Every streaming endpoint (agent and workflow runs, resumes and answers, and the OpenAI-compatible endpoints) now writes an SSE comment whenever it has written nothing forTraconEndpointOptions.StreamKeepAliveInterval(15 seconds by default; see Added). A workflow run that waits for aMaxConcurrentRunsslot now has its run record from the start: a run whoseRunTimeoutpassed while it waited closed the stream with no closing frame and left no record behind; it now endsCanceledwith the timeout error like any other timed-out run. Any other error on the workflow stream now ends it with anerrorframe instead of a dropped connection. -
The job queue now keeps its lease, attempt and cancel promises. A failed lease renewal stopped renewal for the rest of the job, so the lease ran out under a working handler and a second worker ran the job alongside it; renewal now retries on the next tick and runs at a third of the lease instead of half. A job whose worker died was leased again without checking its attempt limit, so a queued agent run (one attempt by default) ran twice; a job past its limit now fails without running, with the error code
tracon.job.attempts-exhausted. A cancelled job whose handler then threw went back toPendingand ran again, or becameFailed; a job that reached a final status no longer changes, and a job cancelled between its lease and its start does not start. -
A workflow step whose agent cannot answer now fails the workflow. A step that ran past
AgentGraph:ChildDeadline, was refused by the tree budget (MaxTotalRuns, tokens, cost or time), asked for a tool approval, or whose provider never answered returned the refusal sentence as its answer: the next node received that sentence as its input and the run endedCompleted. The run now endsFailed: a wait limit with the newTraconChildRunTimeoutException(classTimeout), the budget withTraconRunBudgetExceededException(classQuotaExceeded), a provider fault with the provider’s own error. An agent called as a tool by another agent still gets the refusal as text, so its model can react. -
A pending human request of a workflow run can no longer become impossible to answer. A long prompt - about 1,400 non-ASCII characters is enough - was cut at the event payload limit, the request disappeared from the pending list and
respondrefused it; the request is now listed with its prompt cut and marked, and it can be answered. Retention deleted the request event and the checkpoints of a run waiting for an answer; it now keeps them for as long as the run waits. WithKeepCheckpointsAfterCompletion = false, an answer - even one the port rejected - deleted the checkpoints the waiting run needs; the cleanup now leaves a session’s checkpoints in place while another run of the session waits or is running. -
A harness loop run now records the tokens and cost of every iteration. A non-streaming loop returned and recorded only its last iteration’s usage, and a streaming run of any agent recorded only its last model call - a loop, or a run with a tool round, undercounted its cost and the tenant’s quota. The usage of a streamed run is now the sum of its model calls.
-
Structured-response validation now checks the agent’s answer, not everything the response holds. With a loop, or with text the model wrote before a tool call, the validator received the joined text and rejected a valid final JSON as “not valid JSON”. It now validates the last assistant message that carries text.
-
A parameterized agent no longer runs with its placeholders unfilled. Only
POST /api/agents/{name}/runand evaluations bound parameter values; a queued run, a batch, a replay, the OpenAI-compatible endpoints, MCP, A2A, a workflow step, an agent called as a tool and voice sent the model the literal{{name}}, without even the defaults. Every path now binds the defaults. An agent with a required parameter that has no default is refused where the request enters -400withmissingParametersfor a queued run,400on the OpenAI-compatible endpoints, an error for an MCP or A2A call, a failed job for a batch - and fails before any provider call on a path with no request to refuse. A replay and an automatic continuation are refused for any agent that declares parameters: the recorded input has no parameter values, and the defaults would change the instructions. An evaluation of a parameterized definition registered in code now binds each case’s values. A parameterized definition registered in code (AddAgent(AgentDefinition)) is now checked and bound onPOST /api/agents/{name}/run; its parameters were refused there as unknown. -
A run pinned to an earlier definition version (an evaluation or an experiment) is now validated, priced and recorded as that version. The catalog compiled the pinned version but described it with the current one, so its response was validated against the current response format, and the run recorded the current model, cost and - for an evaluation case - version. An evaluation run now records the pinned version’s model.
-
GET /api/evals/runs/{id}no longer fails with500on the in-memory store when a case’s run threw. A free-form JSON field left unset (Scores,Checks, a job’sPayload) held no JSON at all and could not be written; it is now the empty array, as when it is set to nothing. -
A dependency that times out on its own no longer counts as the caller cancelling. An HTTP-backed store, sink, catalog, cache or identity seam reports
HttpClient.TimeoutasTaskCanceledException; 58 catch sites let that exception through as if the request had been cancelled, which broke each site’s own promise. Among the effects: the MCP and A2A startup check stopped the host when the agent catalog timed out, a run failed because a run store, event sink, response cache or attribution lookup timed out, an approved turn never ran because its “don’t ask again” rule could not be written, a trigger delivery whose queueing timed out could never be retried (the retry was refused as a replay), queued runs and evaluation runs stayedQueued/Runningforever, and image generation and replay answered500instead of their documented502. Every site now tells the two apart by the operation’s own token. -
An
IRunEventSinknow receives a run’s events in sequence order, as its contract states. A run writes from several places at once (concurrent tool calls, a child run, a workflow’s steps), and an event numbered later could reach the store and the sinks before an earlier one. Numbering, the store write and the sink dispatch of one event are now a single step. A sink must not append to the run it is handling: such an append throwsInvalidOperationExceptionand the sink is disabled for the run, instead of waiting for itself. The in-memory run store also returns events in sequence order. -
Editing a workflow that uses function nodes in the console no longer erases them. The editor opened such a workflow with an empty participant list, and saving it after adding an agent replaced the whole chain with that agent. The editor now lists the chain read-only, keeps the kind
Sequential, and sends the chain back unchanged. -
Editing a quota in the console keeps a disabled rule disabled. The form sent
enabled: trueon every save. The agent and the period of a rule are now locked while you edit it: they identify the rule, and changing them wrote a second rule. -
Editing a draft experiment no longer removes its canary policy when the in-memory store is in use. The SQL stores already kept it.
-
Saving a webhook subscription through
PUT /api/webhooks/{name}now resets its consecutive failure count with the SQL stores too, as the in-memory store already did. Before, a subscription that was disabled for failing and then enabled again went back to disabled on its next single failure. -
A live voice session, a voice conversation and an agent batch job now pick up an MCP server change on their next delegation, turn or item. Each resolved its agent once and ran with it until it ended, so after a server was edited or deleted mid-session every later call went to the closed connection (the model heard “Error: Function failed.”), and a server switched back to requiring approval ran its tools without asking. They now resolve the agent again each time; nothing is recompiled while nothing changed. A batch item whose agent was deleted meanwhile fails on its own.
-
A cached agent now sees an MCP tool’s description and input schema as the server last listed them. The compiled agent kept the tool definition of the moment it was compiled, so after a server changed a tool on the same connection, the model kept getting the old description and schema until the agent’s definition changed or the process restarted.
-
One failed
tools/listcall no longer takes an MCP server’s tools away. A server whose listing failed once lost all its tools at that refresh, and every agent that used one was refused as “not registered” until the next good refresh (five minutes by default). The last good list is now kept forTracon:Mcp:StaleToolRetention(default 15 minutes) after it was read, while the server stays registered with the same definition.TimeSpan.Zerokeeps the old behavior. -
A timeout set beyond what .NET timers accept now fails at startup with the option’s name.
CancelAfterrefuses more than about 49.7 days, and the validators refused only zero and negative values, so such a value threw where the timeout was armed: on every run of a workflow (Workflows:RunTimeout), every webhook delivery (Webhooks:Timeout), every live voice delegation (Voice:Live:DelegationTimeout), every MCP connect (Mcp:ConnectionTimeout, which aborted discovery for every tenant), at shutdown (Drain:Timeout), and after a skill script had started, which left the script running (Skills:Scripts:Timeout). The same check now coversTools:DefaultTimeout,Tools:ApprovalPresentationTimeout,Validation:McpTimeout,AgentGraph:ChildDeadlineandWaitTimeout(and an agent definition’ssubAgentsvalues, at compile time),Images:Timeout,Voice:Conversation:IdleTimeoutandMaxConnectionDuration, a tool’s own timeout, andRunEventPollInterval. A provider or voiceTimeout, which becomes an HTTP request timeout, must be at most about 24.8 days (int.MaxValuemilliseconds);TraconClientOptions.Timeoutlikewise. AWorkflowNodeRetryPolicywith a negative delay or multiplier is refused when the function is registered, and its growing delay is capped.tracon evalrefuses a--timeoutor--poll-intervalit cannot wait for. Values below one millisecond are refused too. -
On SQL Server, two requests that save the same new record at the same time no longer fail with a unique-key violation. Twenty stores wrote with an update-then-insert pair outside a transaction, so both requests could take the insert branch: among them sessions, runs, quotas, script grants, approval rules, MCP servers, tenants and the singleton lease. Each pair now runs inside a transaction, which costs two round trips on SQL Server and nothing on PostgreSQL or SQLite. A script grant, quota or approval rule saved with an empty string in a nullable key is no longer matched to the record saved with
nullthere; the HTTP surfaces already store an empty value asnull. -
A provider setting value of the wrong JSON type (for example text where
anthropic.thinking.budgetTokensexpects an integer) is now reported with the key it belongs to. Through the model provider registry the message was replaced with “The model provider request failed.”, soPOST /api/agents/validateand a compilation error did not name the key. -
A run that failed because the provider filtered its response (
ContentFiltered) now records the tokens the call spent in its usage, cost and quota entry. The run recorder writes no usage for a run that ends in an exception, so a filtered run said it cost nothing although its input was billed.
-
Function nodes and agent steps for a workflow graph built in code.
GetWorkflowFunction(name[, configure])binds a function registered withAddWorkflowFunctionandGetWorkflowAgentStep(workflow, agent[, configure])binds a catalog agent as a plain step, so a hand-built graph with a way back and a human gate gets what a stored chain’s steps have: the time limit, the retry policy, the budget gate, a cost line that names the node, the strict read of the settings (WorkflowFunctionStepOptions.Settings, or typed withSetSettings) and an agent step’s own timeout and version pin (WorkflowAgentStepOptions). Until now the wrapper that carries them was reachable only from a stored chain’s compiler, and the budget gate could not be built in consumer code. An agent bound withGetWorkflowAgentstill cannot be the target of a back edge (it runs only after a turn token), so the agent step is the way to put an agent where a message returns. An unknown function or settings that do not fit end the runFailedbefore its first node, naming the workflow and the node; a version pin that cannot hold fails the step by name before its model call. -
WorkflowRegistrationOptions.RunTimeoutandWorkflowRegistrationOptions.Parametersgive a workflow defined in code the run time limit and the parameter schema a stored definition has. The run timeout shortensTracon:Workflows:RunTimeoutor lengthens it up toMaxRunTimeout, and a value above the ceiling stops the host at startup, naming the workflow. With a schema,GET /api/workflowslists it, the run endpoint answers400early for an unknown or missing value, a queued run carries the values, and a run that is answered or resumed uses the recorded ones. Without one, every value is still refused as unknown. -
WorkflowFunctionOptions.MaxConcurrencylimits how many calls of a function run at the same time in the process, across workflows and tenants, in a stored chain and in a graph built in code. A call that finds every slot taken waits; the wait ends when its run is cancelled or reaches its deadline and is not part of the node’sTimeout; each attempt of a retried node takes a slot of its own; the budget gate is read after the slot is taken. The limit is per process, so replicas multiply it. -
A workflow run that the request expiry service fails now publishes
run.failed, with the payload any failed workflow run carries. Until now such a run was closed in the store and the webhooks heard nothing, so a pipeline driven by webhooks waited for a run that had ended. A run closedCompleted(every request answered) publishes nothing: its answer’s own run publishedrun.completed. -
A list of the human requests a tenant’s workflow runs wait on, and a time limit for them.
GET /api/workflows/requests(stateispending,answeredorall;workflowName,skip,take) lists one item per request, oldest first, across every workflow; it needs theReaderrole and read access to runs, and a registeredIRunAuthorizationHandlerthat refuses reading runs rejects it with403instead of filtering it.WorkflowPendingRequestgainsworkflowName,sessionId,expiresAt,answeredandlastResponseRunId, on this list and onGET /api/workflows/runs/{runId}/requests;IWorkflowRunnergainsListRequestsAsync, with a default that lists nothing. The same request id in the same session is one request: an answered run staysAwaitingInputand its continuation publishes the open requests again, so the item names the newest run that waits on it. The list is built from a newworkflow_requeststable (one migration for each SQL provider) that the engine writes when a run starts waiting and the respond endpoint marks answered; it holds no copy of the prompt, which is read from the run’s event. A run that started waiting before the migration has no row and is not in the list. The console lists the same requests in a Workflow requests panel on the Approvals screen. -
A workflow can give its human requests a time limit.
WorkflowDefinition.RequestTimeout(requestTimeoutonPUT /api/workflows/{name}, the Answer time limit field in the console’s editor,WorkflowRegistrationOptions.RequestTimeoutfor a workflow defined in code) is from one minute to 365 days and counts from the moment the run published the request. A background scan (Tracon:Workflows:RequestExpirationScanInterval, one minute, andMaxRequestExpirationsPerScan, 100; one instance of a cluster scans) closes the run when its last request passes:Failedwith the error typeWorkflowRequestExpired(error classTimeout) when something went unanswered,Completedwithout an error when every request had been answered. An answer to a closed run is refused with400, as for any run that is notAwaitingInput. The run keeps its usage, cost and event count. A run that waits keeps the limit of the version it started with. This is what lets retention release what a waiting run keeps: its request event, checkpoints and run input row were never deleted while it waited, and an answered run waits too. The newworkflow_requestsretention target (Tracon:Retention:WorkflowRequests, 30 days after the request expired) deletes the closed rows of the list. Behavior change:requestsjoinsfunctionsas a workflow name that cannot be saved, becauseGET /api/workflows/requestsmatches beforeGET /api/workflows/{name}. -
Tracon.Testing.Contracts.Xunit:WorkflowRequestStoreContractforIWorkflowRequestStore, the seam behind the list; a custom store passes it the way it passes the other store contracts. The request store also lists the runs a stopped process left waiting (IWorkflowRequestStore.ListStrandedAsync): the scan closes a request before its run, and the instance that takes the scan lease next closes the runs whose requests were closed and which still wait.WorkflowDefinitionStoreContractgainsThe_request_timeout_survives_the_round_trip; a customIWorkflowDefinitionStorethat keeps its own copy of a definition must carryRequestTimeout. -
One answer per workflow request.
WorkflowResponseMode(Branch, the default, andSingleAnswer) is set on a stored definition (WorkflowDefinition.ResponseMode,responseModeonPUT /api/workflows/{name}) or on a workflow defined in code with the newAddWorkflow(name, factory, Action<WorkflowRegistrationOptions>)overload, and is reported onWorkflowDescriptor. InSingleAnswer, two identical answers to one request — a double click, a retried call — start one run: the second follows it (the same SSE frames read from the stored run, or the same202withPrefer: respond-async), and a different answer gets409, which names neither the run nor the other answer. An answer whose run failed or was cancelled before the workflow received it can be given again and starts a new run; a queued answer whose job is still waiting or running is followed instead, because the job’s recovery owns that work. Two answers that arrive together may both open a run before one of them claims the request: the other closes its own run asCanceledwith the error typerequest_already_answeredand no events (no webhook, metric or event sink fires for it), and its audit entry names that run. That run is still a run: it is listed byGET /api/runsand counted inRunStatistics.CanceledRuns, so it is part of theErrorRatedenominator. A waiting run is answered under the mode of the definition version it recorded.Branchkeeps opening a new run for every answer. A queuedSingleAnsweranswer whose job was never written (the process stopped after the answer was recorded and before the job was queued, or the queue refused the job and itsQueuedrun could not be closed) is finished by a repeat of the same answer: the repeat writes the job and follows the run, and two repeats at the same time write it once. Known limit: when a process stops in the middle of a streamedSingleAnswerrun, the run staysRunningand a repeat of the answer follows it forever. Only run reconciliation (Tracon:RunReconciliation:Enabled, off by default) closes it: reconciliation closesRunningruns whose heartbeat stopped and reads noQueuedrun. Turn reconciliation on where such a request must stay answerable after a crash. -
GET /api/workflows/runs/{runId}/responseslists the answers given to a run’s requests: who answered, when, the run each answer started and its state. The records live in the newIWorkflowResponseStore(in memory by default; the newworkflow_responsestable on PostgreSQL, SQL Server and SQLite). A record is removed only with its source run, by a data subject erasure; no retention target deletes runs, so the table grows with the run history. A store implementation is checked with the newWorkflowResponseStoreContractinTracon.Testing.Contracts.Xunit. -
RunEventType.WorkflowResponseApplied(workflow.response-appliedonGET /api/runs/{runId}/events): the run an answer starts names the run it answered and the request id right after it starts, even withRecordToolPayloadsoff. The console’s run timeline links it back to the source run. -
The console shows which pending requests already have an answer, links the run that answer started, says when a one-answer workflow refused a different answer, and has a “One answer per request” switch in the workflow editor and the version comparison.
-
The live stream of an agent run (
POST /api/agents/{name}/runwithoutIdempotency-Key) now says when the answer was cut off. A plain-text answer the model stopped at its output token limit or at its context window completes, and itstracon.output.truncatedevent (RunEventCustomTypes.OutputTruncated) was only onGET /api/runs/{id}/events; a client that read the live stream took the partial answer for a complete one. The stream now sends that event as acustomframe right beforedone, always, with the same data/eventsserves. Each recorded event is one frame: a harness loop whose iterations were cut sends more than one, and a tool turn cut off in the middle of the run sends none. With the quota notice on, both notices arrive in record order. A client that reads frames by name and ignores unknowncustomTypevalues needs no change. No event, and so no frame, is written while run recording is off. -
TraconFinishReasons.ContextWindowExceeded: the finish reason a model provider reports when the conversation and the answer filled the model’s context window. Tracon treats it as a cut-off answer and names the window. -
RunErrorClass.TenantProviderRefused(17): the tenant’s provider rules refused the call before any provider was called. -
TraconTenantProviderRefusedException: the one exception every path throws when the tenant’s provider rules refuse a call (its egress policy, a credential binding that cannot be resolved, a provider that cannot take the tenant’s own key). Catch it to tell the tenant’s rule from a provider failure. -
CurrencyCountonRunTreeCost,RunNonTokenCost,RunStatistics,TimeSeriesPointandExperimentVariantResult, andJudgeCostCurrencyCountonOnlineEvaluationSummary: how many currencies the runs behind a money total are priced in. Above one, the total carries no amount (see Fixed). -
Skill scripts can run outside the Tracon host. Register an
ISkillScriptExecutor(new inTracon.Abstractions, withSkillScriptExecutionRequest,SkillScriptOriginandSkillScriptExecutionResult, which moved there from an internal Core type without itsDuration) to run an approved script in a container, a micro-VM or a remote runner. The gates, the content pin, thescript.runaudit entry, the concurrency quota, a wait limit ofTimeoutplus five seconds and the output cap stay with Tracon; the executor decides only where the script runs.UseSkillScriptsregisters the built-in executor, a local process as before, as a default that a host’s own registration replaces in either order;RequireCustomBinding<ISkillScriptExecutor>()stops the host while the built-in one resolves.Tracon.Testing.Contracts.XunitaddsSkillScriptExecutorContractandContractCoverage.ScriptContracts. See the new guide Write your own script executor. -
Multi-speaker dialogue synthesis.
ISpeechDialogueSynthesizervoices an ordered list of turns, each with its own voice, in one request (SpeechDialogueRequest,SpeechDialogueTurn) and returns one audio result with each turn’s time range (SpeechDialogueAudio,SpeechDialogueSegment) and the provider’s limits (SpeechDialogueCapabilities). Code takes it from the newISpeechProviderResolver.GetDialogueSynthesizerAsync(name), which reads the capability from the registered synthesizer and applies the tenant’s egress policy and own-key rules; nothing registers the capability in the container. The built-in ElevenLabs client implements it withtext-to-dialogueand its timestamped variant, refuses a request over 10 voices orVoiceOptions.MaxDialogueCharactersPerRequest(new, default 2,000) before the call, and usesVoiceOptions.DialogueModelId(new) or the provider’s default model, neverSynthesisModelId. -
Provider request settings for speech.
SpeechRequest.ProviderSettingsandSpeechDialogueRequest.ProviderSettingscarry keys under the provider’s prefix; the built-in client readselevenlabs.stability,similarityBoost,style,useSpeakerBoost,speed,seed,previousText,nextTextandpreviousRequestIds(ElevenLabsProviderNames) and refuses an unknown key, a wrong type, a value out of range, and on a dialogue a key the dialogue service does not take.ModelProviderSettings.Validategains an overload that takes the settings dictionary, for a speech provider of your own. -
SpeechAudio.ProviderRequestIdcarries the provider’s id for a generation (the ElevenLabsrequest-idheader), to pass inelevenlabs.previousRequestIdsfor the next part of a long script. -
A workflow function node can be bounded by a timeout.
AddWorkflowFunction<TInput, TOutput>gains an overload that takesAction<WorkflowFunctionOptions>, andWorkflowFunctionOptions.Timeoutsets the registration’s timeout; a node’s ownWorkflowNodeReference.Timeout(timeoutinPUT /api/workflows/{name}) wins over it. When it passes, the handler’s token is cancelled and the run endsFailedwith the newTraconWorkflowFunctionTimeoutException(error typeworkflow_function_timeout, classTimeout). The run stops waiting even for a handler that ignores its token; its late end is logged and its result is discarded. With a retry policy the timeout applies to each attempt.GET /api/workflows/functionsshows the registered timeout (WorkflowFunctionResponse.Timeout). -
An agent node of a
Sequentialnode list can carry its owntimeout, which replacesTracon:AgentGraph:ChildDeadlinefor that step; the hard cutoff stays 30 seconds later. Past it the run endsFailedwithchild_run_timeout, and the message names the node. -
WorkflowDefinition.RunTimeout(runTimeoutinPUT /api/workflows/{name}): a definition’s own run timeout. It may shortenTracon:Workflows:RunTimeout, and may be longer only up to the newTraconWorkflowOptions.MaxRunTimeout(Tracon:Workflows:MaxRunTimeout, unset by default); a longer one is refused with400on save, and a stored one above a ceiling lowered later is cut to it with a warning. -
A job’s time limit:
JobRequest.MaxDurationandJobRecord.MaxDuration, andTraconSchedulingOptions.MaxDurationByHandlerKey(Tracon:Scheduling:MaxDurationByHandlerKey) for every job of a handler key; the job’s own value wins. Past it the worker cancels the handler’s token and the attempt fails withJobErrorCodes.MaxDurationExceeded(tracon.job.max-duration-exceeded); the job is leased again with the full limit while attempts are left, and a handler that returns normally after the limit completes the job. The built-in workflow handlers end the work instead: the run closesCanceledwith the error typejob_max_duration_exceededand is never continued, a queued run’s job fails at once, and a batch fails the item. New columnjobs.max_duration_ms(migrations PostgreSQL0057, SQL Server0045, SQLite0044).JobStoreContractaddsA_jobs_own_time_limit_survives_the_round_trip_and_the_leaseandA_job_without_a_time_limit_reads_back_none;WorkflowDefinitionStoreContractaddsNode_timeouts_and_the_run_timeout_survive_the_round_tripandSaving_again_without_timeouts_clears_them. A third-party store must keep both fields. -
The console’s workflow editor has a timeout field on each step of a chain with a function step, and a run timeout field.
-
A workflow run, resume or answer can be queued:
POST /api/workflows/{name}/run,POST /api/workflows/runs/{runId}/resumeandPOST /api/workflows/runs/{runId}/respondacceptPrefer: respond-asyncand answer202 Acceptedwith the run id (AcceptedRunResponse, now in the OpenAPI document and both generated clients). The caller is checked before the202as on the streamed call, the run’s row isQueueduntil a worker starts it, andPOST /api/runs/{id}/cancelcancels it from the queue. A queued run no longer depends on the connection, so a client that stops reading does not cancel it. The job runs under the new built-in keyJobHandlerKeys.WorkflowRun(tracon.workflow-run), which no schedule can use. -
Idempotency-Keyon the three workflow run endpoints, accepted only together withPrefer: respond-async; a key on a streamed call is refused with400. -
TraconWorkflowOptions.ResumeInterruptedRuns(off by default) andMaxResumeAttempts(default 1): a queued workflow run or atracon.workflowbatch item interrupted by a crash, a deploy or a graceful stop continues from the latest checkpoint in its chain when its job is leased again, as a new run whoseContinuedFromRunIdnames the interrupted one. The step that was running repeats. Turning it on withoutEnableCheckpointingfails at startup. A queued run’s job gets2 + MaxResumeAttemptsattempts while it is on: the job’s last attempt never starts a continuation, because the worker fails a lease past the limit before the handler runs; it closes the chain instead. A batch job keeps its own attempt limit and needs2 + MaxResumeAttemptsattempts to get every continuation. -
RunQuery.ContinuedFromRunIdandGET /api/runs?continuedFromRunId=walk a continuation chain forward. -
A structured
tracon.workflowbatch item may name asessionId: the run’s execution session, which itsrun.completedwebhook and its continuations carry. An agent batch item that names one fails alone. -
RunStoreContractaddsA_second_start_without_a_continuation_link_keeps_the_first_one,A_queued_workflow_row_keeps_its_workflow_session_and_attribution_when_it_starts,Query_filters_by_the_run_a_continuation_continuesandQuery_by_continuation_does_not_return_another_tenants_run.JobStoreContractaddsA_pending_report_marks_the_item_and_counts_nothingandA_report_for_a_finished_item_changes_nothing. A third-party store needs the first and the last of these: the second start of a run keeps a storedContinuedFromRunId, and a report for a finished item is ignored. -
Named speech providers:
AddSpeechSynthesizer(name, factory)andAddSpeechTranscriber(name, factory)register more text-to-speech and speech-to-text engines next to the default one, andISpeechProviderResolver(registered byAddTracon()) returns one by name after the tenant’s checks — for a workflow function node, for example.SpeechProviderDescriptorandSpeechProviderKinddescribe a registration. A name is 1 to 64 characters ofa-z,0-9,.,_and-; a repeated name throws at setup. -
GET /api/voice/providerslists the registered speech providers (new in the OpenAPI document and both generated clients).POST /api/voice/speaktakes an optionalproviderandGET /api/voice/voicesan optional?provider=; an unknown name answers400, a request without a name when no default is registered501. -
Tenant credentials for speech providers:
ISpeechSynthesizerandISpeechTranscribergainSupportsTenantCredentials(defaultfalse),SpeechRequestandSpeechTranscriptionOptionsgainCredential, andISpeechSynthesizergainsListVoicesWithCredentialAsync, whose default refuses a credential. All are default interface members or new init properties, so an existing implementation keeps compiling. -
SpeechAudio.ModelIdandSpeechTranscript.ModelIdcarry the model that did the work, andIVoicePricingReadergainsForSynthesis(providerName, modelId, characters)andForTranscription(providerName, modelId, duration)(defaultnull). A speech call is priced by the model the provider reports, then the one the request named; the built-in client reports its model. -
IWorkflowRunner.ValidateResponseAsync: checks an answer against the pending request before the stream opens. A default interface member, so an existing implementation keeps compiling. -
IRetentionStore.ArchiveBatchAsyncreads one batch, hands it to the archive, and deletes exactly that batch. Its default implementation keeps the old two-call behavior so an existing store compiles, and can delete rows it did not archive — override it.RetentionStoreContractaddsArchive_batches_delete_only_the_rows_they_archived,A_failed_archive_write_deletes_nothingand aSeedOldRowAsync(tenantId, startedAt)hook. -
EvalCaseDraft.Parameters: the parameter values a promoted run was bound with.EvalStoreContractaddsAddCaseAsync_stores_the_parameter_values_of_a_promoted_run. -
RunStoreContract.ClaimOrphanedRunsAsync_reports_the_kind_and_the_tree_position_on_the_claimed_record: a customIRunStorereturnsKind,ParentRunIdandDepthon a claimed record, because continuation decides on them. -
WebhookEvalSummaryandWebhookEventPayload.Eval: the payload of theeval.completedwebhook. -
WebhookRunSummary.Kind:AgentorWorkflow, on everyrun.*webhook. -
OpenAIProviderNames.SettingsPrefix(openai) andOpenAIProviderNames.SupportedSettings(empty). -
HarnessSettings.EnableWebSearch: turns on the model provider’s hosted web search for a harness agent. -
TraconEndpointOptions.StreamKeepAliveInterval: the longest a streaming endpoint stays silent before it writes an SSE comment. Default 15 seconds; keep it below the shortest idle timeout between the server and its readers, and below 30 seconds while the console is in use. -
JobErrorCodes.AttemptsExhausted(tracon.job.attempts-exhausted). -
TraconChildRunTimeoutException(error typechild_run_timeout, classTimeout), withAgentNameandTimeout. -
A harness loop can fail the run instead of returning a response no criterion accepted.
LoopSettings.OnExhausted(the ceiling is reached while a criterion still asks for another iteration) andLoopSettings.OnCriterionError(a criterion throws) take aLoopOutcome:ReturnLastResponse, the default and today’s behavior, orFailRun. Exhaustion fails withTraconLoopUnsatisfiedException(error typeloop_unsatisfied, new classRunErrorClass.LoopUnsatisfied = 15); a criterion error fails with the criterion’s own error. A workflow stops at that agent. WithOnExhausted = FailRunthe turn at the ceiling is evaluated before the run is failed - one more evaluation, which foraiJudgeis one more judge call - and theLoopIterationCompletedevent gainsfailsRun. JSON takes the names ("onExhausted": "FailRun"); an unknown name is refused with400. A run the loop fails keeps the usage and cost of every turn it ran. -
TraconMcpOptions.StaleToolRetention. -
ILiveVoiceProvider.SupportsTenantCredentialsandILiveVoiceProvider.AttachSessionAsync(LiveVoiceAttachRequest), both with default implementations, plusLiveVoiceCreateRequest.CredentialandLiveVoiceAttachRequest. A live voice provider that setsSupportsTenantCredentialsreceives the tenant’s own key on both calls of a session; one that does not is refused for a tenant with its own binding. -
ToolApprovalRule.SourceFingerprint. -
A workflow can declare run parameters, and a run carries values to every agent step.
WorkflowDefinition.Parameters,WorkflowDescriptor.Parameters,WorkflowSaveRequest.Parameters,WorkflowRunRequest.Parameters,WorkflowRunHttpRequest.ParametersandGetRunParameters()(an extension onIWorkflowContext, for a function node). A step receives the values its own schema names; an invalid value is a400before the stream opens, and a participant whose required parameter nothing supplies fails the run before its first model call. A resume or an answer to a waiting run reads the values back, and is refused when the workflow now declares parameters and the run recorded none. -
A queued agent run (
Prefer: respond-async) acceptsparametersanddocuments, and a batch item can be a structured object ({"message", "parameters", "documents"}) fortracon.agent-batchandtracon.workflow.jobs.payloadand the item inputs are not content-protected and now hold these values in plain text. -
RunInputRecord.Parameters,ProtectedColumn.RunInputParameters(10) and a nullablerun_inputs.parameterscolumn on all three storage providers (PostgreSQL0056, SQL Server0044, SQLite0043).nullmeans nothing was recorded and{}means the run had no values; the retention policy keeps the row of a run waiting for a human. -
RunReplayRequest.Parameters: values laid over the ones the source run was bound with. -
A workflow function can take settings, so one function serves several workflows with a different configuration in each.
AddWorkflowFunction<TInput, TOutput, TSettings>(two overloads: a plain one and one that takes anAction<WorkflowFunctionOptions>for the description and the retry policy),WorkflowNodeReference.Settings(a JSON object, ornull),WorkflowFunctionDescriptor.SettingsTypeandSettingsSchema,WorkflowFunctionResponse.SettingsSchema(GET /api/workflows/functions) andTraconWorkflowOptions.MaxNodeSettingsLength(16 KiB). A save reads the settings intoTSettings: an unknown field, a missingrequiredmember, a value of the wrong type and anullwhere the member does not allow one are a400that names the node and the member, as are settings on an agent node, on a function that declares none, or that are not an object. The read repeats when a run starts, so settings saved against an older shape fail the run by name before any node runs. A change applies to the next run, and an older checkpoint stays usable. Settings are stored in the definition and the audit trail: never put a secret in them. Thesettingsmember of a stored node is also part of the workflow list and the generated clients. -
The console builds the chain of a
Sequentialworkflow: add an agent or a function (fromGET /api/workflows/functions), reorder, remove, and edit a function step’s settings as JSON with the function’s fields listed under it. The editor writesnodeswhen the chain holds a function andagentNamesotherwise, tells you when a save changes that, and warns that a run waiting for input cannot be resumed after a change of pattern, steps or order. The server accepts both forms, so a definition saved over HTTP in the other form keeps loading. -
AddLoopEvaluator(kind, Func<IServiceProvider, LoopEvaluator> factory): a loop stop criterion created from the application’s root services. The factory runs once, on the first compilation of an agent that names the kind, so it can takeIAgentCatalog; a factory that throws is not cached and fails that compilation with the kind named. EachAddLoopEvaluatoroverload has a variant with adescription, which the console shows next to the kind. -
GET /api/loop-criteria(LoopCriterionCatalogResponse,LoopCriterionKindResponse,LoopJudgeModelResponse, in the OpenAPI document and both generated clients): the built-in and code-registered loop criterion kinds with their descriptions, and theaiJudgejudge model’s provider and model, ornull. Reader role,AgentsReadscope. Listing runs no factory. -
The console’s agent editor edits the harness loop: criteria (add, remove, reorder, per-kind fields), the iteration ceiling, the fresh-context switch and both outcomes. A kind the host does not list is kept and marked. The version diff shows the loop.
-
A response the model stopped at its output token limit is now its own outcome. A structured (
JsonorJsonSchema) response fails the run with error typeoutput_truncatedand the newRunErrorClass.OutputTruncated(TraconOutputTruncatedExceptionin-process); no structured-response repair turn runs, and theStructuredResponseRejectedevent carriesrepairSkippedTruncatedwhen one was left. A plain-text response completes and the run writes atracon.output.truncatedevent (RunEventCustomTypes.OutputTruncated). Only the turn that ends the run decides; a tool call cut off in an intermediate turn does not. The tokens of the cut-off call count in the run’s usage and cost. The new enum member is in the OpenAPI document and both generated clients. -
Anthropic provider settings
anthropic.effort(output_config.effort:low,medium,high,xhigh,max),anthropic.thinking.mode(enabledoradaptive),anthropic.promptCaching.ttl(5mor1h) andanthropic.promptCaching.placement(requestorsystem, which marks the end of the agent’s instructions so requests with different documents share the cached prefix). A wrong value, a cache setting withoutanthropic.promptCaching = true, and a thinking budget withadaptivemode are errors. Nothing changes for an agent that sets none of them. -
ModelDescriptor.SupportsSampling, read from a provider’sModelscatalog entry. When it isfalse, a definition that setsTemperatureorTopP(on the binding or a fallback link’s model) fails to compile, andPOST /api/agents/validatereportssampling_unsupportedatmodel.temperatureormodel.topP.GET /api/modelsreturnssupportsSampling;nullmeans unknown and checks nothing. -
POST /api/agents/validatewarns withoutput_truncation_riskwhen a binding leavesMaxOutputTokensempty, the provider sends a lower default limit, and the agent asks for a structured response or high reasoning effort. A provider tells its default throughModelOutputLimit, returned from the chat client’sGetService; the Anthropic provider does. -
ModelProviderSettingsContract.InvalidSettingValueand the scenarioA_supported_setting_with_an_invalid_value_is_rejected: a provider that declares a key with a closed set of values proves that a wrong value is rejected and named. It is skipped when the property is leftnull. -
A workflow function node can report what it spent beyond tokens. The new extension
IWorkflowContext.ReportUsageAsync(ToolCallUsage)(inTracon.Workflows) records one item: a text-to-speech character, a render second, a minute of audio. It returnsValueTask<bool>and does not throw except for anullargument; it returnsfalseoutside a Tracon workflow run, for an invalid item (an emptyUnit, a negative quantity or amount, or a value beyond what its column holds: a unit of 64 characters, a currency of 16, a provider of 100, a model of 200, a quantity above 9,999,999,999, an amount above 1,000,000,000) and once the run holds 1,000 items. An item is written at once and is not cancelled with the run, so a cancelled or failed run keeps what its node had spent, and a retried node keeps the items of its failed attempts. The item names the node that reported it; a custom executor in a graph built in code reports with no node name. An item reported after the run closed (a node that outlived its timeout) stays in the run’s detail and does not change the total, the quota entry or the webhook. -
A run has a non-token cost, a total of its own beside the token cost.
RunNonTokenCost(Amount,Currency,IsEstimated,UnsummedItemCount) is onRunRecord.NonTokenCost(the run itself) andRunRecord.TreeNonTokenCost(the run and every run below it, the run counted once), andRunCompletion.NonTokenCostcarries it to the store when a run closes.nullmeans the run reported nothing, which is not zero; anAmountofnullwith a positiveUnsummedItemCountmeans items were reported and none could be priced. One rule decides what adds: an item adds only when it has an amount inTracon:Pricing:Currency(an item with no currency label is in it). Items with no amount, or in another currency, are listed and counted inUnsummedItemCount, never added or converted, and a quantity is never added across units. An estimated item adds and raisesIsEstimated. Tracon never prices a node’s work: the node reports the amount it computed.RunSpend.Total(RunCost?, RunNonTokenCost?)andRunSpend.Total(RunTreeCost?, RunNonTokenCost?)are the one place the token cost and the reported amount are added; amounts in two currencies are never added. -
ToolCallUsage.ProviderandToolCallUsage.Model, names for the reader of the record that take no part in a total. The built-inspeak,transcribeandgenerate_imagetools fill them. A tool still reports withTraconToolUsage.Report, and its measurement now also joins the run’s non-token total the moment the tool reports it. -
GET /api/runs/{runId}/usage(Reader role,runs:readscope) lists the items a run’s function nodes reported, in the order reported. It lists the run’s own items, not its children’s; a tool’s measurement stays onGET /api/runs/{runId}/tools. An unknown run, another tenant’s run and a denied run all answer the same404.GET /api/runsandGET /api/runs/{runId}carrynonTokenCostandtreeNonTokenCost. All of it is new in the OpenAPI document and both generated clients (TraconListRunNonTokenUsageAsync,RunNonTokenCost,NonTokenUsageRecord). -
NonTokenUsageRecordand theIRunStoremembersRecordNonTokenUsageAsyncandListNonTokenUsageAsync. Three migrations add arun_non_token_usagetable (its rows are deleted with their run), four snapshot columns onruns(non_token_cost,non_token_currency,non_token_estimated,non_token_unsummed) andusage_providerandusage_modelontool_invocations: PostgreSQL0058, SQL Server0046and SQLite0045. Existing rows keepNULL: nobody reported a number for them. -
The console shows a tool call’s measurement (quantity and unit, the amount or “unpriced”, “estimated”, provider and model) and, for a run that reported anything, a “Non-token usage” panel ending in “Reported in this run”, “Reported in the whole tree” and “N not added”.
-
The
tracon.recording.stagetag has a new value,non_token_usage: an item a function node reported could not be recorded. Alert rules that list the stages need it. -
Tracon.Testing.Contracts.Xunit:NonTokenUsageContract, and new scenarios inRunStoreContractandToolInvocationContractfor the snapshot, the tree total and a measurement’s provider and model. -
A stored workflow keeps its version history. Every save appends a version and a rollback appends the old content as a new one; nothing is rewritten.
GET /api/workflows/{name}/versions(Reader) lists the full snapshots, newest first,GET /api/workflows/{name}/versions/{a}/diff/{b}returns two of them asleftandright, andPOST /api/workflows/{name}/rollback(Admin, bodyWorkflowRollbackRequest) rolls back. A rollback is judged like a save: a version that names a function that is no longer registered, settings that no longer fit, or an agent version that no longer exists answers400and writes nothing. An unknown version is404, a workflow defined in code is409(its versions read404). The audit trail recordsworkflow.rollbackwith the version it came from and the one it wrote. A version number is never reused: deleting a workflow keeps its history and a workflow saved again under the same name continues the numbering. New in the OpenAPI document and both generated clients (TraconListWorkflowVersionsAsync,TraconGetWorkflowVersionDiffAsync,TraconRollbackWorkflowAsync,WorkflowVersionDiffResponse,WorkflowRollbackRequest). -
IWorkflowDefinitionStore.GetVersionAsync,ListVersionsAsyncandRollbackAsync. Three migrations add theworkflow_definition_versionstable (no foreign key toworkflows): PostgreSQL0059, SQL Server0047and SQLite0046. Each stored workflow starts its history at the version it has today; the versions it had before were never kept. A save writes the current row and its history entry in one transaction. -
A workflow run’s row records the version of the definition it was compiled from (
RunRecord.AgentVersionof the workflow’s root row;nullfor a workflow defined in code and for a row written before this release). A resume or a response is stamped with the version it ran. -
definitionVersiononPOST /api/workflows/runs/{runId}/resumeand.../respond(WorkflowResumeHttpRequest.DefinitionVersion,WorkflowRespondHttpRequest.DefinitionVersion,WorkflowResumeRequest.DefinitionVersion,WorkflowRespondRequest.DefinitionVersion) names the stored version to continue with. A version the workflow does not have is400before a stream opens or a job is queued, and throws before any run row is opened when the engine is called directly; a run is never continued with the current definition in its place. A queued resume and a queued response carry it. -
WorkflowNodeReference.AgentVersionpins an agent step to one version of that agent. The step runs that version’s instructions, model and tools while the agent is edited, and its child run row records the version. Only an agent node in thenodesform can pin; a function node, a value below 1, an agent defined in code and a version the agent does not have are refused with400when the workflow is saved and when it is compiled. The pin is not part of the step’s identity, so changing it does not make a waiting run’s checkpoint unusable. The agent’s own sub-agents and skills still resolve their current version. -
The console lists a workflow’s versions, compares two of them and rolls back, and an agent step in the chain editor can pin a version of a stored agent.
-
Tracon.Testing.Contracts.Xunit: new scenarios inWorkflowDefinitionStoreContractfor the history, the numbering after a delete, rollback, concurrent saves, tenant isolation, a pin’s round trip and cancellation, and the optionalSupportsRawHistory,ExecuteRawAsync,QualifiedWorkflowHistoryTableandRawNowExpressionhooks a SQL provider overrides to prove that a failed history write leaves the current definition as it was.
Changed
Section titled “Changed”-
The agent step of a workflow, in a stored chain and in a graph built in code, now asks the agent for its whole answer instead of a stream. The step never forwarded a delta to the workflow’s feed, so the stream bought nothing, and the streaming path has no structured-response repair. Behavior change: a step whose model answers JSON that does not fit the agent’s schema is asked again up to
Tracon:StructuredResponse:MaxRepairAttempts(before, the broken answer reached the next node), and the step’s ownrunsrow hasIsStreaming = falseand amessage.completedevent beside themessage.deltait already had. Its tokens, cost and input are recorded as before. A loop agent still hands the next node its final iteration only. -
A workflow function node no longer starts once the call tree’s cost or time budget is spent. Until now the budget refused a model call and a new agent step but let a function node run and do its paid work (speech, a render, a GPU minute), and a chain of function nodes only never stopped at all. Behavior change: with
Tracon:AgentGraph:MaxTotalCostorTracon:AgentGraph:MaxDurationset, the next function node does not start once the tree has passed either; the run endsFailedwithrun_budget_exceeded(classQuotaExceeded), the message names the node and the setting to raise, and no later node runs. Each attempt of a retried node is checked, and the refusal is never retried, whatever the registeredIRunErrorClassifiersays.MaxTotalTokensdoes not stop a node (it is on by default and a function node spends no tokens), and a host that sets neither ceiling is not affected. A node that is already running is never stopped, so a tree can pass its ceiling by as much as its last node spends. A node that spends nothing and has to run afterwards, such as a step that stores what the budget has just paid for, sets the newWorkflowFunctionOptions.StartsWhenBudgetIsSpenttotruein its registration; an operator cannot set it in a definition. A run built in code withAddWorkflowand its own executors is not gated: readTraconRunContext.Current?.Budgetthere. -
POST /api/workflows/runs/{runId}/respondchanges in the defaultBranchmode too. The run an answer starts now has its row opened (Runningfor a streamed answer,Queuedfor a queued one) before the answer is recorded and before the stream starts, and the answer’s record is written before the run is handed over. Behavior change: when the record cannot be written (workflow_responsesunavailable), the answer is refused with500and the opened run is closedFailedwith the error typeanswer_not_recordedand no events; before, the answer was not recorded at all. A run store that cannot open the row refuses the streamed answer with500too; before, the engine’s run writer logged that failure and the run went on unrecorded. Every run an answer starts now carriesWorkflowResponseAppliedat sequence 1, so its later events are numbered one higher than before. -
Behavior change: the in-memory job store (the default
IJobStore) refuses a job whose id is already queued, as the PostgreSQL, SQL Server and SQLite stores’ primary key does; before, it replaced the queued job, which could set a leased job back toPending. TheIJobStore.EnqueueAsyncdocumentation now states the refusal, andJobStoreContractchecks it with a new case. A queued run whose enqueue is refused because its job is already there (another call wrote it) is no longer closed asFailed; the call answers with the job that is there. -
AddWorkflow(builder, name, factory, string? description = null)is now two overloads,AddWorkflow(builder, name, factory)andAddWorkflow(builder, name, factory, string? description), beside the new one that takesAction<WorkflowRegistrationOptions>. Every call compiles as before except one that passes a literalnullas the third argument, which is now ambiguous; leave the argument out instead. -
Behavior change: a configuration entry Tracon cannot read now stops the application at startup instead of being dropped without a word. Four settings change:
Tracon:Quotas:ThresholdPercentsandTracon:Webhooks:RetryDelays: an entry that is not a number (aTimeSpanfor the delays), or that is zero or below, was skipped, so the list came out shorter than written. It now fails startup and the message names the key and quotes the entry (ThresholdPercents[1] is '9O').Tracon:ContentProtection:Columns: an entry that is not oneProtectedColumnname — a misspelling, an empty entry, an undefined number, or two names joined by a comma, which was read as a third column — was skipped, and the column you meant stayed in plain text. It now fails startup and the message quotes the entry.Tracon:ContentProtection:Keys: an entry with an empty configuration key name was skipped. It now fails startup and the message names the key id.Tracon:ContentGuard:Pattern:MaskedPii: one misspelled name ("Emial,CreditCard") turned PII masking off for the whole list, and a number with an undefined bit was accepted. It now fails startup and the message quotes the value. An empty value still means no pattern.
The
ColumnsandKeyschecks run while content protection is off too: an old encrypted row is still read throughKeys. A value that code sets for the same member after binding replaces the configured one and clears the error. An undefinedProtectedColumnorPiiPatternsvalue set in code (for exampleoptions.MaskedPii = (PiiPatterns)64inAddPatternContentGuard) now fails startup as well. -
Behavior change: a workflow run that waits to be resumed keeps the definition version it recorded. Before,
resumeandrespondcompiled the current definition: an edit made while the run waited changed its settings, timeouts and parameter schema without notice, and a structural edit (a step added, removed or reordered, the pattern or the stored form changed) made the run impossible to resume. Now such an edit changes nothing for that run. To carry a run over a fix, name the edited version indefinitionVersion; a version whose graph differs from the checkpoint’s fails the run with “the graph’s structure differs”, as before. A run whose row recorded no version (written before this release) resumes with the current definition, and a resume of a workflow that was deleted is still refused. -
Breaking for implementers (preview):
IWorkflowDefinitionStorehas three more members (GetVersionAsync,ListVersionsAsync,RollbackAsync). A store of your own has to keep the history; a default implementation would have dropped every pinned step the first time the workflow was edited. The in-memory store and the three SQL stores implement them, andWorkflowDefinitionStoreContractruns against yours. -
The console writes the
nodesform of a chain when it has a function node or a pinned agent step (it was: a function node). A chain of agents alone with no pin is still saved asagentNames. Pinning the first step of such a chain changes the stored form, which is a graph change: the editor warns, and a run that waits keeps resuming the version it recorded. -
Behavior change: skill script results read differently. A timed-out script now reports
The script timed out and was stopped.(was… and the process tree was terminated.); a script whose executor did not return withinTimeoutplus five seconds reports that it may still be running, and its tool call error isTimed out; the script executor did not return in time.The truncation notice is now one line at the end of the result,[Tracon: output truncated at the N-byte limit per stream.], added only by the runner, which also caps each stream again. An executor that throws fails the call with only the exception type in the tool call record and the model’s view; the message goes to the server log. -
Behavior change:
TraconSkillScriptOptions.MaxScriptContentLengthnow limits a script file underSkillRootstoo. A file larger than the limit, an empty file, or anything that is not a regular file (a FIFO, a link to a device) is refused from its metadata before it is opened and is written to the audit trail asscript.denied. Before, a FIFO blocked the call while it held a concurrency slot, and a link to/dev/zerowas read without end. A stored script whose name is not a plain file name is now refused the same way, before thescript.runentry. -
Behavior change:
TraconSkillScriptOptions.Timeoutaccepts at most about 49.7 days minus five seconds; a larger value fails at startup, because the runner’s wait limit adds five seconds to it. -
Tracon.Testing.Contracts.Xunit:ModelProviderSettingsContract’s invalid-value case now also fails when the refusal message repeats a textInvalidSettingValue. Behavior change: a provider that echoes the rejected value fails the case; name the key and the accepted values instead. -
ISpeechProviderResolvergainsGetDialogueSynthesizerAsync. The member has no default implementation: a resolver of your own must implement the dialogue gate. -
Behavior change:
POST /api/jobs/{id}/cancelnow reaches a running handler. The worker reads each running job’s status everyTracon:Scheduling:PollIntervaland cancels the handler’s token when it finds the job cancelled or gone; before, a running job ran on until its handler calledJobContext.IsCancelledAsync. This costs one store read per running job per poll interval. A job cancelled this way staysCancelled. -
Source break:
builder.AddWorkflowFunction<TInput, TOutput>(name, factory, null)no longer compiles (CS0121): anullthird argument fits both the description parameter and the newAction<WorkflowFunctionOptions>overload. Name the argument (description: null) or leave it out. -
The generated C# client
TraconApiClientchanges three method signatures.TraconListRunsAsyncgains acontinuedFromRunIdparameter beforeskipandtake,TraconVoiceListAsyncgains aproviderparameter andTraconEraseDataSubjectAsyncaforceparameter before the cancellation token. The new parameters are optional, so a call that names its arguments compiles unchanged. A call that passesskip,takeor the cancellation token by position no longer compiles; name the argument. Code compiled against the previous package must be rebuilt. -
Behavior change: the streamed workflow endpoints now check their input before the stream opens.
POST /api/workflows/{name}/runwith an invalidsessionId,resumeof an unknown run or checkpoint, andrespondto a run that does not wait for input answered200and reported the error inside the event stream; an unknown checkpoint also opened a newFailedrun row. They now answer400(invalid session id, unknown checkpoint, a run that does not wait) or404(unknown run, or a run of another tenant) as problem details, and open no run. -
Behavior change: the names
functions(workflows) andhandler-keys(schedules) are reserved.GET /api/workflows/functionsandGET /api/schedules/handler-keysare matched before{name}, so a definition saved under one of these names could never be read back. Saving one now answers400, andAddWorkflowthrowsArgumentException. The check ignores case. -
The console’s tenant provider panel offers speech providers next to the model providers, by the provider name a policy and a binding use. A name saved in a policy that no provider reports any more stays listed and can be removed; before, it was invisible and sent back on every save.
-
IWorkflowFunctionCataloggainsValidateSettings(string name, JsonElement? settings), which returns a message that names the function and the problem, ornull. An implementation of the interface outside Tracon must add it; the registryAddWorkflowFunctionbuilds is the only implementation Tracon ships. -
Tracon.Testing.Contracts.Xunitadds twoWorkflowDefinitionStoreContractcases: a function node’sSettingssurvive the round trip, and saving the definition again without them clears them. A customIWorkflowDefinitionStorethat rebuilds a node from its own columns and drops the field now fails them. -
The generated C# client documents the 41 nullable object properties whose description was lost on the way from the OpenAPI document (for example
AgentDefinition.Harness). -
Every endpoint that starts a run now refuses an attribution that breaks a label limit with
400before the run starts, as the agent run endpoint already did: workflow run, resume and respond, run replay, the inbound trigger endpoint, and/v1/chat/completionsand/v1/responses(in the OpenAI error shape,type: invalid_run_attribution). These endpoints started the run with no user and no labels, and answered200. -
A run that spends tokens on more than one model (a compaction summarizer or an
aiJudgejudge on a model other than the agent’s) prices each model’s share at that model’s price, andcostis the sum. The three*PricePerMillionTokensfields of such a run arenull: one rate cannot describe the total. A model with no price adds nothing to the cost and is not priced at the agent’s rate. The cost isUnknownonly when no model of the run has a price.POST /api/stats/recalculate-costsdoes not re-price such a run when any of its models had a price. A run whose summarizer or judge runs on the agent’s own model is unchanged. -
UseVoice()andUseOpenAILive()now validate their settings when the application starts. Both validators already said so, but neither registration asked for it, so an invalid value — a missing ElevenLabs key, a headerlessOutputFormat, a zeroMaxAppendCharacters, a missing OpenAI key — surfaced at the first voice request or live session. A host registered with such a value now fails at startup, and the message names the setting. -
The OpenAI provider now rejects every
ModelBinding.ProviderSettingskey, as Azure OpenAI already did. The rule coversopenai,openai-responsesand everyUseOpenAICompatible()registration; their settings prefix isopenai(OpenAIProviderNames.SettingsPrefix) andOpenAIProviderNames.SupportedSettingsis empty. Before, the provider never read the dictionary, so a key, for exampleanthropic.promptCachingleft over after a switch from Anthropic, was ignored silently: the agent saved and ran without the behavior its author wrote. NowPOST /api/agents/validatereportsinvalid_settingatmodel.providerSettings, saving the definition returns400, and a stored or code-defined agent with such a key fails compilation. Remove the key withPUT /api/agents/{name}or in code. The console does not editproviderSettingsand keeps the stored value, so a console save of such an agent fails until the key is removed through the API. -
ModelProviderSettingsContract.SupportedSetting(Tracon.Testing.Contracts.Xunit) is nowvirtualand returnsKeyValuePair<string, JsonElement>?, defaultnull. Derive the contract for every provider; a provider that supports no key leaves itnull, and the two scenarios that need an accepted key report as skipped. An existing override fails withCS0508; add?to its return type. -
A harness agent no longer gets the provider’s hosted web search by default. Set
HarnessSettings.EnableWebSearchto turn it on; the search runs at the provider and is billed by it, and the Chat Completions surface accepts it only for search models. A definition that sets bothEnableWebSearchandDisableWebSearchis refused when it is compiled. The console’s harness section shows “Enable web search” in place of “Disable web search”. -
A failed workflow run’s
error.typeis the same stable identity an agent run records -loop_unsatisfied,run_budget_exceeded,upstream_error- instead of the exception’s CLR name, and the run now carries an error class and fingerprint. A consumer that matched a CLR name such asTracon.TraconLoopUnsatisfiedExceptionon a workflow row matches the identity instead. -
A non-streaming harness loop returns the final iteration’s answer; its usage stays the sum of every iteration. A streaming loop still streams every iteration. Each iteration remains on the run as a
LoopIterationCompletedevent. -
A code workflow built with
GetWorkflowAgentfails at a step that cannot answer, as the defined workflows do (see Fixed). -
A job’s lease is renewed at a third of
Scheduling:LeaseDurationinstead of half, so the longest accepted lease is about 149 days. -
IAgentDecorator.Decoratereceives the descriptor of the definition version the agent was compiled from - the pinned version on a pinned resolve. -
Tracon.Testing.Contracts.Xunitadds twoJobStoreContractcases:CompleteAsyncandReleaseForRetryAsyncleave a cancelled or finished job unchanged. A customIJobStorethat overwrites a final status now fails them. -
CompiledAgentCacheholds one entry per tenant, agent name, definition and culture. A compile under a new dependency fingerprint (skills, egress policy, MCP tool bindings) replaces the entry instead of adding one, so the cache no longer grows when a remote server edits its tools;Countcounts these entries. -
The
AddGeneratedTools()extension that the[TraconTool]generator writes into your assembly is nowinternal. It waspublic, so a host that referenced a class library with tools saw the library’s copy next to its own, andAddGeneratedTools()failed withCS0121(or withTRC0005in a host without tools). A library hands its tools to a host through its own public extension that callsAddGeneratedTools()from inside; a host that called a library’s generated extension directly calls that library extension instead. TheTRC0005message now says so. -
Tracon.Testing.Contracts.Xunitadds twoToolApprovalRuleStoreContractcases:SourceFingerprintround-trips, and two rules that differ only in it are two rules. A customIToolApprovalRuleStorethat drops the field or leaves it out of the rule’s identity now fails them. -
Tracon.Testing.Contracts.Xunitadds three store contract cases.ExperimentStoreContractrequires thatSaveAsynckeep the canary policy of a draft and never write one itself.WebhookStoreContractrequires thatSaveSubscriptionAsyncwrite the failure count the subscription carries. A custom store that does otherwise now fails them. -
A replay, an automatic continuation and a resume after a tool approval no longer refuse an agent that declares parameters. They bind the values the source run recorded (a replay request can change some of them) and refuse a source that recorded none. Before, they refused every such agent. If you upgrade with an approval pending on a queued run of a parameterized agent that predates parameter recording, the decision cannot be applied and says so.
-
The batch item rule changed: an item that is a JSON object with a string
messagemember is now read as a structured item instead of as plain text. A text item whose content is such an object is the one ambiguity; the stored item keeps no type. A malformed structured item fails alone, and the schedule endpoints refuse it when it is saved or triggered. A schedule you saved before this release that holds such an item with a member the shape does not name ({"message": "...", "user": "bob"}) is not checked again; its item fails on the next run instead of running as text. -
An inbound trigger that targets a workflow now stores the sender’s text as
{"message": "<text>"}. The body of a webhook is not yours: read as a batch item, a body such as{"message": "...", "parameters": {...}}would have let the sender choose the workflow’s parameter values. The job item’s input shows the wrapped text, and the run still gets the body as its message. -
Online evaluation and
POST /api/runs/{id}/judgeskip a run whose recorded input has no messages, as they skip a run with no recorded input. A workflow run that declares parameters now has such a row (it holds the values). -
GET /api/runs/{id}/inputanswers200with an empty message list for a workflow run that recorded parameters; it answered404before. The values are not returned there. A replay of a workflow run answers400and points to resume. -
POST /api/agents/validatereports every loop problem with its own code (loop_criteria_empty,loop_max_iterations_invalid,loop_outcome_invalid,loop_criterion_kind_missing,loop_criterion_unknown,loop_marker_missing,loop_judge_criteria_missing,loop_judge_unconfigured,loop_judge_unavailable) and the field’s path, for exampleharness.loop.criteria[1].marker. Before, it reported the first loop problem only, as a path-lesscompilation_error. Saving such a definition returns400with every loop message, and a compile error lists them all. -
An
aiJudgeloop criterion with a blank entry injudgeCriteriais now refused, withloop_judge_criteria_missingat the entry’s path (harness.loop.criteria[i].judgeCriteria[j]). Before, such a definition compiled, and the judge received a criterion with no text to check. Remove the blank entries; the console drops them when you leave the field. -
A structured response cut off at the output limit no longer completes with a partial JSON document (validation off) or fails as
StructuredResponseInvalidafter spending every repair turn on the same limit (validation on); it fails once asOutputTruncated. A dashboard or alert rule that counted these runs underStructuredResponseInvalidneeds the new class. -
Online evaluation does not retry a judge whose model answer was cut off at the output limit. The item fails with
judge_failedon the first attempt, as for a permanent4xx; before, the job sent the same request with the same limit until its attempts ran out. Raise the judge binding’sMaxOutputTokens. -
Source break:
IRunStoregains two members,RecordNonTokenUsageAsync(NonTokenUsageRecord, CancellationToken)andListNonTokenUsageAsync(Guid, CancellationToken). They have no default implementation, so a customIRunStoredoes not compile until it adds both. A store must also keepRunCompletion.NonTokenCoston the run and return it, with the tree total, asRunRecord.NonTokenCostandTreeNonTokenCost. Derive the newNonTokenUsageContractbesideRunStoreContractto prove it. The shipped stores implement both members; only a store of your own needs the change. -
Behavior change:
WebhookRunSummary.Costonrun.completedandrun.failednow means the token cost and the amount the run reported beyond tokens, together. It already said “the total amount”; a run that reported nothing sends the same number as before. The newWebhookRunSummary.NonTokenCostis the reported part, absent when nothing was reported, so the token part isCost - NonTokenCostwhenever both are in the same currency. When a tree’s token cost and its reported amount are priced in different currencies,Coststays the token cost alone andNonTokenCostis still given. A consumer that readCostas tokens only should subtract it. -
Behavior change: the amounts a run reported beyond tokens count against the tree budget (
Tracon:AgentGraph:MaxTotalCost), a tenant’s cost quota and the cost in both webhooks. An amount that adds is recorded into the budget the moment it is reported, so the tree’s next model call is refused once the ceiling is passed, and the quota entry of an agent root and of a workflow root includes it. A tool’s measurement joins the same total. A run that reports nothing is unaffected. Still token-only on purpose:GET /api/stats, the dashboard, thetracon.run.costmetric, the comparison of two runs and the console’s Tools screen. A quota has no limit on a unit such as characters. The same ceiling also stops a function node from starting (see the entry that begins “A workflow function node no longer starts” at the top of this section).
1.0.0-preview.3 — 2026-09-24
Section titled “1.0.0-preview.3 — 2026-09-24”The first security release. It closes the tenant-boundary and credential-exposure defects of the earlier previews listed under Security, and it changes the public API: every removed or changed type is named below. Read Security and Changed before you upgrade.
Security
Section titled “Security”Every change below except the audit trail one can refuse a request or a record that a running setup accepted before. Each refusal names what to change.
- A stored record may name only a configuration key inside its own tenant.
The allowed prefix is one per installation, so a record of one tenant could
name another tenant’s key and point it at an address it controls. A
non-default tenant now names keys under
{prefix}{tenant}:(for tenantacme,Tracon:McpSecrets:acme:GithubToken); a flat name directly under the prefix belongs to the default tenant, so a single-tenant installation changes nothing. The rule covers MCP servers, tenant provider bindings, webhook subscriptions and inbound triggers, where the record is saved (400) and where its value is resolved. Move an out-of-tenant name under the tenant’s segment. /api/tenants/{tenantId}/providers,/api/tenants/{tenantId}/egressand the tenant records (/api/tenants) act on a tenant other than the caller’s own only with platform authority: an API key that also carriesPlatformAdmin, the staticAuthToken, or — for a claims principal while multi-tenancy is on — the newTraconPolicies.PlatformAdminpolicy. A missing registration of that policy denies (403). Before, a key bound to one tenant withSecurityAdmin, or a claims-based Admin of one tenant, could rewrite another tenant’s provider binding.- With
AllowRemoteAccesson and neitherAuthTokennor an authorization policy configured, an anonymous remote request now returns401; API keys keep working. A request that still carriesX-Forwarded-For,ForwardedorX-Real-IPcounts as remote, so a reverse proxy on the same machine no longer makes every caller look local. Without a token or a policy, an anonymous request with a non-loopbackHostorOriginreturns403, which blocks DNS rebinding and cross-site requests; the voice WebSocket follows the same rule. - Creating an API key that carries the
PlatformAdminscope now needs the same platform authority as acting on another tenant. Before, a claims-based Admin of one tenant, without theTraconPolicies.PlatformAdminpolicy, could create such a key and use it to reach other tenants. An API key can still only create scopes it carries itself. GET /api/mcp-serversand thePUT /api/mcp-servers/{name}response no longer return stored extra header values: every header keeps its name and carries the value***. Before, anyReaderorAgentsReadAPI key could read a header value such as an API key and call the MCP server directly, past the tool approval gate. Webhook subscriptions (GET /api/webhooks,GET /api/webhooks/{name}and thePUTresponse) follow the same rule. The stored value does not change and is still sent to the target.- The MCP server audit trail records header names only. The values of headers
whose names the secret filter does not recognize, such as
CookieorOcp-Apim-Subscription-Key, no longer enter the audit log as plain text. - A script grant now pins the content it was given for. A stored script, and a
script of a skill registered in code, runs only while its content still has
the hash the grant carries; a change stops it until it is granted again. A
grant without a script name pins the whole script set, so adding, removing or
changing any script stops them all. An existing grant pins nothing and no
longer authorizes a stored or code script: grant each one again. Read
scripts[].contentHashorscriptSetHashfromGET /api/skills/{name}and send it asexpectedContentHashtoPOST /api/skill-script-grants(missing or malformed400, unknown script404, changed content409). Scripts read from disk are not pinned and keep working; keep their directories read-only. Before, an administrator who could write skills could replace a script’s content after a security administrator had approved it, and the old grant kept authorizing the new code. - In a multi-tenant host, granting a stored script needs platform authority
(an API key that also carries
PlatformAdmin, the staticAuthToken, or theTraconPolicies.PlatformAdminpolicy;403otherwise). The script runs under the server’s own operating-system identity and can read other tenants’ data, so the grant is an installation decision, not a tenant’s. - A store of your own must keep
SkillScriptGrant.ContentHash. A store that drops it fails the newSkillScriptGrantContractcases, and its grants refuse every stored script. - A credential header of an MCP server or a webhook subscription is declared
by configuration key name in the new
headerConfigurationKeys, and a save whose plainheaderscarry a credential-looking name (Authorization,Cookie, a name containingapi-key,token,passwordorsecret, or a name ending in-key) returns400. The detail names the header and the new field and never repeats the value. Before,Authorizationwas the only header with a key reference, andX-Api-KeyorOcp-Apim-Subscription-Keywas stored in the database as plain text. A stored row keeps working and is still sent; each connection or delivery logs a warning that names the header. Every key name follows the prefix and tenant rules above, on save (400) and again before each connection or delivery: an MCP server whose map names another tenant’s key does not connect, and such a webhook delivery is dropped. - A header name may appear only once across
headersandheaderConfigurationKeys, compared without regard to case, and must be a valid HTTP field name (400otherwise). Turning OAuth on for an MCP server whose kept plainheadersstill carryAuthorizationreturns400: the MCP client skips its bearer token while that header is present. A webhook’s two maps together may carry at mostTracon:Webhooks:MaxExtraHeadersentries (400instead of being cut at delivery), andheaderConfigurationKeysmay not name anX-Tracon-*header. PUT /api/mcp-servers/{name}andPUT /api/webhooks/{name}keep a stored header map when the request leaves it out or sendsnull;{}removes it. Saving from the admin UI therefore keeps the headers. The side effect: a save that changes only the address sends the kept headers, and the credential values they resolve, to the new address; the server logs a warning with the header names. Send the maps, or{}, in the same save when an address moves.- An
OperationCanceledExceptionthat anIRunAuthorizationHandlerthrows, but that the request itself did not cause (for example, anHttpClienttimeout inside the handler), is now a failed check. A single resource answers the same404as a missing one. Before, it answered500, which revealed that the resource exists.
HeaderConfigurationKeysonMcpServerDefinition,McpServerRequest,WebhookSubscriptionandWebhookSaveRequest: a map from a header name to the configuration key its value is read from. The value is resolved on each connection or delivery and never stored; a response returns the map as stored. Themcp_serversandwebhook_subscriptionstables gain aheader_configuration_keyscolumn (migration0054PostgreSQL,0042SQL Server,0041SQLite); existing rows read an empty map.McpServerStoreContract.Header_configuration_keys_are_preserved,WebhookStoreContract.Header_configuration_keys_are_preservedandWebhookStoreContract.Header_names_that_differ_only_in_case_do_not_fail_the_save. A store of your own must persist the new field and return it from the save as well as from a read.
Tracon.UInow carries the notices of the third-party code in its embedded console: React, React DOM,scheduler, TanStack Query,openapi-fetch, and Tailwind CSS, all MIT.THIRD-PARTY-NOTICES.txtships at the package root and insideTracon.UI.dll. Before, the package redistributed that code without them.- A webhook subscription whose headers differ only in case (
X-Aandx-a) no longer fails the SQL save with500, and such an MCP server no longer fails to connect. - The MCP OAuth authorization flow now sends the same headers as every other connection to the server, including the ones resolved from configuration.
- A header that the MCP transport cannot add (a content header such as
Content-Type, or a value with a line break) is left out with a warning. Before, the transport’s exception carried the header value into the log. - The
TRC0201analyzer names the header for a literal secret in a header dictionary (McpServerDefinition.Headers["X-Api-Key"]) and points atHeaderConfigurationKeys. - A slow or unresponsive database no longer blocks the metrics collection thread while the quota or queue-depth gauge refreshes. Before, this delayed the export of every other instrument of the meter provider.
- The quota threshold notification cache in
QuotaEnforcerno longer grows for the life of the process. Entries of closed quota periods are now dropped. - A failed or cancelled durable threshold claim no longer keeps a quota threshold notification silent until the end of the period. The next run in the same period retries the claim.
- The quota admission check, the threshold accounting, the quota gauge and
GET /api/quotas/usagenow read only the counters of the current periods. Before, every read returned the tenant’s whole usage history, twice per run, and that history only grows. GET /api/models/healthno longer fails for every provider when one provider’s health check throws. The failing provider is reportedUnhealthywith the exception type indetail(The health check failed (...)), and the exception is logged at Warning level. The background health refresh also continues past that provider.- An Azure OpenAI
CredentialFactorycredential that throws while it gets a token (for exampleDefaultAzureCredentialwithout an Azure sign-in) now reportsUnhealthywithCredential error (<exception type>)and logs the exception, instead of escaping the health check. UseAzureOpenAI(IConfiguration)now binds a relativeEndpointand aModelsentry without aName, so start-up validation rejects them with the specific message, the same as the other three providers. Before, a relative endpoint was reported as missing and a nameless entry was silently dropped. An application that relied on the silent drop now fails at start-up.- The out-of-catalog log entry of an
UseOpenAICompatible()provider now namesOpenAIProviderOptions.Modelsinstead of theTracon:Providers:OpenAIsection, which that provider does not read. - Two approval rules whose argument conditions differ are no longer merged into one by the SQL stores when a condition path carries the U+001E or U+001F control character. Before, saving the second rule returned the first one unchanged. Existing rules keep their stored fingerprint; no migration runs.
- The in-memory approval rule store now treats a list value that differs only
in whitespace (
["eu", "us"]and["eu","us"]) as the same condition, the same as the SQL stores. Before, it kept two rules.
Removed
Section titled “Removed”These public types are now internal. None is part of a seam you implement
or a call you make: the same behavior is reached through the public
registration methods, options, and interfaces. The API is not frozen in the
preview line, and the counts below are types, not members.
-
Tracon.PostgreSql,Tracon.SqlServer,Tracon.Sqlite:MigrationRunner. It existed once in each provider package under the same name, so an application that referenced two providers could not name it (CS0433). ResolveIMigrationApplierto apply migrations andISqlPersistenceDiagnosticsfor the migration snapshot; both resolve to the same runner. Thetracon migratecommand is unchanged. -
Tracon.Anthropic,Tracon.Azure,Tracon.Google,Tracon.OpenAI:AnthropicChatClientFactory,AnthropicModelCatalog,AzureOpenAIChatClientFactory,AzureOpenAIModelCatalog,GoogleChatClientFactory,GoogleModelCatalog,OpenAIChatClientFactory,OpenAIModelCatalog, andOpenAIApiSurface. TheUse*registration methods and the*ProviderOptionstypes are the configuration surface; a factory created throughFromClientcould not be plugged into a registered provider anyway. -
Tracon.Abstractions(11):ChatHistoryState,ExperimentAssignment,JobPayload,ReplayToolMismatchException,RunAttributionReader,SafeErrorText,SchemaReadyGate,ToolArgumentConditionLimits,TraconSessionStateKeys,WebhookEventPayloadJsonContext, andWorkflowCheckpointState. A store’s checkpoint listing still returns an empty JSON object ({}) asState; the documentation ofIWorkflowCheckpointStorenow says so. -
Tracon.Core(69):AgentCallGraph,AgentCallGraphProblem,AgentSessionIdentity,AttachmentTypeGuard,AttachmentUriReference,AttachmentValidationResult,AuditActorContext,AuditChainHasher,AuditChainWalker,AuditPayload,AuditRecorder,AuditSecretFilter,AuthorizingAIFunction,CanaryEvaluator,ChildRunApproval,CodeAgentRegistration,ConfigurationKeyGuard,CronExpression,DefaultProviderRetryClassifier,DocumentAttachmentSummary,DocumentChannelMessageBuilder,EgressAddressPolicy,EgressAddressValidator,EgressAddressVerdict,EgressSocketGuard,InMemoryWorkflowCheckpointStore,InboundTriggerDispatchResult,InboundTriggerOutcome,InboundTriggerRateLimiter,InboundTriggerSecretResolver,InboundTriggerValidatedRequest,InboundTriggerValidationResult,InstructionCultureResolver,InstructionParameterBinder,OnlineEvalSummaryService,ProviderCredentialClientCache,QuotaPeriodCalculator,ResolvedRetentionPolicy,RetentionExecutor,RetentionPolicyResolver,RunPromotionOutcome,RunPromotionStatus,RunReplayOutcome,RunReplayPreparation,SessionBranchOutcome,SessionBranchStatus,SkillScriptExecutionResult,SkillScriptNaming,StateGenerationCount,StatePreflight,StatePreflightReport,StateSampleFailure,TextChunker,TextTrimming,ToolApprovalPolicyRegistration,ToolApprovalPolicyRegistry,ToolApprovalRuleEvaluator,TraconEvalCheckRegistration,TruncatingAIFunction,ValidatingAIFunction,VoiceAudioFormats,VoiceConnectionLease,VoiceConnectionLimiter,VoiceConversationProtocol,VoiceConversationRequest,WebhookHttpClient,WebhookUrlValidator,WebhookUrlVerdict,WorkflowDefinitionValidator. -
Tracon.Voice:VoiceProviderNames. The provider value stays the plain stringelevenlabs. -
Tracon.Abstractions(3 more):QuotaDecision,QuotaThresholdCrossing, andSqlPersistenceRegistrationMarker. Only the SQL provider packages register the marker; a marker registered by other code made background services and approval requests wait for a migration that never ran. -
Tracon.Core(16 more):AgentSkillCatalog,CallableAgentResolver,ContentGuardPipeline,ModelProviderCircuitBreaker,ModelProviderHealthCache,ProviderConcurrencyLimiter,QuotaEnforcer,RunSampleRequest,RunSampler,RunTraceCollector,SandboxedSkillScriptRunner,SkillScriptSupport,TenantProviderCredentialResolver,ToolApprovalPresenterRunner,TraconLoopEvaluatorRegistration, andTraconMetrics. Metrics stay available through theTraconDiagnostics.MeterNamemeter, loop evaluators throughAddLoopEvaluator, and quotas throughTraconQuotaOptionsand the quota endpoints. -
The public constructors of the service types that the container or the run pipeline builds are now
internal:RunRecordingAgent,AgentDefinitionCompiler,TraconDiagnosticsCollector,ModelProviderRegistry,SandboxedSkillScriptRunner,ChildAgentInvoker,ContentGuardPipeline,RunEventWriter,AgentSessionManager,QuotaEnforcer,ModelProviderHealthCache,RunSampler,SkillScriptSupport,ModelProviderCircuitBreaker, andTraconMetrics. Each took optional parameters, so a new dependency after1.0.0would have needed one more overload. Resolve the service thatAddTracon()registers:// Before (1.0.0-preview.1 / preview.2)var registry = new ModelProviderRegistry([provider]);// After: resolve the service Tracon registersvar services = new ServiceCollection();services.AddTracon().AddModelProvider(provider);using var serviceProvider = services.BuildServiceProvider();var registry = serviceProvider.GetRequiredService<IModelProviderRegistry>();RunRecordingAgentwraps every agent thatIAgentCatalog.ResolveAsyncreturns, andChildAgentInvokerwraps each callable agent of a compiled definition. The run writer’s lifecycle methods (RunEventWriter.StartAsync,RunEventWriter.CompleteAsync,RunEventWriter.RecordToolInvocationAsyncandRunEventWriter.CompleteLateToolInvocationAsync) are nowinternaltoo: code inside a run reads the writer fromTraconRunContext.Currentand keepsAppendAsyncand the read-only properties.
Changed
Section titled “Changed”-
Tracon.AspNetCorenow depends on exact versions of its five pre-release packages:Microsoft.Agents.AI.Hosting,.Hosting.A2A,.Hosting.AspNetCore,.Hosting.OpenAI, andA2A.AspNetCore. Before, each was a lower bound, so a newer Hosting preview restored without a warning and failed at run time: in1.22.0-preview,Microsoft.Agents.AI.Hosting.AgentSessionStoremoved out of the Hosting assembly (TypeLoadException). A direct reference to another version now reportsNU1608at restore, and a transitive oneNU1107. Move to a newer Hosting preview together with the Tracon release built against it. -
TraconToolRegistrationtakes only the tool in its constructor. Every other setting is aninitproperty with the same default as before, so a setting added later does not change the constructor again:// Before (1.0.0-preview.1 / preview.2)new TraconToolRegistration(fn, requiresApproval: true, maxOutputBytes: 4096);// Afternew TraconToolRegistration(fn) { RequiresApproval = true, MaxOutputBytes = 4096 };The old named arguments fail to compile with
CS1739, which names the argument.AddToolandAddScopedToolwithToolRegistrationOptionsdo not change. -
ITraconBuildernow declares onlyServices. Its 27 registration methods (Configure,AddTool,AddScopedTool,AddToolsFrom,AddAgent,AddSkill,AddAgentSource,AddAgentDecorator,AddRunJudge,AddModelProvider,AddEvalCheck,AddLoopEvaluator,RequireCustomBindingandRequireProductionProfile) are extension methods on the newTraconBuilderExtensionsclass, in theTraconnamespace next toAddTracon(). A new registration method therefore never changes the interface. Calling code does not change;using Tracon;is enough.- An assembly compiled against an earlier
1.0.0-preview.Nrelease (1.0.0-preview.1,1.0.0-preview.2) that creates aTraconToolRegistrationor calls anITraconBuildermethod throwsMissingMethodExceptionuntil it is compiled again. This includes the code the[TraconTool]source generator wrote into your assembly: the exception comes from theAddGeneratedTools()call. - Upgrade every Tracon package together. A provider, MCP, Voice or Testing
package from an earlier preview next to the new
Tracon.Corefails while it registers, for example withMissingMethodExceptioninsideUseOpenAI(), before the start-up version check can run. - An
ITraconBuilderimplementation of your own needs onlyServices. An explicit implementation of a removed method (ITraconBuilder ITraconBuilder.AddTool(...)) fails to compile withCS0539; delete it. A public method with the same name still compiles, but it runs only when the caller holds your concrete type. ThroughITraconBuilder, Tracon’s extension method runs and writes to yourServices, so a test double must return a real service collection from it. - Every registration method now throws
ArgumentNullExceptionfor anullchain (NullReferenceExceptionbefore). - An extension method of your own with the same signature as one of these
no longer loses to the interface member. Declared in a namespace nearer to
the call than
Tracon(for example in your ownnamespace MyApp), it now runs instead of Tracon’s method, with no warning. Imported at the same level asTracon, the call is ambiguous (CS0121). Rename or remove such an extension.
- An assembly compiled against an earlier
-
Every Tracon package now depends on its Tracon siblings at exactly its own version (
[1.0.0-preview.3]), not at that version or any later one. Upgrading one Tracon package and not the others now fails restore withNU1107, or warnsNU1608when your project references the shared package directly. -
A host whose loaded Tracon package assemblies come from more than one release no longer starts. It stops before any hosted service starts, with a
TraconExceptionthat lists each Tracon assembly and its version. Before, it started and failed later, at the first call into a member the other release no longer has. Every Tracon library package is compared exceptTracon.Client, which talks to a server over HTTP and may call another release by design. The check has no setting to turn it off. A process that never starts a host (a bareBuildServiceProvider(), or a test project that runs the store contracts) does not run it. -
A project created with
dotnet new tracon-apitreatsNU1608as an error, so such a graph stops at restore. The template’s project file explains the line; remove it to keepNU1608a warning. -
Upgrade every Tracon package together,
Tracon.Testing.Contracts.Xunitincluded. Measured against1.0.0-preview.2, these calls fail at run time when a1.0.0-preview.2package runs next to a newer sibling:Tracon.AspNetCorecallsTenantProviderCredentialResolver.ValidatePrefix(a tenant provider bindingPUT) andInboundTriggerSecretResolver.ValidatePrefix(a trigger save), which no longer exist (MissingMethodException), andConfigurationKeyGuard.RequirePrefix(an MCP server or webhook save), which is no longer reachable (MethodAccessException);Tracon.McpcallsConfigurationKeyGuard.RequirePrefixwhen it opens an MCP transport; andTracon.Testing.Contracts.XunitcallsJobPayload.ExtractItems(twoJobScheduleStoreContractcases) andWorkflowCheckpointState.IsOmitted(oneWorkflowCheckpointStoreContractcase), which are now internal (MethodAccessException). The published1.0.0-preview.1and1.0.0-preview.2packages accept any newer sibling, so restore does not stop that graph: a host stops at start-up, and a test project that runs the store contracts without a host fails at those calls. -
QuotaUsageQueryhas a newPeriodStartsfilter: for each period, the first day whose counters to return, computed by the caller from the configured time zone. Without it,IQuotaStore.GetUsageAsyncreturns every period’s counters, and its documentation now says so. A customIQuotaStoremust apply the filter;QuotaStoreContracthas four new scenarios for it. -
An interval that drives a background timer now fails startup validation when it is outside 1 millisecond to about 49.7 days:
Approvals:ScanInterval,Canary:ScanInterval,RunReconciliation:HeartbeatIntervalandScanInterval,Scheduling:PollInterval,Health:BackgroundIntervalandMcp:RefreshInterval.Scheduling:LeaseDurationmay be at most about 99 days andSingletonExecution:LeaseDurationabout 149 days, because each lease renews at a fraction of its length. Each of these checks applies only while its feature is on.Retention:BatchDelaymay be zero and is always held to at most about 49.7 days. Before, such a value let the host report that it had started and then stopped it; approval expiration is on by default, soApprovals:ScanInterval=00:00:00did this to a default setup. -
The
tracon.quota.usage,tracon.quota.limitandtracon.job.queue.depthgauges no longer read the database inside the metrics collection callback. When a gauge is enabled, a background service reads the values once at startup and then everyQuotaUsageRefreshInterval/JobQueueDepthRefreshInterval. A collection only reads the last result. The first collection after startup can be empty, and a value can be up to one interval old. -
EnableQuotaUsageGauge,EnableJobQueueDepthGaugeand their refresh intervals are now read at host start. Turning a gauge on, or changing its interval, takes effect at the next start. -
An enabled gauge whose refresh interval is outside 1 millisecond to about 49.7 days now fails startup validation. Before, zero was accepted and meant a refresh on every collection.
-
A
PUT /api/mcp-servers/{name}orPUT /api/webhooks/{name}with a header value of***is rejected with400. A client that reads, edits and saves a record must send the real value of every header again. -
When an
IRunAuthorizationHandlerthrows, the request is still denied (fail-closed), and Tracon now writes oneErrorlog line in theTracon.RunAuthorizationcategory. The line holds the handler type, the access kind, the tenant, agent, run and session ids, and the exception. A403now saysThe run authorization check failed. Retry the request.instead of claiming that the handler denied the call. This covers run starts, the session list, the run, attachment and approval lists, replay, and workflow resume and respond. A404does not change. The exception text never enters the response. -
A session management policy (
TraconSessionOwnershipOptions.ManagementPolicy) whose policy provider or requirement handler throws is now logged as anErrorin theTracon.SessionOwnershipcategory, and the caller still gets the narrowed result. An unregistered policy writes no log line. AnIRunAttributionContextthat throws during an authorization check is logged once per request, and the caller is treated as having no identity. -
An
IAuthorizationPolicyProviderthat throws whileMapTraconresolves a role policy is now logged as aWarningin theTracon.RolePoliciescategory. The role is still treated as not registered. WithRequireRolePolicieson, the startup exception carries the provider’s exception asInnerException. -
SandboxedSkillScriptRunner.RunStoredScriptAsynctakes the skill and the script name (RunStoredScriptAsync(AgentSkillDefinition skill, string scriptName, JsonElement? arguments, CancellationToken cancellationToken)), so the runner compares the grant with the content it is about to run. The(string skillName, AgentSkillScriptDefinition script, ...)overload is gone. -
GET /api/skills/{name}resolves the name the way the runtime does: a skill registered in code wins over a stored one with the same name, and the neworiginfield says which one was returned. A name that exists only in code now returns the skill instead of404. Each script carries the newcontentHashand the skill the newscriptSetHash; both are computed, and a value sent in a request is ignored. -
PUT /api/skills/{name}answers409for a name registered in code withAddSkill, the same rule as a code-defined agent: the code skill wins the name, so a stored skill under it never ran. Before, the save succeeded and wrote a record that never ran.DELETEstill removes such a stored copy and answers409when the name has none. The console opens a code skill read only. -
The in-memory script grant store keeps a revoked grant, with
revokedAtset, the same as the SQL stores. Before, it deleted the row, soGET /api/skill-script-grantslost the record of who granted what. -
TenantProviderCredentialResolvertakesIOptions<TraconOptions>as a third constructor argument, andValidatePrefix(string)is replaced byValidateKeyName(string tenantId, string configurationKeyName). The old name checked only the prefix; the rename keeps a caller from compiling against the weaker rule unchanged. -
ToolApprovalRuleStoreContract(inTracon.Testing.Contracts.Xunit) has two new cases. A store of your own must keep two condition sets apart even when a path carries a control character, and must treat a list value that differs only in whitespace between its elements as the same condition. The identity rule is now written onIToolApprovalRuleStore.AddAsync. A store that compares a list value’s raw JSON text fails the second case. -
The out-of-catalog log entry of the OpenAI, Anthropic, and Google providers now has one shared wording (
Use the <key> setting to add the model to the catalog.). The Gemini health detail for an unparsable model list is nowThe response is not valid JSON., the same as the other providers.
Deprecated
Section titled “Deprecated”McpServerDefinition.AuthorizationConfigurationKeyandMcpServerRequest.AuthorizationConfigurationKey(authorizationConfigurationKeyon the wire). They still save, resolve and read back, and are removed in1.0.0. UseHeaderConfigurationKeys["Authorization"]; a record that namesAuthorizationin both, or in the new map while OAuth is on, returns400. Code that sets the field compiles with warningCS0618.QuotaUsageQuery.AsOf. No store ever applied it, and a store cannot turn an instant into a period without the configured time zone. UseQuotaUsageQuery.PeriodStarts.- The
net8.0andnet9.0targets. Microsoft support for .NET 8 and .NET 9 ends on 2026-11-10, and the first Tracon release after that date drops both targets from every package;net10.0stays. An urgent security release may still carry them. Move an application that targetsnet8.0ornet9.0tonet10.0before you take that release.
A version section is not written ahead of time. At tag time this heading is
renamed to the version and the date it shipped on, and a fresh empty
## [Unreleased] is opened above it, so a section always names artifacts that
actually exist. Until then the release rehearsal and the GitHub release body
both read the notes from here.
1.0.0-preview.2 — 2026-09-20
Section titled “1.0.0-preview.2 — 2026-09-20”The consumer-entry repair release. It keeps the same public API as preview.1 and corrects the package, documentation, and registry paths a new consumer meets first.
Tracon.Templatesnow stamps its own resolved package version into generated projects.dotnet new tracon-apitherefore restores the published preview without a hidden--TraconVersionoverride. The override remains available and is now visible in template help.- The root README agent-catalog example now calls the shipped
ResolveAsyncoverload with its required culture and cancellation arguments. - Package descriptions, package READMEs, API counts, target-framework claims, extension samples, and console screenshots now match the shipped artifacts.
- NuGet-rendered package documentation no longer relies on Mermaid rendering.
tracon --versionandtracon -vreport the informational package version.- The TypeScript client README documents all seven SSE operations and its
exported
readSse/SseDecoderhelpers. - Regression gates now verify the packed template’s default version, rendered GitHub edit links, package README operation counts, shipped-language scope, and console screenshot freshness against UI source.
Changed
Section titled “Changed”- Preview npm releases use the
nextdist-tag, so install previews withnpm install @tracon/client@next. npm does not let thelatesttag be removed, and a package’s first publish bindslatestto it, solatesttracks the newest preview until the first stable release claims it. - GitHub repository metadata, Discussions, preview release classification, and public documentation links now describe the published product.
1.0.0-preview.1 — 2026-09-20
Section titled “1.0.0-preview.1 — 2026-09-20”The first published version: twenty packages on one version line.
The public API is not frozen: PublicAPI.Shipped.txt is empty in every
package, and the surface may still be reduced before 1.0.
RequireProductionProfile()on the Tracon chain.AddTracon()brings every security-sensitive switch up permissive, so a deployment can reach production having never separated tenants, never decided who owns a session and never registered a content guard — in silence. This call refuses to start such a host. It changes no setting and secures nothing by itself: it sets no value, chooses no policy and turns nothing on; the only thing it does is turn a skipped decision into a startup failure. Six decisions are asked about — tenant separation, session ownership, at-rest content protection, content inspection, request rate limiting and retention — and each is either answered by turning the feature on or accepted by name withprofile.Accept(TraconProductionRisk.SingleTenant). There is no way to accept all six at once, and every accepted risk is logged at information level, by name, on each host start. It is off by default: a host that never calls it behaves exactly as before and resolves nothing extra at startup. The set of decisions is a versioned contract: a later release that adds one stops a host that calls this method until the new decision is answered or accepted, and such a release declares the addition as a behavioural breaking change naming the new risk and why it was added.IProductionProfileCheckis the seam behind it — more than one check may carry the same risk, and the strictest answer wins, which is howTracon.AspNetCoreadds theUseTenancy(options => options.Enabled = false)case the core cannot see.Tracon:SessionOwnership:RefuseUnownedSessions(defaultfalse). Session ownership is not retroactive, so rows written before it was turned on belong to nobody; until now those stayed readable by id to anyone in the tenant, and only disappeared from owner-filtered listings. With this on they are refused instead —404on the session and/v1/conversationsroutes,403(errorTypesession_owner_required) on a run that names one,404on a voice socket. A caller who satisfiesManagementPolicystill reads them, so support keeps the access it already had in the management listing; that exemption does not extend to starting a run. A session that does not exist yet is unaffected: the first turn still opens it and claims it.TraconEndpointOptions.MapOpenAIConversations(defaulttrue). Set it tofalseinMapTraconto leave the four/v1/conversationsroutes unmapped; they then answer404and disappear from the OpenAPI document./v1/responsesand/v1/chat/completionsare unaffected.
Changed
Section titled “Changed”-
Licence. Tracon now ships under the PolyForm Small Business License 1.0.0 instead of MIT. Use is free of charge for an individual, an open source project, and any company with fewer than 100 total individuals working as employees and independent contractors and less than 1,000,000 USD (2019, inflation adjusted) revenue in the prior tax year; above that threshold a commercial licence applies (hfarukatasoy@gmail.com). Three packages stay MIT so that writing an extension, proving it against the behaviour contracts, and owning the code
dotnet newgenerates never need one:Tracon.Abstractions,Tracon.Testing.Contracts.XunitandTracon.Templates. The npm client@tracon/clientfollows its NuGet twin and is PolyForm. No package contains a licence key, an activation call or a feature gate, and the licence of a published version never changes. Because PolyForm is not OSI approved, packages now declare<license type="file">and carry the text inside the.nupkgrather than naming an SPDX expression. Nothing was published under MIT, so no existing consumer is affected. See /reference/licensing/. -
The
/v1/conversationsread and delete routes now go through your registeredIRunAuthorizationHandler.GET /v1/conversations/{id},GET /v1/conversations/{id}/itemsandDELETE /v1/conversations/{id}ask it withSessionAccess.Read,ReadandDelete; they previously never asked it at all, while/api/sessions/{id}did. If you have a handler registered that refuses some sessions, it now refuses them on this surface too. The direction is fail-closed and it closes a real gap — an installation whose handler deniedGET /api/sessions/{id}hadGET /v1/conversations/{id}/itemshand back the same chat history — but it is a behaviour change for existing setups.POST /v1/conversationsis not gated: it reserves an identifier and writes nothing.
Added — foundation
Section titled “Added — foundation”- Model provider adapters for OpenAI (Chat Completions and Responses),
Anthropic (Claude), Google (Gemini), and Azure OpenAI, plus an
IModelProviderextension point for any other provider — with per-tenant bring-your-own-key (BYOK) support and per-tenant egress policy. - Persistence backends for PostgreSQL, SQL Server, and SQLite, each with
embedded migrations; the runtime (
Tracon.Core) needs no database. - An embedded React console — 30 screens across 36 routes (Dashboard, Agents, Skills, Playground, Sessions, Runs, Workflows, Jobs, Evals, Experiments, Approvals, Tools, Models, MCP, Triggers, Audit, Diagnostics, Settings) — with zero JavaScript dependency in the consuming project.
- A management HTTP API alongside OpenAI-compatible endpoints
(
/v1/responses,/v1/chat/completions,/v1/conversations). Multi-tenancy is opt-in:TraconTenancyOptions.Enabledisfalseby default and every request resolves to the single default tenant until you turn it on. - Run recording with spans, metrics, and cost; a tamper-evident audit trail with a verifiable hash chain; data subject export and erasure.
- Cost provenance: a priced run records the unit prices actually applied and the provider that answered it, so a historical cost stays explainable after the price list changes.
- Named job lanes: a job carries a lane, a worker subscribes to the lanes it serves, and each lane can hold its own concurrency budget — unrelated kinds of background work no longer block one another. Queue depth, throughput, and duration are reported as OpenTelemetry metrics.
- Opt-in structured-response validation: when an agent asks for JSON or a JSON
schema, the response can be checked before the run closes, with a bounded
repair turn that stays inside the same run’s budget, cost accounting, and
cancellation. A run that carries a durable session gets no repair budget:
the underlying agent framework persists a turn as soon as that one model
call completes, so a repaired run would return an answer its own session
never recorded. Such a rejection fails the run exactly as it would with
repair switched off, and its rejection event carries
repairSuppressedBySessionto tell that apart from a run that simply had no repair configured. - Tool schemas generated from your method signatures express JSON Schema
constraints taken from standard
System.ComponentModel.DataAnnotationsattributes, and support nested object and object-array parameters. - Extension points for custom tools, model providers, run stores, run
judges, agent sources, and scheduled job handlers, each with a published
behavior-contract test suite (
Tracon.Testing.Contracts.Xunit) so a third-party implementation can be verified against the same tests Tracon’s own implementations run. - Workflow execution with five orchestration patterns, checkpoints, resume, and human-in-the-loop approval.
- MCP tool discovery from remote servers, and agent exposure over MCP and A2A.
- A typed management client generated from the OpenAPI document, published
both as a NuGet package (
Tracon.Client) and an npm package (@tracon/client). - A
dotnet new tracon-apiproject template and antraconglobal CLI tool (migrate,migrate status,health,eval). - Native AOT and trimming compatibility for
Tracon.Abstractions,Tracon.Core,Tracon.PostgreSql,Tracon.OpenAI,Tracon.Anthropic,Tracon.Google,Tracon.Azure, andTracon.Voice.
Read next
Section titled “Read next”- Versions and upgrades — how to pin the family and upgrade it without drift
- Compatibility matrices — the framework, runtime, and protocol versions each release supports
- Choosing packages — which packages you actually take a version of