dex-agents 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dex_agents/__init__.py ADDED
@@ -0,0 +1,131 @@
1
+ # Copyright (c) 2026 Super Durable, Inc.
2
+ #
3
+ # Licensed under the Sustainable Use License 1.0.
4
+ # You may not use this file except in compliance with the License.
5
+ # See the LICENSE file in the repository root.
6
+ #
7
+ # SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
+
9
+ """Durable agent sessions for Dex.
10
+
11
+ ``AgentSessionFlow`` runs one agent session per Flow execution. A framework
12
+ adapter implements ``AgentFrameworkRunner`` and passes it to the Flow; the
13
+ Flow runs every model call and every tool call as its own Step, and owns the
14
+ conversation, tool approvals, and live events. See the package README for the full contract.
15
+ """
16
+
17
+ from dex_agents.client import AgentSessionClient
18
+ from dex_agents.events import AgentEventType, AgentEventWriter
19
+ from dex_agents.persistence import (
20
+ AGENT_MESSAGES,
21
+ AGENT_STATE,
22
+ AGENT_TOOL_CALLS,
23
+ AGENT_USAGE,
24
+ DEFAULT_AGENT_EVENTS_CAPACITY_BYTES,
25
+ FRAMEWORK_RUN_STATE,
26
+ INTERRUPT_RESOLUTIONS,
27
+ PENDING_INTERRUPTS,
28
+ QUEUED_USER_MESSAGES,
29
+ TOOL_CALL_RESULTS,
30
+ message_sequence_key,
31
+ )
32
+ from dex_agents.runner import (
33
+ APPROVAL_DEADLINE_PASSED,
34
+ DEFAULT_REJECTION_TOOL_RESULT,
35
+ OUTCOME_UNKNOWN_TOOL_RESULT,
36
+ TURN_FAILURE_NOTE_FOOTER,
37
+ TURN_FAILURE_NOTE_HEADER,
38
+ AgentFrameworkRunner,
39
+ AgentTurnContext,
40
+ AgentTurnOutcome,
41
+ DeferredToolCall,
42
+ DeferredToolExecution,
43
+ FrameworkMessage,
44
+ ToolCallsRequested,
45
+ ToolExecutionError,
46
+ ToolExecutionResult,
47
+ ToolPolicy,
48
+ TurnCompleted,
49
+ build_turn_failure_note,
50
+ describe_tool_call_outcome,
51
+ resolve_tool_policy,
52
+ )
53
+ from dex_agents.session_flow import AgentSessionFlow, AgentSessionOptions
54
+ from dex_agents.values import (
55
+ AgentEvent,
56
+ AgentInterrupt,
57
+ AgentInterruptResolution,
58
+ AgentMessage,
59
+ AgentMessagesPage,
60
+ AgentSessionStartInput,
61
+ AgentSessionStatus,
62
+ AgentSnapshot,
63
+ AgentState,
64
+ AgentToolCall,
65
+ AgentUsage,
66
+ AgentUserMessage,
67
+ InterruptKind,
68
+ ListAgentMessagesRequest,
69
+ MessageRole,
70
+ ResolveInterruptRequest,
71
+ ToolCallCompleted,
72
+ ToolCallStatus,
73
+ ToolEffect,
74
+ )
75
+
76
+ __all__ = [
77
+ "AGENT_MESSAGES",
78
+ "AGENT_STATE",
79
+ "AGENT_TOOL_CALLS",
80
+ "AGENT_USAGE",
81
+ "APPROVAL_DEADLINE_PASSED",
82
+ "DEFAULT_AGENT_EVENTS_CAPACITY_BYTES",
83
+ "DEFAULT_REJECTION_TOOL_RESULT",
84
+ "FRAMEWORK_RUN_STATE",
85
+ "INTERRUPT_RESOLUTIONS",
86
+ "OUTCOME_UNKNOWN_TOOL_RESULT",
87
+ "PENDING_INTERRUPTS",
88
+ "QUEUED_USER_MESSAGES",
89
+ "TOOL_CALL_RESULTS",
90
+ "TURN_FAILURE_NOTE_FOOTER",
91
+ "TURN_FAILURE_NOTE_HEADER",
92
+ "AgentEvent",
93
+ "AgentEventType",
94
+ "AgentEventWriter",
95
+ "AgentFrameworkRunner",
96
+ "AgentInterrupt",
97
+ "AgentInterruptResolution",
98
+ "AgentMessage",
99
+ "AgentMessagesPage",
100
+ "AgentSessionClient",
101
+ "AgentSessionFlow",
102
+ "AgentSessionOptions",
103
+ "AgentSessionStartInput",
104
+ "AgentSessionStatus",
105
+ "AgentSnapshot",
106
+ "AgentState",
107
+ "AgentToolCall",
108
+ "AgentTurnContext",
109
+ "AgentTurnOutcome",
110
+ "AgentUsage",
111
+ "AgentUserMessage",
112
+ "DeferredToolCall",
113
+ "DeferredToolExecution",
114
+ "FrameworkMessage",
115
+ "InterruptKind",
116
+ "ListAgentMessagesRequest",
117
+ "MessageRole",
118
+ "ResolveInterruptRequest",
119
+ "ToolCallCompleted",
120
+ "ToolCallStatus",
121
+ "ToolCallsRequested",
122
+ "ToolEffect",
123
+ "ToolExecutionError",
124
+ "ToolExecutionResult",
125
+ "ToolPolicy",
126
+ "TurnCompleted",
127
+ "build_turn_failure_note",
128
+ "describe_tool_call_outcome",
129
+ "message_sequence_key",
130
+ "resolve_tool_policy",
131
+ ]
dex_agents/client.py ADDED
@@ -0,0 +1,224 @@
1
+ # Copyright (c) 2026 Super Durable, Inc.
2
+ #
3
+ # Licensed under the Sustainable Use License 1.0.
4
+ # You may not use this file except in compliance with the License.
5
+ # See the LICENSE file in the repository root.
6
+ #
7
+ # SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
+
9
+ """Typed Client calls for agent sessions."""
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ from datetime import timedelta
15
+
16
+ from dex import AsyncClient, RpcLockConflictError, StartFlowOptions, StreamMessage
17
+
18
+ from dex_agents.session_flow import AgentSessionFlow
19
+ from dex_agents.values import (
20
+ AgentEvent,
21
+ AgentMessagesPage,
22
+ AgentSessionStartInput,
23
+ AgentSnapshot,
24
+ AgentUserMessage,
25
+ ListAgentMessagesRequest,
26
+ ResolveInterruptRequest,
27
+ )
28
+
29
+ _LOCK_CONFLICT_RETRY_INTERVAL_SECONDS = 0.05
30
+
31
+
32
+ class AgentSessionClient:
33
+ """Start agent sessions and call their RPCs with typed arguments.
34
+
35
+ Wrap the application's ``AsyncClient`` and the registered
36
+ ``AgentSessionFlow``. Every method is a thin call through the Client; the
37
+ session ID is the Flow ID.
38
+
39
+ Example:
40
+ >>> sessions = AgentSessionClient(client, flow)
41
+ >>> await sessions.start_session("session-42")
42
+ >>> await sessions.send_user_message("session-42", "Refund order 7")
43
+ >>> snapshot = await sessions.get_agent_snapshot("session-42")
44
+ >>> for interrupt in snapshot.pending_interrupts:
45
+ ... await sessions.resolve_interrupt("session-42", interrupt.interrupt_id,
46
+ ... approved=True)
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ client: AsyncClient,
52
+ flow: AgentSessionFlow,
53
+ *,
54
+ lock_conflict_retry_timeout: timedelta = timedelta(seconds=5),
55
+ ) -> None:
56
+ """Create a typed view over one agent's sessions.
57
+
58
+ Args:
59
+ client: An open ``AsyncClient`` whose Registry contains ``flow``.
60
+ The caller keeps ownership and closes it.
61
+ flow: The registered ``AgentSessionFlow`` instance.
62
+ lock_conflict_retry_timeout: How long ``resolve_interrupt`` retries
63
+ an ``RpcLockConflictError``. Defaults to five seconds.
64
+ """
65
+ self._client = client
66
+ self._flow = flow
67
+ self._lock_conflict_retry_timeout = lock_conflict_retry_timeout
68
+
69
+ @property
70
+ def flow(self) -> AgentSessionFlow:
71
+ """Return the agent session Flow these calls target.
72
+
73
+ Returns:
74
+ The Flow passed to the constructor.
75
+ """
76
+ return self._flow
77
+
78
+ async def start_session(
79
+ self,
80
+ session_id: str,
81
+ initial_message: str | None = None,
82
+ options: StartFlowOptions | None = None,
83
+ ) -> str:
84
+ """Start one agent session.
85
+
86
+ Args:
87
+ session_id: Flow ID of the new session.
88
+ initial_message: Optional first user message.
89
+ options: Start options, such as a Request ID for an idempotent start.
90
+ ``None`` uses ``StartFlowOptions()``.
91
+
92
+ Returns:
93
+ The server-assigned run ID.
94
+
95
+ Raises:
96
+ FlowAlreadyStartedError: If the Flow ID is already in use.
97
+ """
98
+ return await self._client.start_flow(
99
+ self._flow,
100
+ session_id,
101
+ AgentSessionStartInput(initial_message=initial_message),
102
+ options or StartFlowOptions(),
103
+ )
104
+
105
+ async def send_user_message(self, session_id: str, text: str) -> bool:
106
+ """Queue one user message.
107
+
108
+ Args:
109
+ session_id: Running session.
110
+ text: Message text.
111
+
112
+ Returns:
113
+ ``True`` when queued; ``False`` for whitespace-only text.
114
+
115
+ Raises:
116
+ FlowNotActiveOrNotFoundError: If the session is not running.
117
+ """
118
+ return await self._client.invoke_rpc(
119
+ self._flow.send_user_message, session_id, AgentUserMessage(text=text)
120
+ )
121
+
122
+ async def resolve_interrupt(
123
+ self,
124
+ session_id: str,
125
+ interrupt_id: str,
126
+ *,
127
+ approved: bool,
128
+ rejection_message: str | None = None,
129
+ ) -> bool:
130
+ """Approve or reject one pending interrupt.
131
+
132
+ The call retries ``RpcLockConflictError`` until
133
+ ``lock_conflict_retry_timeout`` passes. A retry is safe because a
134
+ conflicting call makes no change.
135
+
136
+ Args:
137
+ session_id: Running session.
138
+ interrupt_id: ``AgentInterrupt.interrupt_id``.
139
+ approved: ``True`` to run the tool call.
140
+ rejection_message: Message the model sees after a rejection.
141
+
142
+ Returns:
143
+ ``True`` when resolved; ``False`` when it is unknown, already resolved,
144
+ or past its deadline.
145
+
146
+ Raises:
147
+ RpcLockConflictError: If the lock stays busy past the retry timeout.
148
+ FlowNotActiveOrNotFoundError: If the session is not running.
149
+ """
150
+ request = ResolveInterruptRequest(
151
+ interrupt_id=interrupt_id, approved=approved, rejection_message=rejection_message
152
+ )
153
+ loop = asyncio.get_running_loop()
154
+ deadline = loop.time() + self._lock_conflict_retry_timeout.total_seconds()
155
+ while True:
156
+ try:
157
+ return await self._client.invoke_rpc(
158
+ self._flow.resolve_interrupt, session_id, request
159
+ )
160
+ except RpcLockConflictError:
161
+ if loop.time() >= deadline:
162
+ raise
163
+ await asyncio.sleep(_LOCK_CONFLICT_RETRY_INTERVAL_SECONDS)
164
+
165
+ async def get_agent_snapshot(self, session_id: str) -> AgentSnapshot:
166
+ """Return the session snapshot a UI renders.
167
+
168
+ Args:
169
+ session_id: Running or retained closed session.
170
+
171
+ Returns:
172
+ The snapshot from ``get_agent_snapshot``.
173
+ """
174
+ return await self._client.invoke_rpc(self._flow.get_agent_snapshot, session_id)
175
+
176
+ async def list_agent_messages(
177
+ self,
178
+ session_id: str,
179
+ *,
180
+ before_sequence: int | None = None,
181
+ limit: int = 50,
182
+ ) -> AgentMessagesPage:
183
+ """Return one page of stored messages, oldest first.
184
+
185
+ Args:
186
+ session_id: Running or retained closed session.
187
+ before_sequence: Anchor from the previous page, or ``None`` for the newest page.
188
+ limit: Page size, clamped to 1 through 200.
189
+
190
+ Returns:
191
+ The page from ``list_agent_messages``.
192
+ """
193
+ return await self._client.invoke_rpc(
194
+ self._flow.list_agent_messages,
195
+ session_id,
196
+ ListAgentMessagesRequest(before_sequence=before_sequence, limit=limit),
197
+ )
198
+
199
+ async def read_agent_event(
200
+ self,
201
+ session_id: str,
202
+ resume_token: str = "",
203
+ timeout: timedelta | None = None,
204
+ ) -> StreamMessage[AgentEvent]:
205
+ """Wait for the next ``AgentEvents`` message after a resume token.
206
+
207
+ Pass each returned ``resume_token`` to the next call. Events are best
208
+ effort: after a gap, rebuild from ``get_agent_snapshot``.
209
+
210
+ Args:
211
+ session_id: Session whose events to read.
212
+ resume_token: Token from the previous message, or ``""`` for the
213
+ oldest retained event.
214
+ timeout: Server-side long-poll duration, or ``None`` for the default.
215
+
216
+ Returns:
217
+ The next event and its resume token.
218
+
219
+ Raises:
220
+ LongPollTimeoutError: If no event arrives before the long poll ends.
221
+ """
222
+ return await self._client.read_stream(
223
+ session_id, self._flow.agent_events, resume_token, timeout
224
+ )
dex_agents/events.py ADDED
@@ -0,0 +1,222 @@
1
+ # Copyright (c) 2026 Super Durable, Inc.
2
+ #
3
+ # Licensed under the Sustainable Use License 1.0.
4
+ # You may not use this file except in compliance with the License.
5
+ # See the LICENSE file in the repository root.
6
+ #
7
+ # SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
+
9
+ """Live event writing for the ``AgentEvents`` Stream."""
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import json
15
+ import threading
16
+ from collections.abc import Callable, Mapping
17
+ from enum import StrEnum
18
+
19
+ from dex_agents.values import AgentEvent, ToolCallStatus
20
+
21
+
22
+ class AgentEventType(StrEnum):
23
+ """Name every event the session writes to ``AgentEvents``.
24
+
25
+ ``AgentEvent.type`` holds one of these values as a string. Payloads are
26
+ JSON objects with snake_case keys.
27
+
28
+ Attributes:
29
+ TURN_STARTED: A user message started a turn. Payload ``{"sequence"}``,
30
+ the user message sequence.
31
+ TURN_COMPLETED: The turn produced its final answer. Payload
32
+ ``{"message_count"}``, the number of messages the turn appended.
33
+ TURN_FAILED: ``RecordTurnFailure`` ended the turn. Payload ``{"error"}``.
34
+ TEXT_DELTA: Visible model text. Payload ``{"text"}``.
35
+ REASONING_DELTA: A provider-authored reasoning summary. Payload ``{"text"}``.
36
+ TOOL_CALL_STARTED: A tool started. Payload ``{"tool_call_id", "tool_name"}``.
37
+ TOOL_CALL_FINISHED: A tool call's record became final, including a
38
+ rejection. Payload ``{"tool_call_id", "tool_name", "status"}``.
39
+ TOOL_CALL_DEFERRED: The model asked for a tool call, which runs in its own Step.
40
+ Payload ``{"tool_call_id", "tool_name", "effect", "requires_approval"}``.
41
+ INTERRUPT_RAISED: A tool call waits for approval. Payload
42
+ ``{"interrupt_id", "tool_call_id", "tool_name", "deadline"}``.
43
+ INTERRUPT_RESOLVED: An interrupt was answered or expired. Payload
44
+ ``{"interrupt_id", "approved", "rejection_message"}``.
45
+ USAGE_RECORDED: The session added one model call's usage. Payload
46
+ ``{"recorded", "session"}``, each an ``AgentUsage`` object.
47
+ """
48
+
49
+ TURN_STARTED = "turn_started"
50
+ TURN_COMPLETED = "turn_completed"
51
+ TURN_FAILED = "turn_failed"
52
+ TEXT_DELTA = "text_delta"
53
+ REASONING_DELTA = "reasoning_delta"
54
+ TOOL_CALL_STARTED = "tool_call_started"
55
+ TOOL_CALL_FINISHED = "tool_call_finished"
56
+ TOOL_CALL_DEFERRED = "tool_call_deferred"
57
+ INTERRUPT_RAISED = "interrupt_raised"
58
+ INTERRUPT_RESOLVED = "interrupt_resolved"
59
+ USAGE_RECORDED = "usage_recorded"
60
+
61
+
62
+ class AgentEventWriter:
63
+ """Write live events for one Step attempt to ``AgentEvents``.
64
+
65
+ The session gives every runner call a writer bound to the current turn ID,
66
+ Step execution ID, and Step attempt. Writes enqueue immediately and never
67
+ block. They are best effort: a retry can repeat them and a consumer can miss
68
+ them, so never put durable results only in events.
69
+
70
+ Every method is safe to call from any thread, such as a synchronous tool
71
+ running in an executor thread. A call from another thread hands the event
72
+ to the event loop that created the writer, and the Stream receives it in
73
+ call order. Events written after the runner call returns are dropped.
74
+
75
+ Example:
76
+ >>> async def run_model_step(self, turn: AgentTurnContext) -> AgentTurnOutcome:
77
+ ... async for chunk in model.stream(...):
78
+ ... turn.events.write_text_delta(chunk.text)
79
+ """
80
+
81
+ def __init__(
82
+ self,
83
+ write_agent_event: Callable[[AgentEvent], None],
84
+ turn_id: str | None,
85
+ attempt: int,
86
+ step_execution_id: str = "",
87
+ ) -> None:
88
+ """Create a writer that stamps every event with its turn and Step attempt.
89
+
90
+ The session constructs writers. Construct one yourself only to test a
91
+ runner without a Dex Server. A writer created while an event loop runs
92
+ calls ``write_agent_event`` only on that loop's thread.
93
+
94
+ Args:
95
+ write_agent_event: Callable that appends one event to the Stream.
96
+ turn_id: Turn stamped on every event, or ``None``.
97
+ attempt: One-based Step attempt stamped on every event.
98
+ step_execution_id: Dex Step execution ID stamped on every event.
99
+ """
100
+ self._write_agent_event = write_agent_event
101
+ self._turn_id = turn_id
102
+ self._attempt = attempt
103
+ self._step_execution_id = step_execution_id
104
+ try:
105
+ self._loop: asyncio.AbstractEventLoop | None = asyncio.get_running_loop()
106
+ except RuntimeError:
107
+ self._loop = None
108
+ self._loop_thread_id = threading.get_ident()
109
+
110
+ @property
111
+ def turn_id(self) -> str | None:
112
+ """Return the turn ID stamped on every event.
113
+
114
+ Returns:
115
+ ``"turn-<n>"``, or ``None`` outside a turn.
116
+ """
117
+ return self._turn_id
118
+
119
+ @property
120
+ def attempt(self) -> int:
121
+ """Return the Step attempt stamped on every event.
122
+
123
+ Returns:
124
+ The one-based attempt of the Step that owns this writer.
125
+ """
126
+ return self._attempt
127
+
128
+ @property
129
+ def step_execution_id(self) -> str:
130
+ """Return the Dex Step execution ID stamped on every event.
131
+
132
+ Returns:
133
+ The ID shared by every attempt of the Step that owns this writer.
134
+ """
135
+ return self._step_execution_id
136
+
137
+ def write_event(self, event_type: AgentEventType | str, payload: Mapping[str, object]) -> None:
138
+ """Write one event with a JSON object payload.
139
+
140
+ Prefer the typed methods below for the standard event types. Use this
141
+ method for an adapter-specific type; give it an adapter prefix, such as
142
+ ``"openai_agents.handoff"``, so it never collides with a standard type.
143
+
144
+ Args:
145
+ event_type: Event type stored in ``AgentEvent.type``.
146
+ payload: JSON-compatible object stored as ``AgentEvent.payload`` text.
147
+
148
+ Raises:
149
+ TypeError: If ``payload`` is not JSON-serializable.
150
+ RuntimeError: If called from another thread after the writer's
151
+ event loop closed.
152
+ """
153
+ event = AgentEvent(
154
+ type=str(event_type),
155
+ turn_id=self._turn_id,
156
+ attempt=self._attempt,
157
+ payload=json.dumps(dict(payload), ensure_ascii=False, sort_keys=True),
158
+ step_execution_id=self._step_execution_id,
159
+ )
160
+ if self._loop is None or threading.get_ident() == self._loop_thread_id:
161
+ self._write_agent_event(event)
162
+ return
163
+ # The Dex Step output queue belongs to the event loop and is not thread-safe.
164
+ self._loop.call_soon_threadsafe(self._write_agent_event, event)
165
+
166
+ def write_text_delta(self, text: str) -> None:
167
+ """Write a chunk of visible model text.
168
+
169
+ Every call writes one Stream message. Batch very small chunks when the
170
+ framework emits one per token.
171
+
172
+ Args:
173
+ text: Text chunk, written as ``{"text": text}``.
174
+ """
175
+ if text:
176
+ self.write_event(AgentEventType.TEXT_DELTA, {"text": text})
177
+
178
+ def write_reasoning_delta(self, text: str) -> None:
179
+ """Write a chunk of a provider-authored reasoning summary.
180
+
181
+ Call it only for an explicit provider reasoning-summary event. Never
182
+ label ordinary model text as reasoning.
183
+
184
+ Args:
185
+ text: Reasoning summary chunk, written as ``{"text": text}``.
186
+ """
187
+ if text:
188
+ self.write_event(AgentEventType.REASONING_DELTA, {"text": text})
189
+
190
+ def write_tool_call_started(self, tool_call_id: str, tool_name: str) -> None:
191
+ """Report that a tool started running.
192
+
193
+ The session writes this event when a tool call Step starts. A runner
194
+ writes it only for a tool its framework cannot defer.
195
+
196
+ Args:
197
+ tool_call_id: Framework tool call ID.
198
+ tool_name: Tool name.
199
+ """
200
+ self.write_event(
201
+ AgentEventType.TOOL_CALL_STARTED,
202
+ {"tool_call_id": tool_call_id, "tool_name": tool_name},
203
+ )
204
+
205
+ def write_tool_call_finished(
206
+ self,
207
+ tool_call_id: str,
208
+ tool_name: str,
209
+ status: ToolCallStatus,
210
+ ) -> None:
211
+ """Report that a tool finished running.
212
+
213
+ Args:
214
+ tool_call_id: Framework tool call ID.
215
+ tool_name: Tool name.
216
+ status: ``SUCCEEDED`` or ``FAILED`` for a call that ran, or another
217
+ final status the session recorded.
218
+ """
219
+ self.write_event(
220
+ AgentEventType.TOOL_CALL_FINISHED,
221
+ {"tool_call_id": tool_call_id, "tool_name": tool_name, "status": str(status)},
222
+ )
@@ -0,0 +1,87 @@
1
+ # Copyright (c) 2026 Super Durable, Inc.
2
+ #
3
+ # Licensed under the Sustainable Use License 1.0.
4
+ # You may not use this file except in compliance with the License.
5
+ # See the LICENSE file in the repository root.
6
+ #
7
+ # SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
+
9
+ """Persistence definitions shared by every agent session Flow type.
10
+
11
+ Every ``AgentSessionFlow`` registers these exact definition objects, so a
12
+ Client can read any agent session with ``AsyncClient.get_attributes``. The
13
+ ``AgentEvents`` Stream is not here: each Flow type owns its own Stream object,
14
+ available as ``AgentSessionFlow.agent_events``.
15
+
16
+ Example:
17
+ >>> values = await client.get_attributes("session-42", AGENT_STATE, AGENT_USAGE)
18
+ >>> state = values.get(AGENT_STATE)
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dex import Attribute, AttributeMap, Channel, ChannelMap
24
+
25
+ from dex_agents.values import (
26
+ AgentInterrupt,
27
+ AgentInterruptResolution,
28
+ AgentMessage,
29
+ AgentState,
30
+ AgentToolCall,
31
+ AgentUsage,
32
+ AgentUserMessage,
33
+ ToolCallCompleted,
34
+ )
35
+
36
+ #: Current session position. Unset until ``StartAgentSession`` commits.
37
+ AGENT_STATE: Attribute[AgentState] = Attribute("AgentState", AgentState)
38
+
39
+ #: Conversation messages keyed by ``message_sequence_key(sequence)``.
40
+ AGENT_MESSAGES: AttributeMap[AgentMessage] = AttributeMap("AgentMessages", AgentMessage)
41
+
42
+ #: Opaque framework run state JSON text, set only while a turn is suspended.
43
+ FRAMEWORK_RUN_STATE: Attribute[str] = Attribute("FrameworkRunState", str)
44
+
45
+ #: Deferred tool call records of the active turn, keyed by tool call ID.
46
+ AGENT_TOOL_CALLS: AttributeMap[AgentToolCall] = AttributeMap("AgentToolCalls", AgentToolCall)
47
+
48
+ #: Interrupts that ``resolve_interrupt`` can still answer.
49
+ PENDING_INTERRUPTS: Attribute[list[AgentInterrupt]] = Attribute(
50
+ "PendingInterrupts", list[AgentInterrupt]
51
+ )
52
+
53
+ #: Model usage of the whole session.
54
+ AGENT_USAGE: Attribute[AgentUsage] = Attribute("AgentUsage", AgentUsage)
55
+
56
+ #: User messages waiting for the session to start their turns.
57
+ QUEUED_USER_MESSAGES: Channel[AgentUserMessage] = Channel("QueuedUserMessages", AgentUserMessage)
58
+
59
+ #: Interrupt answers keyed by interrupt ID.
60
+ INTERRUPT_RESOLUTIONS: ChannelMap[AgentInterruptResolution] = ChannelMap(
61
+ "InterruptResolutions", AgentInterruptResolution
62
+ )
63
+
64
+ #: Internal results of tool call Steps; ``ResumeTurn`` waits for one per tool call.
65
+ TOOL_CALL_RESULTS: Channel[ToolCallCompleted] = Channel("ToolCallResults", ToolCallCompleted)
66
+
67
+ #: Default ``AgentEvents`` Stream capacity: 10 MiB.
68
+ DEFAULT_AGENT_EVENTS_CAPACITY_BYTES = 10 * 1024 * 1024
69
+
70
+
71
+ def message_sequence_key(sequence: int) -> str:
72
+ """Return the ``AgentMessages`` map instance for a message sequence.
73
+
74
+ Use it to read one stored message with ``AGENT_MESSAGES.instance(...)``.
75
+
76
+ Args:
77
+ sequence: Positive message sequence.
78
+
79
+ Returns:
80
+ The sequence as a 12-digit zero-padded decimal, such as ``"000000000007"``.
81
+
82
+ Raises:
83
+ ValueError: If ``sequence`` is not positive.
84
+ """
85
+ if sequence <= 0:
86
+ raise ValueError("message sequence must be positive")
87
+ return f"{sequence:012d}"
dex_agents/py.typed ADDED
File without changes