Repository navigation
Dex agents 0.2.0
Tool calls that never ran
- All three cores:
RecordToolFailureandRecordToolOutcomeUnknownread the recovery error'sattempt_progress. When it isNOT_STARTED, no attempt of the tool Step reached a Worker, so the tool never ran. The call then recordsfailed, notoutcome_unknown, with anerrorthat starts with the newTOOL_CALL_NOT_RUN_ERROR_PREFIX(toolCallNotRunErrorPrefix,ToolCallNotRunErrorPrefix),the tool did not run, so it is safe to call again, then": "and the recovery detail. The model sees an ordinary failed call and can call the tool again. No final status is added. NOT_COMPLETED, andUNSPECIFIEDfrom a server that reports no progress, keep the 0.1.0 behavior: a read recordsfailedand any other calloutcome_unknown. So do task polls,CancelToolTask, and a call whose tool already asked a question, because their tool already ran.- This needs the Dex SDK release that adds
RecoveryErrorInfo.attempt_progress(attemptProgress,AttemptProgress).
Dex SDK 1.35.0
- Every package requires Dex SDK 1.35.0 (
dex-python-sdk==1.35.0,@superdurable/dex1.35.0, Gosdk-gov1.35.0) and a Dex Server with protocol 4: server-zd v0.1.17 or later. - The testing exports' private engines (
dex_agents.testing,@superdurable/dex-agents/testing, Goagentstest) rundexcli devwith a-zd-configfile that setsdev.durabilityMode: processCrashSafe, so tests stay fast now thatdex-server-zd0.1.18 defaults tostrict. A configuration file you pass replaces it; setdev.durabilityModethere too.
MCP tool source
dex_agents.mcp(extradex-agents[mcp],mcp>=2.3,<3) and@superdurable/dex-agents/mcp(optional peer@modelcontextprotocol/sdk^1.32.2):McpToolSourcelists the tools of MCP servers configured on the Worker, over stdio or Streamable HTTP, and runs each call in its own tool Step.readOnlyHintmakes a toolread,idempotentHintmakes itidempotent_write, and an explicitToolPolicywins. A call whose response never arrived is re-issued only forreadandidempotent_writetools; other tools recordoutcome_unknown. A task of a Streamable HTTP server that any connection can poll becomes a tool task polled byPollToolTask(see Tool tasks); a stdio task, and in TypeScript a session-bound task, is polled inside the tool Step with heartbeats and a bounded wait. A retried attempt resumes the same task once its checkpoint reached Dex. In Python, a task's single form elicitation becomes a session question; its answer, decline, or passed deadline goes back withtasks/update(accept,decline,cancel), once, from a poll's first attempt, and only to the request it answers; a retried attempt asks the user again. An identical request after the task leftinput_required, or 10 seconds after the answer, is asked as a new question. Otherinput_requiredanswers recordfailed.mcp.task_statusis written only when a task's status changes. TypeScript requests a 24-hour TTL,taskTtlMs, for a polled task.- Both cores:
DeferredToolCall.tool_source_reference(stored onAgentToolCallastool_source_reference) andDeferredToolExecution.heartbeat_checkpoint/save_heartbeat_checkpoint. - TypeScript:
toolEffectFromMcpToolAnnotationsmapsidempotentHinttoidempotent_writewhateverdestructiveHintsays, as the Python adapters do.
UI streams
@superdurable/dex-agents/ui-stream: an AI SDK UI message stream (protocolv1) foruseChat, resumed through a UI stream resume token, withreset-stepfor a newRunModelattempt and tool approvals throughresolve_interrupt; plain Server-Sent Events resumed withLast-Event-ID; and snapshots as AI SDKUIMessageobjects or wire JSON.dex_agents.ui_stream: Server-Sent Event frames resumed withLast-Event-ID, and snapshot JSON, for async and synchronous Clients.- The testing exports' private engines accept
dex-server-zdconfiguration overrides:serverZdConfigPathin TypeScript andserver_zd_configin Python. - The browser snapshot JSON,
convertAgentSnapshotToJsonandagent_snapshot_to_json, leaves out the fields only the session's Steps use:framework_itemon messages, andtool_source_reference,trace_context,input_continuation, andinput_answerson tool calls. Both cores produce the same JSON. - The AI SDK UI message stream pauses for an approval only while its interrupt is still in
PendingInterrupts. A reconnect afterresolve_interruptand before the approval Step commits keeps reading instead of showing the approval as pending again.
OpenTelemetry GenAI spans
Both cores emit OpenTelemetry GenAI spans: one trace per activation, rooted at invoke_agent {agent name}, with a chat span per RunModel attempt and an execute_tool {tool name} span per tool Step attempt. Every span carries the session Flow ID as gen_ai.conversation.id and session.id, and its Dex Flow, run, and Step identity. Activations link to the previous one. Content is by reference unless the application opts in. Tracing never fails or retries a Step: OpenTelemetry errors are logged and the span is skipped.
dex-agents: the optionalotelextra installsopentelemetry-api.AgentSessionOptions.tracingtakesAgentTracingOptions(is_enabled,tracer_provider,agent_name,should_capture_message_content,semantic_conventions_version). Runners may returnModelCallMetadataasmodel_call_metadatainTurnCompletedandToolCallsRequested.@superdurable/dex-agents:@opentelemetry/api@^1.3.0is an optional peer dependency.AgentSessionOptions.tracingtakesAgentTracingOptions(isEnabled,tracerProvider,agentName,shouldCaptureMessageContent,semanticConventionsVersion). Runners may returnmodelCallMetadatain either outcome.- Both session Flows register the
AgentTraceContextAttribute, which holds the current activation'straceparentwhenever its root has a valid span context, sampled or not, so the activation's spans share its sampling decision.AgentToolCallgains an optionaltrace_context(traceContext), the activation root its tool spans join. - A
chatspan is namedchat {model}only when the runner returnsModelCallMetadata. Every framework adapter returns it with what its framework reports for the call and leaves the rest unset; each adapter's README lists its fields. When oneRunModelmakes several model calls, an adapter keeps only the values every call shares and leaves out the response ID and finish reasons. A provider identifier becomes its well-knowngen_ai.provider.namevalue of the pinned conventions when one applies, and other values pass through unchanged. No adapter derives a provider from a model name, class, base URL, or environment variable.- OpenAI Agents SDK, Python and TypeScript: the requested model when it is given by name, and the response ID unless it is the SDK's placeholder (
__fake_id__in Python,FAKE_IDin TypeScript). The SDK reports no provider, response model, or finish reason. - Pydantic AI: the requested model, and the response model, provider, response ID, and finish reason of the
ModelResponse. For thegoogle,google-cloud, andopenaiproviders the provider comes from the response'scanonical_system, which names the endpoint the client reached, so a Vertex AI client reportsgcp.vertex_aiand an Azure OpenAI clientazure.ai.openai. - LangChain and LangGraph: the chat model's
ls_model_name, and themodel_name,model_provider,id, and finish reason of the reply'sresponse_metadata.google_genaibecomesgcp.gen_ai, becauseChatGoogleGenerativeAIreports it for both the Gemini API and Vertex AI. - Google ADK: the requested model, and the response's
model_versionand finish reason. ADK keeps no response ID and names no provider. - Vercel AI SDK and Mastra: the model ID, the provider ID by its longest known prefix (
openai.responsesbecomesopenai,google.vertex.chatbecomesgcp.vertex_ai; an unknown ID keeps its first segment, sotogetherai.chatbecomestogetherai), the response model when it differs from the requested one, and the finish reason. They report no response ID: when the provider sends none, the AI SDK and Mastra fill in a generated ID and the requested model, and their public APIs do not say which it is. - Claude Agent SDK, Python and TypeScript: the model of the CLI's
initmessage, the provider from the result's model usage (firstPartybecomesanthropic,bedrockandmantlebecomeaws.bedrock,vertexbecomesgcp.vertex_ai, andgatewayreports none), and the response model, message ID, and stop reason of the replies. A segment in which a subagent called a model reports no request model.
- OpenAI Agents SDK, Python and TypeScript: the requested model when it is given by name, and the response ID unless it is the SDK's placeholder (
createMastraAgentSessionFlowandcreateOpenAIAgentsSessionFlowpasstracingto the session Flow. They dropped it before, so their sessions traced with the global tracer provider and the default options.- The Python helpers
claude_agent_session_flow,adk_agent_session_flow,openai_agents_session_flow,langchain_agent_session_flow,langgraph_agent_session_flow, andpydantic_ai_session_flowtaketracing, anAgentTracingOptions. - The TypeScript Claude Agent SDK adapter counts a subagent's model calls in
AgentUsage.modelCalls, as the Python adapter does. A turn segment with a subagent call therefore gets anINTERNALchatspan without usage in both languages.
Built-in agent loop (Stepwise mode)
Phase 1 of the M2 plan. Stepwise mode runs an agent defined in Dex terms, with no framework: one model call per RunModel Step, and every tool call its own Step, through the same session Flow the adapters use.
dex-agents:dex_agents.stepwisewithAgentDefinition,@function_tool(JSON Schemas from type hints, standard library only), theModelClientprotocol,StepwiseAgentRunner, andstepwise_agent_session_flow. Aftermax_model_calls_per_turnmodel calls the next call offers no tools; a tool call the model makes anyway gets the resultThe tool call was not run: this turn reached its limit of <n> model calls., and the turn ends.dex-agents:dex_agents.litellm.LiteLLMModelClientbehind the new extradex-agents[litellm](litellm>=1.104,<1.106; 1.106 makes every Responses API call import the undeclaredmcppackage). OpenAI models use the Responses API withstore=Falseand encrypted reasoning; other providers use streaming Chat Completions with Anthropic thinking signatures. Reasoning items and signatures replay byte for byte, and the client never retries.dex-agents:ScriptedModelClientindex_agents.testing, andAgentTestStack.start_worker(python_executable=...)to run a Worker on another environment, such as an earlier release.@superdurable/dex-agents/stepwise: Stepwise mode:createStepwiseAgentSessionFlow,StepwiseAgentRunner,functionTool, and theModelClientinterface.@superdurable/dex-agents/ai-sdk:createAiSdkModelClientfor any AI SDKLanguageModel, behind the optional peerai@^7.0.159. It makes onestreamTextcall perRunModel(one step,maxRetries0, tools withoutexecute) and replays reasoning and tool call parts with their provider metadata unchanged.@superdurable/dex-agents/testing:ScriptedModelClientfor Stepwise tests without a model key.
Stepwise tool sources, MCP resources and prompts, and the demo model
- Both cores:
AgentDefinition.tool_sources(toolSources) takes tool sources, such asMcpToolSource, through the newToolSourceandSourceToolDefinitionprotocols index_agents.stepwiseand@superdurable/dex-agents/stepwise. In everyRunModelof a chat or executing turn, the runner lists each source's tools and offers them after the function tools and before the built-in tools. A call of a source tool becomes the source'sdeferred_tool_call, so its policy, effect class, approval, idempotency key, andtool_source_referenceapply, and it runs through the source'sexecute_deferred_tool_callin its own tool Step. Each source has a unique, stablesource_name(sourceName), and the runner stores the source's reference as{"stepwise_tool_source": <source name>, "reference": ...}, so the tool Step andPollToolTaskreach the source that built the call without a listing and whatever the source order; a source name the Worker lacks raises a retryable error. The runner passes the definition it offered aslisted_tool(listedTool), so a refresh during the model call cannot change it.McpToolSourcedefaults its source name to"mcp"(DEFAULT_MCP_SOURCE_NAME,defaultMcpSourceName), which adding or removing a server keeps; twoMcpToolSources in one agent must name at least one.StepwiseAgentRunnernow implementspoll_tool_task(pollToolTask) and passes each task to its source; function tools start no tasks. A name that no listing offered fails as an unknown tool without an approval and without reaching a source. Each listing is bounded bytool_source_listing_timeout(toolSourceListingTimeoutMs, 10 seconds), and a source that fails or times out offers no tools in that model call instead of failing the turn. A listed name that repeats a function tool, an enabled built-in tool, or another source's tool fails theRunModelattempt before any model request, as a built-in name collision does; names a source declares with the optionaldeclared_tool_names()are checked when the definition is built. The optionalcached_tools()feeds the token estimate. - Both cores:
McpToolSourcecaches each server's listing per Worker, refreshes it in the background afterlisting_ttl(listingTtlMs, 5 minutes), and leaves a server that failed to list alone forlisting_failure_backoff(listingFailureBackoffMs, 30 seconds). One listing per server runs at a time. - Both cores:
McpToolSource(..., resource_and_prompt_tools_enabled=True)(resourceAndPromptToolsEnabled: true) also lists thereadtoolsmcp_list_resources_and_prompts,mcp_read_resource, andmcp_get_prompt, which take a configured server's name and reuse the source's transport and headers, with the source's connection attempts. Off by default; two sources with them are refused when the agent definition is built. The names, descriptions, argument schemas, and output JSON (sorted keys, whole numbers without.0) are the same in both cores. TypeScript needs nothing beyond@modelcontextprotocol/sdk1.32.2 for them. - Both cores:
ScriptedModelClient,ScriptedModelReply,ScriptedToolCall,ScriptedModelResponder, and the scripted provider and API constants are exported fromdex_agents.stepwiseand@superdurable/dex-agents/stepwiseas a supported model client for running an agent without a model key, such as an application's demo mode.requestskeeps the latest 100 requests,max_recorded_requests(maxRecordedRequests). Thetestingexports and module paths stay and name the same classes.
Questions, durable waits, and incremental turn messages
Both cores, with identical Step types, RPC names, wire values, events, and model-visible text. Everything is inert until an application enables it.
AgentSessionOptions.built_in_tools(builtInTools) enables theask_useranddurable_waitbuilt-in tools, whose definitions areBUILT_IN_TOOL_DEFINITIONS(builtInToolDefinitions).RunModelroutes their calls to their own Steps with no approval and thereadeffect, and fails invalid arguments at once.nullfor an optionalask_userargument means absent.ask_userraises an interrupt of kindquestion, which the newanswer_questionRPC answers and the newAwaitQuestionAnswerStep waits for; the answer is the tool result and the turn continues. A tool can ask too by returningToolInputRequested;AwaitQuestionAnswerraises its question under thePendingInterruptslock, so parallel tool calls can ask at once.interrupt_deadlineapplies to questions.durable_waitwaits on a whole-second Timer in the newAwaitTimerStep.AgentSessionOptions.max_durable_wait(maxDurableWaitMs) defaults to 365 days and must be whole seconds.ToolCallsRequested.new_messages(newMessages) stores a turn's items as they appear. A runner that sets it implementsbuild_unfinished_turn_messages(buildUnfinishedTurnMessages), whichRecordTurnFailurecalls; a runner without it fails the turn inRunModel's own commit, after one model call.- New wire values:
AgentToolCallstatusesawaiting_answerandwaiting_for_timer(non-final) and fieldswait_until,input_continuation,input_answers,input_question,input_choices, andinput_allows_free_text;AgentInterruptfieldsquestion,choices, andallows_free_text;AgentInterruptResolution.answer;AnswerQuestionRequest;QuestionAnswer; thetimer_startedevent; and new keys oninterrupt_raisedandinterrupt_resolved. - Both cores write RFC 3339 times with milliseconds and
Z, such as2026-11-01T12:00:00.123Z. Every reader in both cores, 0.1.0 included, accepts the earlier+00:00form too.
Plans and plan mode
Both cores, with identical Step types, persistence names, RPC names, wire values, events, and model-visible text. Everything is inert until an application enables it.
- The
write_todosbuilt-in replaces the newAgentPlanAttribute (revision,status,tasks,updated_at) with the next revision.RunModelrecords the call in its own commit and still moves toResumeTurn; no tool Step runs. An empty list clears the plan, and a response with twowrite_todoscalls fails both. The session writesplan_updated, and again when an approval marks a draftactive. AgentSessionOptions.plan_mode_enabled(planModeEnabled) acceptsAgentUserMessage.plan_mode(planMode) messages, which start a turn whose newAgentState.interaction_modeisplanning;send_user_messagerefuses them otherwise. It needswrite_todosand a runner that declaressupports_plan_mode(supportsPlanMode,PlanModeRunnerin Python). A planning turn rejects every call butwrite_todos.- The new
execute_planRPC approves one plan revision. It locks the newPendingPlanExecutionAttribute and publishes to the newPlanExecutionsChannelMap, keyed by revision, whichAwaitUserMessagewaits on after queued messages; it starts anexecutingturn and writesplan_execution_started. A stale revision, or a second approval of a revision not started yet, returns false. - Stepwise:
AgentDefinition.plan_mode_enabled(planModeEnabled). A planning turn offers onlywrite_todos, and every model call shows the plan withbuild_plan_note(buildPlanNote) before the turn's messages.AgentDefinition.planning_tool_choice(planningToolChoice) isautoby default, so a planning turn may also answer in text and end without a draft;requiredforceswrite_todoson the first call while no draft exists. In Python,requiredis refused for a model client whosesupports_forced_tool_choicereturns false, asLiteLLMModelClientdoes for Claude Opus 5.5, Claude Sonnet 5.5, and Anthropic extended thinking, which on an Anthropic, Bedrock Claude, or Vertex AI Claude model is athinkingoption or anyreasoning_effortbut"none".InstructionContextgainsinteraction_modeandplan. - Both cores reject
NaNandInfinityin built-in tool arguments and report the first unknown argument by code point, so the errors the model sees are the same. An explicitnullforplan_modeorinteraction_modereads as the default in both cores. AgentTurnContextgainsinteraction_modeandplan,AgentSnapshotgainsplan, andAgentSessionClient(Python) and the TypeScript testing client gainexecute_planand aplan_modeargument.
Steering and turn cancellation
Both cores, with identical Step types, RPC names, wire values, events, and model-visible text. Nothing changes until an application calls the new RPCs.
steer_user_message(SteerUserMessageRequest, with a queued message'smessage_idor newtext) steers the running turn, anddelete_queued_message(DeleteQueuedMessageRequest) removes a queued message. Both are transactional and lock nothing: a message consumed while the RPC runs fails it withChannelMessageNotFoundError, and nothing commits. While no turn is active,steer_user_messagewithtextqueues the message.cancel_turn(CancelTurnRequest) stops the running turn with no new turn;truemeans the request is recorded, and a turn whose model call already committed its final answer still completes. It locksTurnCancellationRequestandPendingInterrupts, so concurrent cancels apply in order, and a second cancel of the same turn changes nothing.cancel_turncancels the running model call or compaction (Python cancels the coroutine, which stops the LiteLLM call; TypeScript abortscancellationSignal, which the AI SDK client passes asabortSignal), and every Step that waits for an approval, an answer, a durable wait, or a task's next poll. The newFinishTurnCancellationStep decides from committed state how the turn ends: throughApplyTurnControlwhen the model call was cancelled, or throughResumeTurnonce every started tool Step finishes. A started tool task is abandoned: the newCancelToolTaskStep calls the optionalToolTaskCanceller.cancel_tool_task(cancelToolTask), withToolTaskCancellation, best-effort and recordsoutcome_unknownwithcancellation_reasonturn_cancelledandTOOL_TASK_ABANDONED_ERROR, even when the cancellation keeps failing. The Stepwise runner forwards it to a tool source's optionalcancel_tool_task, andMcpToolSourcesendstasks/cancel. A tool call Step that has started still runs to the end.- A steered turn ends at its next safe Step boundary. A model call or tool call that has started finishes. Calls that have not started record
rejectedwith the newAgentToolCall.cancellation_reason(steeredorturn_cancelled) and the result{"status":"cancelled","reason":"superseded_by_steered_user_message"}or{"status":"cancelled","reason":"turn_cancelled_by_user"}. Final statuses do not change. The newApplyTurnControlStep ends the turn, stores the runner's unfinished-turn messages or a steered or cancelled note, and starts a turn with every steered message, in order. That turn is a planning turn when any steered message hasplan_modeor a draft plan exists, as for a queued message, andexecute_planreturns false while a steered message is pending. Awrite_todoscall settles inRunModel's own commit, so one that a model call made while the user steered still replaces the plan. - New persistence: the
SteeredUserMessagesChannel, theTurnCancellationRequestAttribute, and the internalTurnControlRequestsChannel andToolCallCancellationsChannelMap.AgentStategainsaccepts_turn_control(acceptsTurnControl), which a turn that 0.2.0 started carries.ResumeTurnnow waits onTurnControlRequeststoo and locksPendingInterrupts.AwaitToolApproval,AwaitQuestionAnswer, andAwaitTimeralso wait on theirToolCallCancellationsinstance.AwaitUserMessagewakes on a steer or cancellation. get_agent_snapshotreturnsqueued_messagesandsteered_messages(PendingUserMessagewith the Channel message ID) next toqueued_message_count. New eventsturn_steeredandturn_cancelled;interrupt_resolved.cancellation_reasonis set when steering or cancellation removes an interrupt. New constantsTURN_STEERED_NOTE_HEADER,TURN_CANCELLED_NOTE_HEADER,TOOL_CALL_CANCELLED_BY_STEERING_RESULT, andTOOL_CALL_CANCELLED_BY_TURN_CANCELLATION_RESULT(camelCase in TypeScript).AgentSessionClientgainssteer_user_message,delete_queued_message, andcancel_turn; the TypeScriptAgentSessionTestClientgainssteerUserMessage,steerQueuedMessage,deleteQueuedMessage, andcancelTurn. The TypeScript UI message stream ends a steered or cancelled turn withfinishreasonother.build_unfinished_turn_messages(buildUnfinishedTurnMessages) is also called when a steered or cancelled turn ends. A runner that keeps its items in run state may implement it to keep its exact transcript.
Tool tasks
Both cores, with identical Step types, wire values, events, and error texts (the missing-poller text names each language's method). Inert until a runner returns the new outcome.
execute_deferred_tool_callmay returnToolTaskStarted(task_id, poll_interval_seconds)({ kind: "tool_task_started", taskId, pollIntervalSeconds }) when a tool started remote work. The tool Step records the new non-final statuswaiting_for_taskwith the newAgentToolCallfieldstask_idandtask_poll_interval_seconds, writes the newtool_task_startedevent, and moves to the newPollToolTaskStep.PollToolTaskwaits on a whole-second Timer and calls the runner'sToolTaskPoller.poll_tool_task(ToolTaskPoll)(optionalpollToolTaskin TypeScript) once per execution. It returns aToolExecutionResult, aToolTaskPending(poll_interval_seconds, continuation), whose continuation the next poll reads asinput_continuation, or aToolInputRequested, which asks throughAwaitQuestionAnswer. A task's question never ends the call: an answer, a decline, and a passed deadline each come back toPollToolTaskat once as aQuestionAnswerwith the newresolutionfield (answered,declined,deadline_passed), handed to the runner once asToolTaskPoll.pending_answerwhile the new record fieldtask_answer_pendingis set. A poll gets three attempts, then the call's failure route by effect. Each poll is anexecute_toolspan.PollToolTasktakes no lock, so steering waits for a started task and keeps its questions; a call with a task is never recordedrejected.cancel_turnabandons the task (see Steering and turn cancellation).DeferredToolExecution.supports_tool_tasks(supportsToolTasks) tells a tool whether the runner polls tasks; the MCP sources poll inside the tool Step without it.- A runner without the poll method that returns
ToolTaskStarted, a blanktask_id, or an interval that is not a number recordsfailedfor areadcall andoutcome_unknownotherwise (MISSING_TOOL_TASK_POLLER,BLANK_TOOL_TASK_ID,INVALID_TOOL_TASK_POLL_INTERVAL). DeferredToolOutcomegainsToolTaskStarted.
Context compaction
Both cores, with identical Step types, persistence names, wire values, events, and model-visible text (plan section A7). Inert until an application sets a compaction policy.
AgentSessionOptions.compactiontakes aCompactionPolicy(context_window_tokens,trigger_ratio0.85,retained_ratio0.3; TypeScriptcontextWindowTokens,triggerRatio,retainedRatio). It needs a runner with the compaction capability, PythonContextCompactionRunnerand TypeScript's optionalestimateInputTokens,selectCompactionCutoff, andsummarizeContext; the Flow constructor refuses it otherwise.- Before a model call that would cross the threshold,
RunModelmoves to the newCompactContextStep instead of calling the model.CompactContextmakes one summary call, withRunModel's timeouts, retries, and failure route, and writes the newContextSummaryAttribute; a crash reruns only the summary call. From then on the runner receives only the messages after the summary, plus the summary inAgentTurnContext.context_summary(contextSummary). Summarized messages stay stored until retention deletes them; see Chunked messages and retention. - Stepwise implements the capability, with
AgentDefinition.compaction. Its model input starts with[Dex] Summary of the earlier conversation:and the summary text, it cuts only between model responses whose tool calls all have stored results, so a long turn compacts mid-turn without separating a tool call from its result, and its summary call streams no text. When the summary request plus the configuredmax_output_tokenswould exceedContextSummaryRequest.input_token_budget(inputTokenBudget), the policy's window, it leaves out the oldest messages up to a cut that never splits a model response, and says so with[Dex] <n> earlier messages did not fit in this request and are left out.(Pythonbuild_context_summary_omission_note, TypeScriptbuildContextSummaryOmissionNote). - Each
CompactContextattempt emits achatspan for the summary call, asRunModeldoes, named from the optionalContextSummaryResult.model_call_metadata(modelCallMetadata). - The
message_countofturn_completed,turn_steered, andturn_cancelledcounts every message the turn appended, also those a mid-turn compaction summarized. A steer or cancellation that arrives duringCompactContextapplies at the nextRunModel, before any model call. - New wire values:
AgentState.statuscompacting_context;ContextSummary;CompactContextInput;AgentSnapshot.context_summary; thecompaction_startedandcompaction_finishedevents. The browser snapshot JSON of both UI stream modules carriescontext_summary, and the AI SDK UI message stream keeps the summary call's usage out of the message's steps. - Python:
AgentSessionStatusgainsCOMPACTING_CONTEXT, so an exhaustivematchover the status must handle it. TypeScript:AgentSessionStatusgains"compacting_context". A 0.1.0 client cannot decode a snapshot taken during compaction, so upgrade the clients that read snapshots before enabling it. - The 2,000-turn compaction sessions that calibrate the deployment guide run in the new
Agents Long Tests Nightlyworkflow: Python withDEX_AGENTS_LONG_TESTS=1, TypeScript withnpm run test:long.Agents CIruns an 800-turn guard in each core instead.
Chunked messages and retention
- Messages are stored as a sequentially chunked AttributeMap in every core.
AgentMessagesholds only the newest messages, one instance per message. When it holds more thanmessage_window_size(messageWindowSize,WithMessageWindowSizein Go; 64) messages, the oldest move into immutable instances of the newAgentMessageChunksAttributeMap, keyed by their first sequence, in the same commit. A chunk holds at mostmessage_chunk_size(32) messages andmessage_chunk_max_bytes(messageChunkMaxBytes; 1 MiB, at most 8 MiB) of encoded JSON. A larger message skips the window and goes straight into its own chunk when it is stored, so a message too large for the Server fails its turn at once and never wedges a later commit. One commit moves at most 4 chunks and 8 MiB. The newAgentMessageIndexAttribute lists every kept chunk's first and last sequence, the window start, and the oldest kept message; it changes only when chunks are written or deleted. The newTurnReplySequenceAttribute records where the latest turn's replies start. RunModelno longer loads the whole message map. With compaction, it loads the window and only the chunks after the summary; the snapshot andlist_agent_messagesload the window and every kept chunk.- With compaction, retention deletes the oldest chunk while every message in it is summarized and at least
retained_message_limit(retainedMessageLimit; 1,000;None/nullkeeps every message) messages are stored after it, so Flow state stays bounded and the newest 1,000 messages always stay. A deleted message leaveslist_agent_messagesandget_agent_snapshot. Their last page now starts at the oldest stored message, and the newAgentMessagesPage.deleted_before_sequence(deletedBeforeSequence) marks the deleted range. Copy messages out first if the application needs the whole transcript. A runner without the compaction capability on a session with deleted messages logs a warning on each model call. The Go core has no compaction, so it never deletes messages.
Go core
- New module
github.com/superdurable/dex/agents/go/core(packagedexagents), the Go core on the shared session contract: the session Flow and its Steps, persistence, RPCs (send_user_message,resolve_interrupt,get_agent_snapshot,list_agent_messages), theAgentEventsStream, effect classes and tool policies, approvals and approval deadlines, the turn-failure note, and theAgentFrameworkRunnercontract with incremental turn messages (ToolCallsRequested.NewMessagesandUnfinishedTurnMessageBuilder). It registers every persistence name of the contract and decodes every value the other cores write. Built-in tools, questions, durable waits, steering, plans, compaction, tool tasks, MCP, spans, UI streams, and framework adapters are not planned for Go. A Go Worker writesaccepts_turn_controlfalse, so steering RPCs refuse its turns. - Stepwise mode in
stepwise:NewAgentDefinition,NewFunctionToolandNewTypedFunctionToolwith the Python core's argument checks and messages,StepwiseAgentRunner, andNewStepwiseAgentSessionFlow. Model clients on the official SDKs:openaimodel.NewResponsesModelClient(openai-go, provideropenai, APIresponses, the same items as the Python LiteLLM client) andanthropicmodel.NewMessagesModelClient(anthropic-sdk-go). agentstest: a scripted model client whose items match the Python core's, a privatedexcli devengine, killable Worker processes, and Step history.- Clients interoperate across languages: Python and TypeScript Clients drive sessions on Go Workers, and a Go Client drives one on Python Workers, tested against a real engine. Serve each Flow type with Workers of one language: a Go Step that finds state from steering, plan mode, or compaction fails the session with a reason instead of acting on it. RPC names come from
dex.RPCOptions.Name, which needs a Go SDK release aftersdk-go/v1.32.0. - The Anthropic client stores the LiteLLM chat message that the Python core stores (
anthropic,chat_completions), so either core replays the other's thinking blocks, and it replays the TypeScript core's AI SDK Anthropic items with their signatures. An empty response stores no item.
Fixes
- TypeScript:
send_user_message, theAwaitUserMessageStep that takes its messages, andStartAgentSession's initial message check for blank text with Python'sstr.strip()whitespace, as the Python core does. Text made only of U+001C to U+001F or U+0085 is now blank, and text made only of U+FEFF is now a message. 0.1.0 usedString.trim(). - MCP server configurations no longer print secrets. In Python,
reprandstrofMcpStdioServerConfigandMcpStreamableHttpServerConfigshowenvandheadersnames with every value replaced by***. In TypeScript,util.inspectandconsole.logof anMcpToolSourceshow its servers the same way. The values sent to the servers are unchanged. - TypeScript:
send_user_messageis a transactional RPC, as the contract and the Python core declare it. - Python:
ScriptedModelClientwrites non-ASCII text in tool arguments and output items unescaped, as the TypeScript client and Python function tools do. - Both cores: the text a model sees for an MCP tool result renders structured content and non-text content blocks with sorted keys,
json.dumpsspacing, and non-ASCII unescaped, as the MCP resource and prompt tools already did. Python wrote them with escaped non-ASCII, and TypeScript wrote them compactly. - TypeScript: MCP resource and prompt tool output sorts integer-like keys by code point, as Python does. It put them first, in numeric order.
- Python: the test engine in
dex_agents.testingtries three times on new free ports when another process takes its port, as the TypeScript one does.
Conformance suite
agents/conformance/holds golden wire values, the names manifest, the model-visible text table, and scripted-turn transcripts that every core must reproduce. Both cores run it in their CI suites.
Compatibility
- Upgrade rule: stop every 0.1.0 Worker of a Flow type before any 0.2.0 Worker of it starts. Do not run them side by side. Once a 0.2.0 Worker stores a message in a 0.1.0 session, it moves the session's older messages from
AgentMessagesintoAgentMessageChunks, at most four chunks a Step. If a 0.1.0 Worker then runs a Step of that session, its turns fail, because it cannot read the moved messages. No stored message is lost. - Enable the new options and call the new RPCs only after every Worker of the Flow type runs 0.2.0. A session started on 0.1.0 continues on 0.2.0.
- Code that read a message with
get_attributes(AGENT_MESSAGES.instance(message_sequence_key(n)))finds only the window there; read messages withlist_agent_messagesorget_agent_snapshot. StepwiseAgentRunnernow implementsToolTaskPoller, so itsDeferredToolExecution.supports_tool_tasks(supportsToolTasks) is true. A subclass that already definedpoll_tool_taskkeeps its own. Function tools cannot start tasks, so a Stepwise agent without tool sources behaves as in 0.1.0.- Call
steer_user_messageandcancel_turnonly after every Worker runs 0.2.0 and every turn that was active during the rollout has ended. A Step's waits and locks come from the Worker that scheduled it, so such a turn can hold a wait that a steer never wakes. The RPCs refuse a turn that an earlier Worker started or advanced (AgentState.accepts_turn_controlis false), and a steer while no turn is active is queued, which every Worker's idle wait takes. A turn that started on 0.2.0 during a mixed rollout is not refused. ResumeTurnnow locksPendingInterruptsin its short Execute, soresolve_interruptandanswer_questioncan raiseRpcLockConflictErrorin sessions that never steer.AgentSessionClientretries it for 5 seconds; directinvoke_rpccallers should retry it too.- Python:
ToolCallStatusgains the three non-final members, so an exhaustivematchorassert_neverover the status of a stored record, such asAgentSnapshot.tool_calls, must handle them. Runners still see only final records.AgentFrameworkRunneris generic in whatexecute_deferred_tool_callreturns; a bareAgentFrameworkRunnermeansAgentFrameworkRunner[ToolExecutionResult], as in 0.1.0, and a runner whose tools ask the user implementsAgentFrameworkRunner[DeferredToolOutcome].AgentSessionFlow.runnerreturnsAgentFrameworkRunner[DeferredToolOutcome]. Every other 0.1.0 API accepts 0.1.0 code: new fields and parameters have defaults. - TypeScript:
AgentFrameworkRunner<ToolOutcome = ToolExecutionResult>is generic, so existing runners and code that calls them compile unchanged. A runner whose tools ask the user implementsAgentFrameworkRunner<DeferredToolOutcome>.AgentSnapshot.toolCallsand records read withClient.getAttributesare nowAgentToolCallRecord, whose status also allows the non-finalawaiting_answer,waiting_for_timer, andwaiting_for_task; code that assigns them toAgentToolCallmust widen the type.AgentToolCallStatus, which runners see, keeps its six values. Every other 0.1.0 type still accepts 0.1.0 code: new fields are optional. - TypeScript Claude Agent SDK:
AgentUsage.modelCallscounts a subagent's model calls, as the Python adapter always did; 0.1.0 counted only the main conversation. TheAgentUsageAttribute is cumulative, so in a session that started on 0.1.0 the total mixes both meanings: turns that ran on 0.1.0 count main-conversation calls only, and later turns count subagent calls too. - The Python adapters' lockfiles record the core's new extras; adapter code is unchanged.
Full changelog: agents/CHANGELOG.md. Changes since 0.1.0: agents/v0.1.0...agents/v0.2.0