Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/agents-long-nightly.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Agents Long Tests Nightly

# The 2,000-turn compaction sessions run for minutes. Agents CI runs a 300-turn guard instead.
# The 2,000-turn compaction sessions run for minutes. Agents CI runs an 800-turn guard instead.

on:
schedule:
Expand Down
89 changes: 46 additions & 43 deletions agents/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,8 +424,8 @@ compaction policy.
route, and writes the new `ContextSummary` Attribute; a crash reruns only the
summary call. From then on the runner receives only the messages after the
summary, plus the summary in `AgentTurnContext.context_summary`
(`contextSummary`). Messages stay in `AgentMessages`, so
`list_agent_messages` and `get_agent_snapshot` still return all of them.
(`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
Expand Down Expand Up @@ -458,47 +458,40 @@ compaction policy.
- The 2,000-turn compaction sessions that calibrate the deployment guide run
in the new `Agents Long Tests Nightly` workflow: Python with
`DEX_AGENTS_LONG_TESTS=1`, TypeScript with `npm run test:long`. `Agents CI`
runs a 300-turn guard in each core instead.

### Session settings

- `AgentSessionStartInput.session_settings` (`sessionSettings`, Go
`SessionSettings`): an optional JSON object of per-session settings, such
as `{"model": "gpt-5-mini"}`, stored in the new `SessionSettings`
Attribute. `update_session_settings` replaces them; the change takes effect
when the next turn starts, and a running turn keeps the settings it started
with, stored in `TurnSessionSettings` and tagged with the turn, for every
model call and retry. A copy that an earlier turn left reads as no settings.
- Runners read `AgentTurnContext.session_settings`, an empty mapping when
there are none; `ContextSummaryRequest` and Stepwise's `InstructionContext`
carry them too, and `AgentSnapshot.session_settings` returns the next
turn's settings.
- Stepwise: `AgentDefinition.resolve_model` (`resolveModel`, Go
`stepwise.WithModelResolver`) maps the settings to the model client of each
model call, including summary calls and token estimates.
- Settings are Flow state, and the snapshot and the browser snapshot JSON
return them, so they hold references the Worker resolves, never secrets.
Only model calls see them; tool calls do not. Their compact JSON text in
UTF-8 is at most `MAX_SESSION_SETTINGS_BYTES` (16,384, Go
`MaxSessionSettingsBytes`); `validate_session_settings` and
`session_settings_size` (`validateSessionSettings`, `sessionSettingsSize`)
check it, and `AgentSessionClient.start_session(..., session_settings=...)`,
`AgentSessionClient.update_session_settings`, the TypeScript
`AgentSessionTestClient.startSession`, and Go's `StartSessionWithSettings`
and `UpdateSessionSettings` call them first. The cores measure floats and
U+2028/U+2029 differently, so the session's own checks accept
`SESSION_SETTINGS_SIZE_TOLERANCE_BYTES` (1,024) more.
- `StartAgentSession` fails the session at once, with no retry, for start
settings it refuses, with a reason that starts with
`INVALID_START_SESSION_SETTINGS_REASON`. `update_session_settings` returns
false for a missing or `null` `session_settings`.
- Stepwise in Python checks a resolved model against
`planning_tool_choice="required"` when the planning call runs.
- Sessions started on 0.1.0 have no settings and keep running with an empty
mapping until an update. Start sessions with settings, and call
`update_session_settings`, only after every Worker of the Flow type runs
0.2.0: a 0.1.0 Worker drops the start settings and starts turns without
them.
runs an 800-turn guard in each core instead.

### Chunked messages and retention

- Messages are stored as a sequentially chunked AttributeMap in every core.
`AgentMessages` holds only the newest messages, one instance per message.
When it holds more than `message_window_size` (`messageWindowSize`,
`WithMessageWindowSize` in Go; 64) messages, the oldest move into immutable
instances of the new `AgentMessageChunks` AttributeMap, keyed by their first
sequence, in the same commit. A chunk holds at most `message_chunk_size`
(32) messages and `message_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 new `AgentMessageIndex`
Attribute 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 new `TurnReplySequence` Attribute records where the latest
turn's replies start.
- `RunModel` no longer loads the whole message map. With compaction, it loads
the window and only the chunks after the summary; the snapshot and
`list_agent_messages` load 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`/`null` keeps every message) messages
are stored after it, so Flow state stays bounded and the newest 1,000
messages always stay. A deleted message leaves `list_agent_messages` and
`get_agent_snapshot`. Their last page now starts at the oldest stored
message, and the new `AgentMessagesPage.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

Expand Down Expand Up @@ -569,8 +562,18 @@ compaction policy.

### 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 `AgentMessages` into `AgentMessageChunks`, 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 with `list_agent_messages` or
`get_agent_snapshot`.
- `StepwiseAgentRunner` now implements `ToolTaskPoller`, so its
`DeferredToolExecution.supports_tool_tasks` (`supportsToolTasks`) is true. A
subclass that already defined `poll_tool_task` keeps its own. Function tools
Expand Down
5 changes: 3 additions & 2 deletions agents/conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,9 @@ sweep an enum. The `AgentEvent` cases have one event per event type.
formats, and the RFC 3339 format with examples;
- `limits`: snapshot and page limits, the default list page size, the default
durable wait and Stepwise model call limits, the channel read limit,
`max_session_settings_bytes`, and `session_settings_size_tolerance_bytes`,
each taken from the Python core's constants;
`max_session_settings_bytes`, `session_settings_size_tolerance_bytes`, and
the default message window size, chunk size, chunk byte cap, chunks moved per
commit, and retention limit, each taken from the Python core's constants;
- `session_settings_sizes`: settings with the size every core's
`session_settings_size` must compute (UTF-8 bytes of the core's own compact
JSON text, non-ASCII unescaped). `bytes` is every core's size unless
Expand Down
76 changes: 76 additions & 0 deletions agents/conformance/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,15 @@
BUILT_IN_TOOL_DEFINITIONS,
DEFAULT_AGENT_EVENTS_CAPACITY_BYTES,
DEFAULT_MAX_DURABLE_WAIT,
DEFAULT_MESSAGE_CHUNK_MAX_BYTES,
DEFAULT_MESSAGE_CHUNK_SIZE,
DEFAULT_MESSAGE_WINDOW_SIZE,
DEFAULT_REJECTION_TOOL_RESULT,
DEFAULT_RETAINED_MESSAGE_LIMIT,
INVALID_TOOL_TASK_POLL_INTERVAL,
MAX_SESSION_SETTINGS_BYTES,
MESSAGE_ROLLOVER_MAX_BYTES_PER_COMMIT,
MESSAGE_ROLLOVER_MAX_CHUNKS_PER_COMMIT,
NO_PLAN_NOTE,
OUTCOME_UNKNOWN_TOOL_RESULT,
PLANNING_TURN_INSTRUCTION,
Expand All @@ -73,6 +79,9 @@
AgentInterrupt,
AgentInterruptResolution,
AgentMessage,
AgentMessageChunk,
AgentMessageChunkRange,
AgentMessageIndex,
AgentMessagesPage,
AgentPlan,
AgentPlanTask,
Expand Down Expand Up @@ -111,6 +120,7 @@
TurnCancellationRequest,
TurnControlKind,
TurnControlRequest,
TurnReplySequence,
TurnSessionSettings,
UpdateSessionSettingsRequest,
build_plan_execution_message,
Expand Down Expand Up @@ -225,6 +235,9 @@
"ExecutePlanRequest": ExecutePlanRequest,
"PlanExecutionRequest": PlanExecutionRequest,
"ContextSummary": ContextSummary,
"AgentMessageChunk": AgentMessageChunk,
"AgentMessageIndex": AgentMessageIndex,
"TurnReplySequence": TurnReplySequence,
"CompactContextInput": CompactContextInput,
"TurnFailureRouteInput": CompactContextInput | None,
"AgentEvent": AgentEvent,
Expand Down Expand Up @@ -898,6 +911,45 @@ def build_value_documents() -> list[dict[str, Any]]:
"full", "A second-generation summary.", full_summary()
)
)
documents.append(
ValueCases(
"AgentMessageChunk", "One AgentMessageChunks map instance: an immutable archive chunk."
).value(
"two-messages",
"A chunk of two messages, keyed by its first sequence.",
AgentMessageChunk(messages=[user_message(2), tool_message()]),
)
)
documents.append(
ValueCases("AgentMessageIndex", "The AgentMessageIndex Attribute.")
.value(
"full",
"Retention deleted the first chunk, a byte-capped chunk is shorter, and an oversized "
"message 80 sits alone among the window's messages.",
AgentMessageIndex(
chunks=[
AgentMessageChunkRange(first_sequence=33, last_sequence=64),
AgentMessageChunkRange(first_sequence=65, last_sequence=71),
AgentMessageChunkRange(first_sequence=72, last_sequence=72),
AgentMessageChunkRange(first_sequence=80, last_sequence=80),
],
first_sequence=33,
window_first_sequence=73,
),
)
.value(
"defaults",
"Nothing archived yet.",
AgentMessageIndex(chunks=[], first_sequence=1, window_first_sequence=1),
)
)
documents.append(
ValueCases("TurnReplySequence", "The TurnReplySequence Attribute.").value(
"full",
"Turn 3's replies start after its two user messages, 10 and 11.",
TurnReplySequence(turn_id="turn-3", sequence=12),
)
)
documents.append(
ValueCases("CompactContextInput", "Input of CompactContext.").value(
"full", "A cutoff sequence.", CompactContextInput(through_sequence=40)
Expand Down Expand Up @@ -976,6 +1028,18 @@ def build_value_documents() -> list[dict[str, Any]]:
.value(
"last", "The oldest page.", AgentMessagesPage(messages=[], next_before_sequence=None)
)
.value(
"after-retention",
"The oldest kept page after retention deleted messages 1 through 32.",
AgentMessagesPage(
messages=[user_message(33)], next_before_sequence=None, deleted_before_sequence=33
),
)
.wire(
"released-0.1.0",
"As dex-agents 0.1.0 wrote it, without deleted_before_sequence.",
{"messages": [], "next_before_sequence": None},
)
)

documents.append(
Expand Down Expand Up @@ -1208,6 +1272,12 @@ def build_names() -> dict[str, Any]:
"format": "the plan revision as a decimal",
"examples": {str(revision): plan_execution_key(revision) for revision in (1, 12)},
},
"AgentMessageChunks": {
"format": "the chunk's first message sequence as a 12-digit zero-padded decimal",
"examples": {
str(sequence): message_sequence_key(sequence) for sequence in (1, 33, 123457)
},
},
"AgentToolCalls": {"format": "the tool call ID"},
"InterruptResolutions": {"format": "the interrupt ID"},
"ToolCallCancellations": {"format": "the tool call ID"},
Expand Down Expand Up @@ -1251,6 +1321,12 @@ def build_names() -> dict[str, Any]:
"stepwise_default_max_model_calls_per_turn": DEFAULT_MAX_MODEL_CALLS_PER_TURN,
"max_session_settings_bytes": MAX_SESSION_SETTINGS_BYTES,
"session_settings_size_tolerance_bytes": SESSION_SETTINGS_SIZE_TOLERANCE_BYTES,
"default_message_window_size": DEFAULT_MESSAGE_WINDOW_SIZE,
"default_message_chunk_size": DEFAULT_MESSAGE_CHUNK_SIZE,
"default_message_chunk_max_bytes": DEFAULT_MESSAGE_CHUNK_MAX_BYTES,
"message_rollover_max_chunks_per_commit": MESSAGE_ROLLOVER_MAX_CHUNKS_PER_COMMIT,
"message_rollover_max_bytes_per_commit": MESSAGE_ROLLOVER_MAX_BYTES_PER_COMMIT,
"default_retained_message_limit": DEFAULT_RETAINED_MESSAGE_LIMIT,
},
}

Expand Down
33 changes: 32 additions & 1 deletion agents/conformance/names.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
"is_transactional": false,
"locks": [],
"load_attribute_maps": [
"AgentMessageChunks",
"AgentMessages",
"AgentToolCalls"
],
Expand All @@ -155,6 +156,7 @@
"is_transactional": false,
"locks": [],
"load_attribute_maps": [
"AgentMessageChunks",
"AgentMessages"
],
"load_channels": []
Expand Down Expand Up @@ -206,6 +208,16 @@
"kind": "stream",
"value_type": "AgentEvent"
},
{
"name": "AgentMessageChunks",
"kind": "attribute_map",
"value_type": "AgentMessageChunk"
},
{
"name": "AgentMessageIndex",
"kind": "attribute",
"value_type": "AgentMessageIndex"
},
{
"name": "AgentMessages",
"kind": "attribute_map",
Expand Down Expand Up @@ -301,6 +313,11 @@
"kind": "channel",
"value_type": "TurnControlRequest"
},
{
"name": "TurnReplySequence",
"kind": "attribute",
"value_type": "TurnReplySequence"
},
{
"name": "TurnSessionSettings",
"kind": "attribute",
Expand Down Expand Up @@ -419,6 +436,14 @@
"12": "12"
}
},
"AgentMessageChunks": {
"format": "the chunk's first message sequence as a 12-digit zero-padded decimal",
"examples": {
"1": "000000000001",
"33": "000000000033",
"123457": "000000123457"
}
},
"AgentToolCalls": {
"format": "the tool call ID"
},
Expand Down Expand Up @@ -606,6 +631,12 @@
"turn_control_at_most": 2147483647,
"stepwise_default_max_model_calls_per_turn": 100,
"max_session_settings_bytes": 16384,
"session_settings_size_tolerance_bytes": 1024
"session_settings_size_tolerance_bytes": 1024,
"default_message_window_size": 64,
"default_message_chunk_size": 32,
"default_message_chunk_max_bytes": 1048576,
"message_rollover_max_chunks_per_commit": 4,
"message_rollover_max_bytes_per_commit": 8388608,
"default_retained_message_limit": 1000
}
}
13 changes: 13 additions & 0 deletions agents/conformance/values/AgentMessageChunk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"type": "AgentMessageChunk",
"wire_kind": "json",
"description": "One AgentMessageChunks map instance: an immutable archive chunk.",
"cases": [
{
"name": "two-messages",
"description": "A chunk of two messages, keyed by its first sequence.",
"wire": "{\"messages\":[{\"created_at\":\"2026-11-02T09:30:00.000Z\",\"framework_item\":\"{\\\"type\\\":\\\"user_text\\\",\\\"text\\\":\\\"Where is order 7?\\\"}\",\"role\":\"user\",\"sequence\":2,\"text\":\"Where is order 7?\",\"tool_arguments\":null,\"tool_call_id\":null,\"tool_name\":null,\"tool_output\":null,\"turn_id\":\"turn-1\"},{\"created_at\":\"2026-11-02T09:30:00.000Z\",\"framework_item\":\"{\\\"type\\\":\\\"tool_result\\\",\\\"call_id\\\":\\\"call-7\\\",\\\"output\\\":\\\"moved 5\\\"}\",\"role\":\"tool\",\"sequence\":7,\"text\":null,\"tool_arguments\":\"{\\\"amount\\\": 5}\",\"tool_call_id\":\"call-7\",\"tool_name\":\"transfer_funds\",\"tool_output\":\"Gr\\u00fc\\u00dfe \\\"quoted\\\" \\\\ back\\nline\\ttab \\u2615 \\ud83d\\ude00 </script> & <ok>\",\"turn_id\":\"turn-3\"}]}",
"canonical": "{\"messages\":[{\"created_at\":\"2026-11-02T09:30:00.000Z\",\"framework_item\":\"{\\\"type\\\":\\\"user_text\\\",\\\"text\\\":\\\"Where is order 7?\\\"}\",\"role\":\"user\",\"sequence\":2,\"text\":\"Where is order 7?\",\"tool_arguments\":null,\"tool_call_id\":null,\"tool_name\":null,\"tool_output\":null,\"turn_id\":\"turn-1\"},{\"created_at\":\"2026-11-02T09:30:00.000Z\",\"framework_item\":\"{\\\"type\\\":\\\"tool_result\\\",\\\"call_id\\\":\\\"call-7\\\",\\\"output\\\":\\\"moved 5\\\"}\",\"role\":\"tool\",\"sequence\":7,\"text\":null,\"tool_arguments\":\"{\\\"amount\\\": 5}\",\"tool_call_id\":\"call-7\",\"tool_name\":\"transfer_funds\",\"tool_output\":\"Gr\\u00fc\\u00dfe \\\"quoted\\\" \\\\ back\\nline\\ttab \\u2615 \\ud83d\\ude00 </script> & <ok>\",\"turn_id\":\"turn-3\"}]}"
}
]
}
19 changes: 19 additions & 0 deletions agents/conformance/values/AgentMessageIndex.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"type": "AgentMessageIndex",
"wire_kind": "json",
"description": "The AgentMessageIndex Attribute.",
"cases": [
{
"name": "full",
"description": "Retention deleted the first chunk, a byte-capped chunk is shorter, and an oversized message 80 sits alone among the window's messages.",
"wire": "{\"chunks\":[{\"first_sequence\":33,\"last_sequence\":64},{\"first_sequence\":65,\"last_sequence\":71},{\"first_sequence\":72,\"last_sequence\":72},{\"first_sequence\":80,\"last_sequence\":80}],\"first_sequence\":33,\"window_first_sequence\":73}",
"canonical": "{\"chunks\":[{\"first_sequence\":33,\"last_sequence\":64},{\"first_sequence\":65,\"last_sequence\":71},{\"first_sequence\":72,\"last_sequence\":72},{\"first_sequence\":80,\"last_sequence\":80}],\"first_sequence\":33,\"window_first_sequence\":73}"
},
{
"name": "defaults",
"description": "Nothing archived yet.",
"wire": "{\"chunks\":[],\"first_sequence\":1,\"window_first_sequence\":1}",
"canonical": "{\"chunks\":[],\"first_sequence\":1,\"window_first_sequence\":1}"
}
]
}
Loading
Loading