modmex-ai 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.
Files changed (78) hide show
  1. modmex_ai/__init__.py +155 -0
  2. modmex_ai/agents/__init__.py +16 -0
  3. modmex_ai/agents/agent.py +249 -0
  4. modmex_ai/agents/context.py +16 -0
  5. modmex_ai/agents/execution.py +172 -0
  6. modmex_ai/agents/handoff.py +117 -0
  7. modmex_ai/agents/result.py +23 -0
  8. modmex_ai/agents/stream.py +28 -0
  9. modmex_ai/approvals.py +35 -0
  10. modmex_ai/errors.py +107 -0
  11. modmex_ai/evals.py +84 -0
  12. modmex_ai/flows/__init__.py +15 -0
  13. modmex_ai/flows/continuation.py +12 -0
  14. modmex_ai/flows/flow.py +608 -0
  15. modmex_ai/flows/result.py +32 -0
  16. modmex_ai/flows/state.py +86 -0
  17. modmex_ai/flows/stream.py +22 -0
  18. modmex_ai/guardrails/__init__.py +27 -0
  19. modmex_ai/guardrails/guardrail.py +89 -0
  20. modmex_ai/guardrails/result.py +11 -0
  21. modmex_ai/http/__init__.py +6 -0
  22. modmex_ai/http/async_client.py +178 -0
  23. modmex_ai/http/client.py +234 -0
  24. modmex_ai/http/retries.py +14 -0
  25. modmex_ai/http/sse.py +29 -0
  26. modmex_ai/messages/__init__.py +4 -0
  27. modmex_ai/messages/message.py +17 -0
  28. modmex_ai/models/__init__.py +26 -0
  29. modmex_ai/models/base.py +29 -0
  30. modmex_ai/models/fake.py +39 -0
  31. modmex_ai/models/fallback.py +31 -0
  32. modmex_ai/models/profile.py +18 -0
  33. modmex_ai/models/provider_state.py +17 -0
  34. modmex_ai/models/request.py +22 -0
  35. modmex_ai/models/response.py +33 -0
  36. modmex_ai/models/settings.py +11 -0
  37. modmex_ai/models/stream.py +28 -0
  38. modmex_ai/models/usage.py +41 -0
  39. modmex_ai/observability.py +24 -0
  40. modmex_ai/providers/__init__.py +1 -0
  41. modmex_ai/providers/openai/__init__.py +26 -0
  42. modmex_ai/providers/openai/audio.py +161 -0
  43. modmex_ai/providers/openai/chat.py +86 -0
  44. modmex_ai/providers/openai/mapper.py +478 -0
  45. modmex_ai/providers/openai/profile.py +22 -0
  46. modmex_ai/providers/openai/realtime.py +409 -0
  47. modmex_ai/providers/openai/responses.py +99 -0
  48. modmex_ai/providers/openai/transcription.py +190 -0
  49. modmex_ai/providers/openai_compatible/__init__.py +4 -0
  50. modmex_ai/realtime/__init__.py +5 -0
  51. modmex_ai/realtime/event.py +13 -0
  52. modmex_ai/realtime/session.py +81 -0
  53. modmex_ai/realtime/transport.py +16 -0
  54. modmex_ai/schemas/__init__.py +13 -0
  55. modmex_ai/schemas/json_schema.py +110 -0
  56. modmex_ai/schemas/modmex.py +38 -0
  57. modmex_ai/sessions/__init__.py +7 -0
  58. modmex_ai/sessions/durable.py +38 -0
  59. modmex_ai/sessions/item.py +97 -0
  60. modmex_ai/sessions/memory.py +38 -0
  61. modmex_ai/sessions/session.py +28 -0
  62. modmex_ai/sessions/snapshot.py +38 -0
  63. modmex_ai/tools/__init__.py +4 -0
  64. modmex_ai/tools/executor.py +44 -0
  65. modmex_ai/tools/tool.py +215 -0
  66. modmex_ai/tracing/__init__.py +5 -0
  67. modmex_ai/tracing/step.py +27 -0
  68. modmex_ai/tracing/trace.py +26 -0
  69. modmex_ai/voice/__init__.py +49 -0
  70. modmex_ai/voice/chained.py +385 -0
  71. modmex_ai/voice/continuation.py +19 -0
  72. modmex_ai/voice/event.py +37 -0
  73. modmex_ai/voice/providers.py +129 -0
  74. modmex_ai/voice/session.py +21 -0
  75. modmex_ai/voice/turn.py +41 -0
  76. modmex_ai-0.1.0.dist-info/METADATA +305 -0
  77. modmex_ai-0.1.0.dist-info/RECORD +78 -0
  78. modmex_ai-0.1.0.dist-info/WHEEL +4 -0
modmex_ai/__init__.py ADDED
@@ -0,0 +1,155 @@
1
+ """Lightweight AI flows, agents, tools, and model adapters."""
2
+
3
+ from modmex_ai.agents import (
4
+ Agent,
5
+ AgentResult,
6
+ AgentStreamEvent,
7
+ AgentStreamEventType,
8
+ Handoff,
9
+ RECOMMENDED_PROMPT_PREFIX,
10
+ RunContext,
11
+ prompt_with_handoff_instructions,
12
+ )
13
+ from modmex_ai.models import (
14
+ AsyncModelClient,
15
+ FakeModel,
16
+ FallbackModel,
17
+ ModelClient,
18
+ ModelProfile,
19
+ ModelRequest,
20
+ ModelResponse,
21
+ ModelStreamEvent,
22
+ ModelStreamEventType,
23
+ ModelSettings,
24
+ ProviderState,
25
+ ToolCall,
26
+ Usage,
27
+ )
28
+ from modmex_ai.sessions import InMemorySession, Session, SessionItem, SessionSnapshot
29
+ from modmex_ai.sessions import DurableSessionStore, InMemoryDurableSessionStore, SessionConflictError
30
+ from modmex_ai.approvals import ApprovalDecision, ApprovalDecisionType, ApprovalPolicy, ApprovalRequest
31
+ from modmex_ai.observability import ObservabilityEvent, ObservabilityObserver
32
+ from modmex_ai.evals import EvalCase, EvalResult, EvalRunner
33
+ from modmex_ai.tools import Tool, ToolExecutor, ToolResult, as_tool, tool
34
+ from modmex_ai.flows import Flow, FlowContinuation, FlowResult, FlowStreamEvent, FlowStreamEventType, FlowStateConflictError, FlowStateStatus, FlowStateStore, FlowSuspended, FlowSuspension, InMemoryFlowStateStore, PersistedFlowState
35
+ from modmex_ai.guardrails import (
36
+ Guardrail,
37
+ GuardrailResult,
38
+ InputGuardrail,
39
+ OutputGuardrail,
40
+ ToolInputGuardrail,
41
+ ToolOutputGuardrail,
42
+ )
43
+ from modmex_ai.realtime import RealtimeEvent, RealtimeSession, RealtimeTransport
44
+ from modmex_ai.voice import (
45
+ ChainedVoiceSession,
46
+ CallableSpeechToTextProvider,
47
+ CallableTextToSpeechProvider,
48
+ LiveSpeechToTextProvider,
49
+ LiveSpeechToTextSession,
50
+ SpeechToTextProvider,
51
+ StreamingSpeechToTextProvider,
52
+ StreamingTextToSpeechProvider,
53
+ TextToSpeechProvider,
54
+ Transcription,
55
+ VoiceInputEvent,
56
+ VoiceInputEventType,
57
+ VoiceAgentSession,
58
+ VoiceSessionEvent,
59
+ VoiceSessionEventType,
60
+ VoiceContinuation,
61
+ VoiceTerminationReason,
62
+ VoiceTurnOptions,
63
+ VoiceTurnMetrics,
64
+ VoiceTurnOutcome,
65
+ VoiceTurnReason,
66
+ VoiceTurnObserver,
67
+ )
68
+
69
+ __all__ = [
70
+ "Agent",
71
+ "AsyncModelClient",
72
+ "AgentResult",
73
+ "AgentStreamEvent",
74
+ "AgentStreamEventType",
75
+ "FakeModel",
76
+ "FallbackModel",
77
+ "InMemorySession",
78
+ "DurableSessionStore",
79
+ "InMemoryDurableSessionStore",
80
+ "SessionConflictError",
81
+ "ModelClient",
82
+ "ModelProfile",
83
+ "ModelRequest",
84
+ "ModelResponse",
85
+ "ModelStreamEvent",
86
+ "ModelStreamEventType",
87
+ "ModelSettings",
88
+ "ProviderState",
89
+ "RECOMMENDED_PROMPT_PREFIX",
90
+ "RunContext",
91
+ "Session",
92
+ "SessionItem",
93
+ "SessionSnapshot",
94
+ "ApprovalDecision",
95
+ "ApprovalDecisionType",
96
+ "ApprovalPolicy",
97
+ "ApprovalRequest",
98
+ "ObservabilityEvent",
99
+ "ObservabilityObserver",
100
+ "EvalCase",
101
+ "EvalResult",
102
+ "EvalRunner",
103
+ "Tool",
104
+ "ToolCall",
105
+ "ToolExecutor",
106
+ "ToolResult",
107
+ "Flow",
108
+ "FlowContinuation",
109
+ "FlowResult",
110
+ "FlowStreamEvent",
111
+ "FlowStreamEventType",
112
+ "FlowStateConflictError",
113
+ "FlowStateStatus",
114
+ "FlowStateStore",
115
+ "FlowSuspended",
116
+ "FlowSuspension",
117
+ "InMemoryFlowStateStore",
118
+ "PersistedFlowState",
119
+ "Handoff",
120
+ "Guardrail",
121
+ "GuardrailResult",
122
+ "InputGuardrail",
123
+ "OutputGuardrail",
124
+ "ToolInputGuardrail",
125
+ "ToolOutputGuardrail",
126
+ "Usage",
127
+ "ChainedVoiceSession",
128
+ "CallableSpeechToTextProvider",
129
+ "CallableTextToSpeechProvider",
130
+ "SpeechToTextProvider",
131
+ "LiveSpeechToTextProvider",
132
+ "LiveSpeechToTextSession",
133
+ "StreamingSpeechToTextProvider",
134
+ "StreamingTextToSpeechProvider",
135
+ "TextToSpeechProvider",
136
+ "Transcription",
137
+ "VoiceInputEvent",
138
+ "VoiceInputEventType",
139
+ "VoiceAgentSession",
140
+ "VoiceContinuation",
141
+ "VoiceSessionEvent",
142
+ "VoiceSessionEventType",
143
+ "VoiceTerminationReason",
144
+ "VoiceTurnOptions",
145
+ "VoiceTurnMetrics",
146
+ "VoiceTurnOutcome",
147
+ "VoiceTurnReason",
148
+ "VoiceTurnObserver",
149
+ "RealtimeEvent",
150
+ "RealtimeSession",
151
+ "RealtimeTransport",
152
+ "as_tool",
153
+ "prompt_with_handoff_instructions",
154
+ "tool",
155
+ ]
@@ -0,0 +1,16 @@
1
+ from modmex_ai.agents.agent import Agent
2
+ from modmex_ai.agents.context import RunContext
3
+ from modmex_ai.agents.handoff import Handoff, RECOMMENDED_PROMPT_PREFIX, prompt_with_handoff_instructions
4
+ from modmex_ai.agents.result import AgentResult
5
+ from modmex_ai.agents.stream import AgentStreamEvent, AgentStreamEventType
6
+
7
+ __all__ = [
8
+ "Agent",
9
+ "AgentResult",
10
+ "AgentStreamEvent",
11
+ "AgentStreamEventType",
12
+ "Handoff",
13
+ "RECOMMENDED_PROMPT_PREFIX",
14
+ "RunContext",
15
+ "prompt_with_handoff_instructions",
16
+ ]
@@ -0,0 +1,249 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ from typing import Any
6
+
7
+ from modmex_ai.agents.context import RunContext
8
+ from modmex_ai.agents.execution import AgentExecution
9
+ from modmex_ai.agents.handoff import Handoff, normalize_handoffs
10
+ from modmex_ai.agents.result import AgentResult
11
+ from modmex_ai.agents.stream import AgentStreamEvent, AgentStreamEventType
12
+ from modmex_ai.errors import OutputGuardrailTriggered, OutputValidationError
13
+ from modmex_ai.guardrails import (
14
+ InputGuardrail,
15
+ OutputGuardrail,
16
+ ToolInputGuardrail,
17
+ ToolOutputGuardrail,
18
+ enforce_input_guardrails,
19
+ enforce_output_guardrails,
20
+ )
21
+ from modmex_ai.messages import Message
22
+ from modmex_ai.models import (
23
+ ModelClient,
24
+ ModelResponse,
25
+ ModelSettings,
26
+ ModelStreamEvent,
27
+ ModelStreamEventType,
28
+ ProviderState,
29
+ Usage,
30
+ )
31
+ from modmex_ai.schemas import dumps, schema_for_model, validate_model
32
+ from modmex_ai.sessions import SessionItem
33
+ from modmex_ai.tools import Tool, as_tool, tool as create_tool
34
+
35
+
36
+ _NESTED_USAGE_STATE_KEY = "__modmex_ai_nested_usage"
37
+
38
+
39
+ class Agent:
40
+ def __init__(
41
+ self,
42
+ *,
43
+ name: str,
44
+ instructions: str,
45
+ output_type: type[Any] | None = None,
46
+ output_strict: bool = True,
47
+ tools: list[Tool | Any] | None = None,
48
+ handoffs: list[str | Handoff] | None = None,
49
+ model: ModelClient | None = None,
50
+ settings: ModelSettings | None = None,
51
+ max_tool_calls: int = 8,
52
+ input_guardrails: list[InputGuardrail] | None = None,
53
+ output_guardrails: list[OutputGuardrail] | None = None,
54
+ max_output_guardrail_retries: int = 0,
55
+ ) -> None:
56
+ self.name = name
57
+ self.instructions = instructions
58
+ self.output_type = output_type
59
+ self.output_strict = output_strict
60
+ self.tools = [as_tool(value) for value in (tools or [])]
61
+ self.handoffs = normalize_handoffs(handoffs)
62
+ self.model = model
63
+ self.settings = settings
64
+ self.max_tool_calls = max_tool_calls
65
+ self.input_guardrails = input_guardrails or []
66
+ self.output_guardrails = output_guardrails or []
67
+ if max_output_guardrail_retries < 0:
68
+ raise ValueError("max_output_guardrail_retries must be non-negative")
69
+ self.max_output_guardrail_retries = max_output_guardrail_retries
70
+
71
+ def add_tool(self, value: Tool | Any) -> Tool:
72
+ registered = as_tool(value)
73
+ self.tools.append(registered)
74
+ return registered
75
+
76
+ def tool(
77
+ self,
78
+ func: Any = None,
79
+ *,
80
+ name: str | None = None,
81
+ description: str | None = None,
82
+ input_guardrails: list[ToolInputGuardrail] | None = None,
83
+ output_guardrails: list[ToolOutputGuardrail] | None = None,
84
+ requires_approval: bool = False,
85
+ approval_reason: str | None = None,
86
+ ):
87
+ def decorator(inner):
88
+ return self.add_tool(create_tool(
89
+ inner,
90
+ name=name,
91
+ description=description,
92
+ input_guardrails=input_guardrails,
93
+ output_guardrails=output_guardrails,
94
+ requires_approval=requires_approval,
95
+ approval_reason=approval_reason,
96
+ ))
97
+ return decorator if func is None else decorator(func)
98
+
99
+ def as_tool(self, *, tool_name: str | None = None, tool_description: str | None = None) -> Tool:
100
+ def run_agent(input: str, ctx=None) -> Any:
101
+ result = self.run(input, context=ctx)
102
+ if isinstance(ctx, RunContext):
103
+ nested_usage = ctx.state.setdefault(_NESTED_USAGE_STATE_KEY, Usage())
104
+ nested_usage.add(result.usage)
105
+ return result.output
106
+ return create_tool(run_agent, name=tool_name or self.name, description=tool_description or self.instructions)
107
+
108
+ def run(self, input: Any, **kwargs: Any) -> AgentResult:
109
+ return self._run_execution(self._execution(input, **kwargs))
110
+
111
+ async def run_async(self, input: Any, **kwargs: Any) -> AgentResult:
112
+ execution = self._execution(input, **kwargs)
113
+ acomplete = getattr(execution.model, "acomplete", None)
114
+ if not callable(acomplete):
115
+ return await asyncio.to_thread(self._run_execution, execution)
116
+ while True:
117
+ step = execution.accept_response(await acomplete(execution.next_request()))
118
+ if step.result is not None:
119
+ return step.result
120
+ for tool_call in step.tool_calls or []:
121
+ execution.begin_tool_call(tool_call)
122
+ execution.accept_tool_result(await execution.executor.execute_async(tool_call, context=execution.context))
123
+
124
+ def run_stream(self, input: Any, **kwargs: Any):
125
+ execution = self._execution(input, **kwargs)
126
+ while True:
127
+ completed: ModelResponse | None = None
128
+ for raw_event in execution.model.stream(execution.next_request()):
129
+ event = self._model_stream_event(raw_event)
130
+ if event.type == ModelStreamEventType.TEXT_DELTA:
131
+ yield AgentStreamEvent(type=AgentStreamEventType.TEXT_DELTA, text_delta=event.text_delta or "")
132
+ elif event.type == ModelStreamEventType.TOOL_CALL_DELTA:
133
+ yield AgentStreamEvent(type=AgentStreamEventType.TOOL_CALL_DELTA, tool_call=event.tool_call)
134
+ elif event.type == ModelStreamEventType.COMPLETED:
135
+ completed = event.response
136
+ if completed is None:
137
+ raise RuntimeError("Model stream ended without a completed response")
138
+ step = execution.accept_response(completed)
139
+ if step.result is not None:
140
+ yield AgentStreamEvent(type=AgentStreamEventType.HANDOFF if step.result.handoff_target else AgentStreamEventType.COMPLETED, result=step.result)
141
+ return
142
+ for tool_call in step.tool_calls or []:
143
+ execution.begin_tool_call(tool_call)
144
+ yield AgentStreamEvent(type=AgentStreamEventType.TOOL_CALL_DELTA, tool_call=tool_call)
145
+ tool_result = execution.executor.execute(tool_call, context=execution.context)
146
+ execution.accept_tool_result(tool_result)
147
+ yield AgentStreamEvent(type=AgentStreamEventType.TOOL_FINISHED, tool_call=tool_call, data={"output": tool_result.output})
148
+
149
+ async def arun_stream(self, input: Any, **kwargs: Any):
150
+ """Async counterpart of ``run_stream`` for models with native streaming."""
151
+ execution = self._execution(input, **kwargs)
152
+ astream = getattr(execution.model, "astream", None)
153
+ if not callable(astream):
154
+ raise NotImplementedError("Model does not implement native async streaming")
155
+ while True:
156
+ completed: ModelResponse | None = None
157
+ async for raw_event in astream(execution.next_request()):
158
+ event = self._model_stream_event(raw_event)
159
+ if event.type == ModelStreamEventType.TEXT_DELTA:
160
+ yield AgentStreamEvent(type=AgentStreamEventType.TEXT_DELTA, text_delta=event.text_delta or "")
161
+ elif event.type == ModelStreamEventType.TOOL_CALL_DELTA:
162
+ yield AgentStreamEvent(type=AgentStreamEventType.TOOL_CALL_DELTA, tool_call=event.tool_call)
163
+ elif event.type == ModelStreamEventType.COMPLETED:
164
+ completed = event.response
165
+ if completed is None:
166
+ raise RuntimeError("Model stream ended without a completed response")
167
+ step = execution.accept_response(completed)
168
+ if step.result is not None:
169
+ yield AgentStreamEvent(type=AgentStreamEventType.HANDOFF if step.result.handoff_target else AgentStreamEventType.COMPLETED, result=step.result)
170
+ return
171
+ for tool_call in step.tool_calls or []:
172
+ execution.begin_tool_call(tool_call)
173
+ yield AgentStreamEvent(type=AgentStreamEventType.TOOL_CALL_DELTA, tool_call=tool_call)
174
+ tool_result = await execution.executor.execute_async(tool_call, context=execution.context)
175
+ execution.accept_tool_result(tool_result)
176
+ yield AgentStreamEvent(type=AgentStreamEventType.TOOL_FINISHED, tool_call=tool_call, data={"output": tool_result.output})
177
+
178
+ def _run_execution(self, execution: AgentExecution) -> AgentResult:
179
+ while True:
180
+ step = execution.accept_response(execution.model.complete(execution.next_request()))
181
+ if step.result is not None:
182
+ return step.result
183
+ for tool_call in step.tool_calls or []:
184
+ execution.begin_tool_call(tool_call)
185
+ execution.accept_tool_result(execution.executor.execute(tool_call, context=execution.context))
186
+
187
+ def _execution(
188
+ self,
189
+ input: Any,
190
+ *,
191
+ model: ModelClient | None = None,
192
+ context: RunContext | Any = None,
193
+ provider_state: ProviderState | None = None,
194
+ run_input_guardrails: bool = True,
195
+ run_output_guardrails: bool = True,
196
+ ) -> AgentExecution:
197
+ active_model = model or self.model
198
+ if active_model is None:
199
+ raise ValueError(f"Agent {self.name!r} has no model")
200
+ run_context = context if isinstance(context, RunContext) else RunContext(input=input, context=context)
201
+ if run_input_guardrails:
202
+ enforce_input_guardrails(input, self.input_guardrails, context=run_context)
203
+ return AgentExecution(agent=self, model=active_model, input=input, context=run_context, provider_state=provider_state, run_output_guardrails=run_output_guardrails)
204
+
205
+ @staticmethod
206
+ def _model_stream_event(value: ModelStreamEvent | ModelResponse) -> ModelStreamEvent:
207
+ if isinstance(value, ModelResponse):
208
+ return ModelStreamEvent.completed(value)
209
+ if isinstance(value, ModelStreamEvent):
210
+ return value
211
+ raise TypeError("Model stream must yield ModelStreamEvent or ModelResponse")
212
+
213
+ def _output_schema(self) -> dict[str, Any] | None:
214
+ return schema_for_model(self.output_type) if self.output_type else None
215
+
216
+ def _should_retry_output(self, *, output: Any, response_text: str | None, messages: list[Message | SessionItem], context: RunContext, retry_count: int) -> bool:
217
+ try:
218
+ enforce_output_guardrails(output, self.output_guardrails, context=context)
219
+ except OutputGuardrailTriggered as error:
220
+ if retry_count >= self.max_output_guardrail_retries:
221
+ raise
222
+ context.trace.add("guardrail", f"{self.name}_output_retry", attempt=retry_count + 1, reason=str(error))
223
+ messages.extend([
224
+ Message(role="assistant", content=response_text or ""),
225
+ Message(role="developer", content="The previous candidate output was rejected by an output guardrail: " f"{error}. Produce a corrected replacement that fully satisfies the output schema and the guardrail rule."),
226
+ ])
227
+ return True
228
+ return False
229
+
230
+ def _initial_messages(self, input: Any, *, include_output_schema_prompt: bool = True) -> list[Message | SessionItem]:
231
+ instructions = self.instructions
232
+ if self.output_type and include_output_schema_prompt:
233
+ instructions += "\n\nReturn only JSON that matches this schema:\n" + json.dumps(schema_for_model(self.output_type), separators=(",", ":"))
234
+ return [Message(role="system", content=instructions), *self._input_messages(input)]
235
+
236
+ def _input_messages(self, input: Any) -> list[Message | SessionItem]:
237
+ if isinstance(input, list) and all(isinstance(item, (Message, SessionItem, dict)) for item in input):
238
+ return [item if isinstance(item, (Message, SessionItem)) else SessionItem(**item) if item.get("type") else Message(**item) for item in input]
239
+ return [Message(role="user", content=input if isinstance(input, str) else dumps(input))]
240
+
241
+ def _parse_output(self, text: str | None) -> Any:
242
+ if self.output_type is None:
243
+ return text or ""
244
+ if text is None:
245
+ raise OutputValidationError(f"Agent {self.name!r} returned no output")
246
+ return validate_model(json.loads(text), self.output_type)
247
+
248
+ def _handoff_schemas(self) -> list[dict[str, Any]]:
249
+ return [handoff.schema() for handoff in self.handoffs]
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import field
4
+ from typing import Any
5
+
6
+ from modmex import BaseModel
7
+
8
+ from modmex_ai.tracing import Trace
9
+
10
+
11
+ class RunContext(BaseModel):
12
+ input: Any
13
+ context: Any = None
14
+ state: dict[str, Any] = field(default_factory=dict)
15
+ metadata: dict[str, Any] = field(default_factory=dict)
16
+ trace: Trace = field(default_factory=Trace)
@@ -0,0 +1,172 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from modmex_ai.agents.context import RunContext
7
+ from modmex_ai.agents.handoff import find_handoff
8
+ from modmex_ai.agents.result import AgentResult
9
+ from modmex_ai.errors import MaxToolCallsExceeded
10
+ from modmex_ai.messages import Message
11
+ from modmex_ai.models import ModelClient, ModelRequest, ModelResponse, ProviderState, ToolCall, Usage
12
+ from modmex_ai.sessions import SessionItem
13
+ from modmex_ai.tools import ToolExecutor, ToolResult
14
+
15
+ if TYPE_CHECKING:
16
+ from modmex_ai.agents.agent import Agent
17
+
18
+
19
+ @dataclass
20
+ class AgentExecutionStep:
21
+ """The next action requested by the provider-neutral agent state machine."""
22
+
23
+ result: AgentResult | None = None
24
+ tool_calls: list[ToolCall] | None = None
25
+
26
+
27
+ class AgentExecution:
28
+ """Shared Agent state machine; sync and async drivers supply I/O only."""
29
+
30
+ def __init__(
31
+ self,
32
+ *,
33
+ agent: Agent,
34
+ model: ModelClient,
35
+ input: Any,
36
+ context: RunContext,
37
+ provider_state: ProviderState | None,
38
+ run_output_guardrails: bool,
39
+ ) -> None:
40
+ self.agent = agent
41
+ self.model = model
42
+ self.context = context
43
+ self.run_output_guardrails = run_output_guardrails
44
+ self.messages = agent._initial_messages(
45
+ input,
46
+ include_output_schema_prompt=not self.model.profile.supports_structured_output,
47
+ )
48
+ self.executor = ToolExecutor(agent.tools)
49
+ self.tool_schemas = [*self.executor.schemas(), *agent._handoff_schemas()]
50
+ self.output_schema = agent._output_schema()
51
+ self.tool_call_count = 0
52
+ self.output_guardrail_retry_count = 0
53
+ self.usage = Usage()
54
+ self.generated_items: list[SessionItem] = []
55
+ self.provider_state = provider_state
56
+
57
+ def next_request(self) -> ModelRequest:
58
+ self.context.trace.add(
59
+ "model_request",
60
+ self.agent.name,
61
+ tools=[tool["name"] for tool in self.tool_schemas],
62
+ )
63
+ return ModelRequest(
64
+ messages=self.messages,
65
+ tools=self.tool_schemas,
66
+ output_schema=self.output_schema,
67
+ output_strict=self.agent.output_strict,
68
+ settings=self.agent.settings,
69
+ provider_state=self.provider_state,
70
+ )
71
+
72
+ def accept_response(self, response: ModelResponse) -> AgentExecutionStep:
73
+ if response.provider_state is not None:
74
+ self.provider_state = response.provider_state
75
+ self.usage.add(response.usage)
76
+ self.context.trace.add(
77
+ "model_response",
78
+ self.agent.name,
79
+ request_id=response.request_id,
80
+ tool_calls=[call.name for call in response.tool_calls],
81
+ usage=response.usage,
82
+ )
83
+ if response.tool_calls:
84
+ return self._tool_step(response.tool_calls)
85
+
86
+ output = self.agent._parse_output(response.output_text)
87
+ if self.run_output_guardrails and self.agent._should_retry_output(
88
+ output=output,
89
+ response_text=response.output_text,
90
+ messages=self.messages,
91
+ context=self.context,
92
+ retry_count=self.output_guardrail_retry_count,
93
+ ):
94
+ self.output_guardrail_retry_count += 1
95
+ return AgentExecutionStep()
96
+ return AgentExecutionStep(result=self._result(output=output))
97
+
98
+ def begin_tool_call(self, tool_call: ToolCall) -> None:
99
+ item = SessionItem(
100
+ type="function_call",
101
+ tool_call_id=tool_call.tool_call_id,
102
+ name=tool_call.name,
103
+ arguments=tool_call.arguments,
104
+ )
105
+ self.generated_items.append(item)
106
+ self.messages.append(item)
107
+
108
+ def accept_tool_result(self, result: ToolResult) -> None:
109
+ item = SessionItem(
110
+ type="function_call_output",
111
+ tool_call_id=result.tool_call_id,
112
+ name=result.name,
113
+ output=result.output,
114
+ )
115
+ self.generated_items.append(item)
116
+ nested_usage = self.context.state.pop("__modmex_ai_nested_usage", None)
117
+ if nested_usage:
118
+ self.usage.add(nested_usage)
119
+ self.context.trace.add("tool_call", result.name, tool_call_id=result.tool_call_id)
120
+ self.messages.append(item)
121
+
122
+ def _tool_step(self, tool_calls: list[ToolCall]) -> AgentExecutionStep:
123
+ self.tool_call_count += len(tool_calls)
124
+ if self.tool_call_count > self.agent.max_tool_calls:
125
+ raise MaxToolCallsExceeded(f"Agent {self.agent.name!r} exceeded max_tool_calls")
126
+ for tool_call in tool_calls:
127
+ handoff = find_handoff(tool_call, self.agent.handoffs)
128
+ if handoff is None:
129
+ continue
130
+ handoff_input = handoff.invoke(self.context, tool_call.arguments)
131
+ self.generated_items.extend([
132
+ SessionItem(
133
+ type="handoff_call",
134
+ tool_call_id=tool_call.tool_call_id,
135
+ name=handoff.name,
136
+ arguments=tool_call.arguments,
137
+ ),
138
+ SessionItem(
139
+ type="handoff_call_output",
140
+ tool_call_id=tool_call.tool_call_id,
141
+ name=handoff.name,
142
+ output={"transferred": True},
143
+ ),
144
+ ])
145
+ self.context.trace.add(
146
+ "handoff_call",
147
+ handoff.name,
148
+ tool_call_id=tool_call.tool_call_id,
149
+ to=handoff.agent,
150
+ )
151
+ return AgentExecutionStep(result=AgentResult(
152
+ output=None,
153
+ agent=self.agent.name,
154
+ handoff_target=handoff.agent,
155
+ handoff_name=handoff.name,
156
+ handoff_input=handoff_input,
157
+ usage=self.usage,
158
+ items=self.generated_items,
159
+ provider_state=self.provider_state,
160
+ trace=self.context.trace,
161
+ ))
162
+ return AgentExecutionStep(tool_calls=tool_calls)
163
+
164
+ def _result(self, *, output: Any) -> AgentResult:
165
+ return AgentResult(
166
+ output=output,
167
+ agent=self.agent.name,
168
+ usage=self.usage,
169
+ items=self.generated_items,
170
+ trace=self.context.trace,
171
+ provider_state=self.provider_state,
172
+ )