universal-mcp-agents 0.1.19rc1__py3-none-any.whl → 0.1.24rc3__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 (57) hide show
  1. universal_mcp/agents/__init__.py +15 -16
  2. universal_mcp/agents/base.py +46 -35
  3. universal_mcp/agents/bigtool/state.py +1 -1
  4. universal_mcp/agents/cli.py +2 -5
  5. universal_mcp/agents/codeact0/__init__.py +2 -3
  6. universal_mcp/agents/codeact0/__main__.py +4 -7
  7. universal_mcp/agents/codeact0/agent.py +444 -96
  8. universal_mcp/agents/codeact0/langgraph_agent.py +1 -1
  9. universal_mcp/agents/codeact0/llm_tool.py +2 -254
  10. universal_mcp/agents/codeact0/prompts.py +247 -137
  11. universal_mcp/agents/codeact0/sandbox.py +52 -18
  12. universal_mcp/agents/codeact0/state.py +26 -6
  13. universal_mcp/agents/codeact0/tools.py +400 -74
  14. universal_mcp/agents/codeact0/utils.py +175 -11
  15. universal_mcp/agents/codeact00/__init__.py +3 -0
  16. universal_mcp/agents/{unified → codeact00}/__main__.py +4 -6
  17. universal_mcp/agents/codeact00/agent.py +578 -0
  18. universal_mcp/agents/codeact00/config.py +77 -0
  19. universal_mcp/agents/{unified → codeact00}/langgraph_agent.py +2 -2
  20. universal_mcp/agents/{unified → codeact00}/llm_tool.py +1 -1
  21. universal_mcp/agents/codeact00/prompts.py +364 -0
  22. universal_mcp/agents/{unified → codeact00}/sandbox.py +52 -18
  23. universal_mcp/agents/codeact00/state.py +66 -0
  24. universal_mcp/agents/codeact00/tools.py +525 -0
  25. universal_mcp/agents/codeact00/utils.py +678 -0
  26. universal_mcp/agents/codeact01/__init__.py +3 -0
  27. universal_mcp/agents/{codeact → codeact01}/__main__.py +4 -11
  28. universal_mcp/agents/codeact01/agent.py +413 -0
  29. universal_mcp/agents/codeact01/config.py +77 -0
  30. universal_mcp/agents/codeact01/langgraph_agent.py +14 -0
  31. universal_mcp/agents/codeact01/llm_tool.py +25 -0
  32. universal_mcp/agents/codeact01/prompts.py +246 -0
  33. universal_mcp/agents/codeact01/sandbox.py +162 -0
  34. universal_mcp/agents/{unified → codeact01}/state.py +26 -10
  35. universal_mcp/agents/codeact01/tools.py +648 -0
  36. universal_mcp/agents/{unified → codeact01}/utils.py +175 -11
  37. universal_mcp/agents/llm.py +14 -4
  38. universal_mcp/agents/react.py +3 -3
  39. universal_mcp/agents/sandbox.py +124 -69
  40. universal_mcp/applications/llm/app.py +76 -24
  41. {universal_mcp_agents-0.1.19rc1.dist-info → universal_mcp_agents-0.1.24rc3.dist-info}/METADATA +6 -5
  42. universal_mcp_agents-0.1.24rc3.dist-info/RECORD +66 -0
  43. universal_mcp/agents/codeact/__init__.py +0 -3
  44. universal_mcp/agents/codeact/agent.py +0 -240
  45. universal_mcp/agents/codeact/models.py +0 -11
  46. universal_mcp/agents/codeact/prompts.py +0 -82
  47. universal_mcp/agents/codeact/sandbox.py +0 -85
  48. universal_mcp/agents/codeact/state.py +0 -11
  49. universal_mcp/agents/codeact/utils.py +0 -68
  50. universal_mcp/agents/codeact0/playbook_agent.py +0 -355
  51. universal_mcp/agents/unified/README.md +0 -45
  52. universal_mcp/agents/unified/__init__.py +0 -3
  53. universal_mcp/agents/unified/agent.py +0 -289
  54. universal_mcp/agents/unified/prompts.py +0 -192
  55. universal_mcp/agents/unified/tools.py +0 -188
  56. universal_mcp_agents-0.1.19rc1.dist-info/RECORD +0 -64
  57. {universal_mcp_agents-0.1.19rc1.dist-info → universal_mcp_agents-0.1.24rc3.dist-info}/WHEEL +0 -0
@@ -1,37 +1,46 @@
1
- import inspect
2
- from collections.abc import Callable
1
+ import copy
2
+ import json
3
+ import re
4
+ import uuid
3
5
  from typing import Literal, cast
4
6
 
5
- from langchain_core.messages import AIMessage, ToolMessage
6
- from langchain_core.tools import StructuredTool
7
- from langchain_core.tools import tool as create_tool
7
+ from langchain_anthropic import ChatAnthropic
8
+ from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
8
9
  from langgraph.checkpoint.base import BaseCheckpointSaver
9
10
  from langgraph.graph import START, StateGraph
10
- from langgraph.types import Command, RetryPolicy
11
+ from langgraph.types import Command, RetryPolicy, StreamWriter
11
12
  from universal_mcp.tools.registry import ToolRegistry
12
- from universal_mcp.types import ToolConfig, ToolFormat
13
+ from universal_mcp.types import ToolFormat
13
14
 
14
15
  from universal_mcp.agents.base import BaseAgent
15
- from universal_mcp.agents.codeact0.llm_tool import ai_classify, call_llm, data_extractor, smart_print
16
+ from universal_mcp.agents.codeact0.llm_tool import smart_print
16
17
  from universal_mcp.agents.codeact0.prompts import (
18
+ AGENT_BUILDER_GENERATING_PROMPT,
19
+ AGENT_BUILDER_META_PROMPT,
20
+ AGENT_BUILDER_PLANNING_PROMPT,
21
+ build_tool_definitions,
17
22
  create_default_prompt,
18
23
  )
19
- from universal_mcp.agents.codeact0.sandbox import eval_unsafe, execute_ipython_cell
20
- from universal_mcp.agents.codeact0.state import CodeActState
21
- from universal_mcp.agents.codeact0.utils import inject_context, smart_truncate
24
+ from universal_mcp.agents.codeact0.sandbox import eval_unsafe, execute_ipython_cell, handle_execute_ipython_cell
25
+ from universal_mcp.agents.codeact0.state import AgentBuilderCode, AgentBuilderMeta, AgentBuilderPlan, CodeActState
26
+ from universal_mcp.agents.codeact0.tools import (
27
+ create_meta_tools,
28
+ enter_agent_builder_mode,
29
+ )
30
+ from universal_mcp.agents.codeact0.utils import build_anthropic_cache_message, get_connected_apps_string, strip_thinking, extract_plan_parameters
22
31
  from universal_mcp.agents.llm import load_chat_model
23
- from universal_mcp.agents.utils import filter_retry_on
32
+ from universal_mcp.agents.utils import convert_tool_ids_to_dict, filter_retry_on, get_message_text
24
33
 
25
34
 
26
- class CodeActAgent(BaseAgent):
35
+ class CodeActPlaybookAgent(BaseAgent):
27
36
  def __init__(
28
37
  self,
29
38
  name: str,
30
39
  instructions: str,
31
40
  model: str,
32
41
  memory: BaseCheckpointSaver | None = None,
33
- tools: ToolConfig | None = None,
34
42
  registry: ToolRegistry | None = None,
43
+ agent_builder_registry: object | None = None,
35
44
  sandbox_timeout: int = 20,
36
45
  **kwargs,
37
46
  ):
@@ -42,103 +51,442 @@ class CodeActAgent(BaseAgent):
42
51
  memory=memory,
43
52
  **kwargs,
44
53
  )
45
- self.model_instance = load_chat_model(model, thinking=True)
46
- self.tools_config = tools or {}
54
+ self.model_instance = load_chat_model(model)
55
+ self.agent_builder_model_instance = load_chat_model("anthropic:claude-sonnet-4-5-20250929", thinking=False)
47
56
  self.registry = registry
57
+ self.agent_builder_registry = agent_builder_registry
58
+ self.agent = agent_builder_registry.get_agent() if agent_builder_registry else None
59
+
60
+ self.tools_config = self.agent.tools if self.agent else {}
48
61
  self.eval_fn = eval_unsafe
49
62
  self.sandbox_timeout = sandbox_timeout
50
- self.processed_tools: list[StructuredTool | Callable] = []
51
-
52
- # TODO(manoj): Use toolformat native instead of langchain
53
- # TODO(manoj, later): Add better sandboxing
54
- # Old Nishant TODO s:
55
- # - Make codeact faster by calling upto API call (this done but should be tested)
56
- # - Add support for async eval_fn
57
- # - Throw Error if code snippet is too long (> 1000 characters) and suggest to split it into smaller parts
58
- # - Multiple models from config
59
-
60
- async def _build_graph(self):
61
- exported_tools = []
63
+ self.default_tools_config = {
64
+ "llm": ["generate_text", "classify_data", "extract_data", "call_llm"],
65
+ }
66
+ self.final_instructions = ""
67
+ self.tools_context = {}
68
+ self.eval_mode = kwargs.get("eval_mode", False)
69
+
70
+ async def _build_graph(self): # noqa: PLR0915
71
+ """Build the graph for the CodeAct Playbook Agent."""
72
+ meta_tools = create_meta_tools(self.registry)
73
+ self.additional_tools = [
74
+ smart_print,
75
+ meta_tools["web_search"],
76
+ meta_tools["read_file"],
77
+ meta_tools["save_file"],
78
+ meta_tools["upload_file"],
79
+ ]
80
+
62
81
  if self.tools_config:
63
- if not self.registry:
64
- raise ValueError("Tools are configured but no registry is provided")
65
- # Langchain tools are fine
66
- exported_tools = await self.registry.export_tools(self.tools_config, ToolFormat.LANGCHAIN)
67
- additional_tools = [smart_print, data_extractor, ai_classify, call_llm]
68
- additional_tools = [t if isinstance(t, StructuredTool) else create_tool(t) for t in additional_tools]
69
- self.instructions, self.tools_context = create_default_prompt(
70
- exported_tools, additional_tools, self.instructions
71
- )
82
+ await self.registry.load_tools(self.tools_config) # Load provided tools
83
+ if self.default_tools_config:
84
+ await self.registry.load_tools(self.default_tools_config) # Load default tools
72
85
 
73
- def call_model(state: CodeActState) -> Command[Literal["sandbox"]]:
74
- messages = [{"role": "system", "content": self.instructions}] + state["messages"]
86
+ async def call_model(state: CodeActState) -> Command[Literal["execute_tools"]]:
87
+ """This node now only ever binds the four meta-tools to the LLM."""
88
+ messages = build_anthropic_cache_message(self.final_instructions) + state["messages"]
75
89
 
76
- # Run the model and potentially loop for reflection
77
- model_with_tools = self.model_instance.bind_tools(tools=[execute_ipython_cell], tool_choice="auto")
78
- response = cast(AIMessage, model_with_tools.invoke(messages))
90
+ agent_facing_tools = [
91
+ execute_ipython_cell,
92
+ enter_agent_builder_mode,
93
+ meta_tools["search_functions"],
94
+ meta_tools["load_functions"],
95
+ ]
79
96
 
97
+ if isinstance(self.model_instance, ChatAnthropic):
98
+ model_with_tools = self.model_instance.bind_tools(
99
+ tools=agent_facing_tools,
100
+ tool_choice="auto",
101
+ cache_control={"type": "ephemeral", "ttl": "1h"},
102
+ )
103
+ if isinstance(messages[-1].content, str):
104
+ pass
105
+ else:
106
+ last = copy.deepcopy(messages[-1])
107
+ last.content[-1]["cache_control"] = {"type": "ephemeral", "ttl": "5m"}
108
+ messages[-1] = last
109
+ else:
110
+ model_with_tools = self.model_instance.bind_tools(
111
+ tools=agent_facing_tools,
112
+ tool_choice="auto",
113
+ )
114
+ response = cast(AIMessage, await model_with_tools.ainvoke(messages))
80
115
  if response.tool_calls:
81
- if len(response.tool_calls) > 1:
82
- raise Exception("Not possible in Claude with llm.bind_tools(tools=tools, tool_choice='auto')")
83
- if response.tool_calls[0]["name"] != "execute_ipython_cell":
84
- raise Exception(
85
- f"Unexpected tool call: {response.tool_calls[0]['name']}. Expected 'execute_ipython_cell'."
86
- )
87
- if (
88
- response.tool_calls[0]["args"].get("snippet") is None
89
- or not response.tool_calls[0]["args"]["snippet"].strip()
90
- ):
91
- raise Exception("Tool call 'execute_ipython_cell' requires a non-empty 'snippet' argument.")
92
- return Command(goto="sandbox", update={"messages": [response]})
116
+ return Command(goto="execute_tools", update={"messages": [response]})
93
117
  else:
94
- return Command(update={"messages": [response]})
95
-
96
- # If eval_fn is a async, we define async node function.
97
- if inspect.iscoroutinefunction(self.eval_fn):
98
- raise ValueError("eval_fn must be a synchronous function, not a coroutine.")
99
- # async def sandbox(state: StateSchema):
100
- # existing_context = state.get("context", {})
101
- # context = {**existing_context, **tools_context}
102
- # # Execute the script in the sandbox
103
- # output, new_vars = await eval_fn(state["script"], context)
104
- # new_context = {**existing_context, **new_vars}
105
- # return {
106
- # "messages": [{"role": "user", "content": output}],
107
- # "context": new_context,
108
- # }
109
- else:
110
-
111
- def sandbox(state: CodeActState) -> Command[Literal["call_model"]]:
112
- tool_call = state["messages"][-1].tool_calls[0] # type: ignore
113
- code = tool_call["args"]["snippet"]
114
- previous_add_context = state.get("add_context", {})
115
- add_context = inject_context(previous_add_context, self.tools_context)
116
- existing_context = state.get("context", {})
117
- context = {**existing_context, **add_context}
118
- # Execute the script in the sandbox
119
-
120
- output, new_context, new_add_context = self.eval_fn(
121
- code, context, previous_add_context, 180
122
- ) # default timeout 3 min
123
- output = smart_truncate(output)
118
+ return Command(update={"messages": [response], "model_with_tools": model_with_tools})
119
+
120
+ async def execute_tools(state: CodeActState) -> Command[Literal["call_model", "agent_builder"]]:
121
+ """Execute tool calls"""
122
+ last_message = state["messages"][-1]
123
+ tool_calls = last_message.tool_calls if isinstance(last_message, AIMessage) else []
124
+
125
+ tool_messages = []
126
+ new_tool_ids = []
127
+ tool_result = ""
128
+ ask_user = False
129
+ ai_msg = ""
130
+ effective_previous_add_context = state.get("add_context", {})
131
+ effective_existing_context = state.get("context", {})
132
+ # logging.info(f"Initial new_tool_ids_for_context: {new_tool_ids_for_context}")
133
+
134
+ for tool_call in tool_calls:
135
+ tool_name = tool_call["name"]
136
+ tool_args = tool_call["args"]
137
+ try:
138
+ if tool_name == "enter_agent_builder_mode":
139
+ tool_message = ToolMessage(
140
+ content=json.dumps("Entered Agent Builder Mode."),
141
+ name=tool_call["name"],
142
+ tool_call_id=tool_call["id"],
143
+ )
144
+ return Command(
145
+ goto="agent_builder",
146
+ update={
147
+ "agent_builder_mode": "planning",
148
+ "messages": [tool_message],
149
+ }, # Entered Agent Builder mode
150
+ )
151
+ elif tool_name == "execute_ipython_cell":
152
+ code = tool_call["args"]["snippet"]
153
+ output, new_context, new_add_context = await handle_execute_ipython_cell(
154
+ code,
155
+ self.tools_context, # Uses the dynamically updated context
156
+ self.eval_fn,
157
+ effective_previous_add_context,
158
+ effective_existing_context,
159
+ )
160
+ effective_existing_context = new_context
161
+ effective_previous_add_context = new_add_context
162
+ tool_result = output
163
+ elif tool_name == "load_functions":
164
+ # The tool now does all the work of validation and formatting.
165
+ tool_result, new_context_for_sandbox, valid_tools, unconnected_links = await meta_tools[
166
+ "load_functions"
167
+ ].ainvoke(tool_args)
168
+ # We still need to update the sandbox context for `execute_ipython_cell`
169
+ new_tool_ids.extend(valid_tools)
170
+ if new_tool_ids:
171
+ self.tools_context.update(new_context_for_sandbox)
172
+ if unconnected_links:
173
+ ask_user = True
174
+ ai_msg = f"Please login to the following app(s) using the following links and let me know in order to proceed:\n {unconnected_links} "
175
+
176
+ elif tool_name == "search_functions":
177
+ tool_result = await meta_tools["search_functions"].ainvoke(tool_args)
178
+ else:
179
+ raise Exception(
180
+ f"Unexpected tool call: {tool_call['name']}. "
181
+ "tool calls must be one of 'enter_agent_builder_mode', 'execute_ipython_cell', 'load_functions', or 'search_functions'. For using functions, call them in code using 'execute_ipython_cell'."
182
+ )
183
+ except Exception as e:
184
+ tool_result = str(e)
185
+
186
+ tool_message = ToolMessage(
187
+ content=json.dumps(tool_result),
188
+ name=tool_call["name"],
189
+ tool_call_id=tool_call["id"],
190
+ )
191
+ tool_messages.append(tool_message)
192
+
193
+ if ask_user:
194
+ tool_messages.append(AIMessage(content=ai_msg))
195
+ return Command(
196
+ update={
197
+ "messages": tool_messages,
198
+ "selected_tool_ids": new_tool_ids,
199
+ "context": effective_existing_context,
200
+ "add_context": effective_previous_add_context,
201
+ }
202
+ )
203
+
204
+ return Command(
205
+ goto="call_model",
206
+ update={
207
+ "messages": tool_messages,
208
+ "selected_tool_ids": new_tool_ids,
209
+ "context": effective_existing_context,
210
+ "add_context": effective_previous_add_context,
211
+ },
212
+ )
213
+
214
+ async def agent_builder(state: CodeActState, writer: StreamWriter) -> Command[Literal["call_model"]]:
215
+ agent_builder_mode = state.get("agent_builder_mode")
216
+ if agent_builder_mode == "planning":
217
+ plan_id = str(uuid.uuid4())
218
+ writer({"type": "custom", id: plan_id, "name": "planning", "data": {"update": bool(self.agent)}})
219
+ planning_instructions = self.instructions + AGENT_BUILDER_PLANNING_PROMPT + self.preloaded_defs
220
+ messages = [{"role": "system", "content": planning_instructions}] + strip_thinking(state["messages"])
221
+
222
+ model_with_structured_output = self.agent_builder_model_instance.with_structured_output(
223
+ AgentBuilderPlan
224
+ )
225
+ response = await model_with_structured_output.ainvoke(messages)
226
+ plan = cast(AgentBuilderPlan, response)
227
+
228
+ writer({"type": "custom", id: plan_id, "name": "planning", "data": {"plan": plan.steps}})
229
+ ai_msg = AIMessage(
230
+ content=json.dumps(plan.model_dump()),
231
+ additional_kwargs={
232
+ "type": "planning",
233
+ "plan": plan.steps,
234
+ "update": bool(self.agent),
235
+ },
236
+ )
237
+
238
+ if self.eval_mode:
239
+ mock_user_message = HumanMessage(content="yes, this is great")
240
+ return Command(
241
+ goto="agent_builder",
242
+ update={
243
+ "messages": [ai_msg, mock_user_message],
244
+ "agent_builder_mode": "generating",
245
+ "plan": plan.steps,
246
+ },
247
+ )
124
248
 
125
249
  return Command(
126
- goto="call_model",
127
250
  update={
128
- "messages": [
129
- ToolMessage(
130
- content=output,
131
- name=tool_call["name"],
132
- tool_call_id=tool_call["id"],
133
- )
134
- ],
135
- "context": new_context,
136
- "add_context": new_add_context,
251
+ "messages": [ai_msg],
252
+ "agent_builder_mode": "confirming",
253
+ "plan": plan.steps,
254
+ }
255
+ )
256
+
257
+ elif agent_builder_mode == "confirming":
258
+ # Deterministic routing based on three exact button inputs from UI
259
+ user_text = ""
260
+ for m in reversed(state["messages"]):
261
+ try:
262
+ if getattr(m, "type", "") in {"human", "user"}:
263
+ user_text = (get_message_text(m) or "").strip()
264
+ if user_text:
265
+ break
266
+ except Exception:
267
+ continue
268
+
269
+ t = user_text.lower()
270
+ if t == "yes, this is great":
271
+ self.meta_id = str(uuid.uuid4())
272
+ name, description = None, None
273
+ if self.agent:
274
+ # Update flow: use existing name/description and do not re-generate
275
+ name = getattr(self.agent, "name", None)
276
+ description = getattr(self.agent, "description", None)
277
+ writer(
278
+ {
279
+ "type": "custom",
280
+ id: self.meta_id,
281
+ "name": "generating",
282
+ "data": {
283
+ "update": True,
284
+ "name": name,
285
+ "description": description,
286
+ },
287
+ }
288
+ )
289
+ else:
290
+ writer({"type": "custom", id: self.meta_id, "name": "generating", "data": {"update": False}})
291
+
292
+ meta_instructions = self.instructions + AGENT_BUILDER_META_PROMPT
293
+ messages = [{"role": "system", "content": meta_instructions}] + state["messages"]
294
+
295
+ model_with_structured_output = self.agent_builder_model_instance.with_structured_output(
296
+ AgentBuilderMeta
297
+ )
298
+ meta_response = await model_with_structured_output.ainvoke(messages)
299
+ meta = cast(AgentBuilderMeta, meta_response)
300
+ name, description = meta.name, meta.description
301
+
302
+ # Emit intermediary UI update with created name/description
303
+ writer(
304
+ {
305
+ "type": "custom",
306
+ id: self.meta_id,
307
+ "name": "generating",
308
+ "data": {"update": False, "name": name, "description": description},
309
+ }
310
+ )
311
+
312
+ return Command(
313
+ goto="agent_builder",
314
+ update={
315
+ "agent_builder_mode": "generating",
316
+ "agent_name": name,
317
+ "agent_description": description,
318
+ },
319
+ )
320
+ if t == "i would like to modify the plan":
321
+ prompt_ai = AIMessage(
322
+ content="What would you like to change about the plan? Let me know and I'll update the plan accordingly.",
323
+ additional_kwargs={"stream": "true"},
324
+ )
325
+ return Command(update={"agent_builder_mode": "planning", "messages": [prompt_ai]})
326
+ if t == "let's do something else":
327
+ return Command(goto="call_model", update={"agent_builder_mode": "inactive"})
328
+
329
+ # Fallback safe default
330
+ return Command(goto="call_model", update={"agent_builder_mode": "inactive"})
331
+
332
+ elif agent_builder_mode == "generating":
333
+ generating_instructions = self.instructions + AGENT_BUILDER_GENERATING_PROMPT + self.preloaded_defs
334
+ messages = [{"role": "system", "content": generating_instructions}] + state["messages"]
335
+
336
+ model_with_structured_output = self.agent_builder_model_instance.with_structured_output(
337
+ AgentBuilderCode
338
+ )
339
+ response = await model_with_structured_output.ainvoke(messages)
340
+ func_code = cast(AgentBuilderCode, response).code
341
+
342
+ # Extract function name (handle both regular and async functions)
343
+ match = re.search(r"^\s*(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", func_code, re.MULTILINE)
344
+ if match:
345
+ function_name = match.group(1)
346
+ else:
347
+ function_name = "generated_agent"
348
+
349
+ # Use generated metadata if available
350
+ final_name = state.get("agent_name") or function_name
351
+ final_description = state.get("agent_description") or f"Generated agent: {function_name}"
352
+ add_context = state.get("add_context", {})
353
+ if "functions" not in add_context:
354
+ add_context["functions"] = []
355
+ add_context["functions"].append(func_code)
356
+
357
+ # Save or update an Agent using the helper registry
358
+ try:
359
+ if not self.agent_builder_registry:
360
+ raise ValueError("AgentBuilder registry is not configured")
361
+
362
+ plan_params = extract_plan_parameters(state["plan"])
363
+
364
+ # Build instructions payload embedding the plan and function code
365
+ instructions_payload = {
366
+ "plan": state["plan"],
367
+ "script": func_code,
368
+ "params": plan_params,
369
+ }
370
+
371
+ # Convert tool ids list to dict
372
+ tool_dict = convert_tool_ids_to_dict(state["selected_tool_ids"])
373
+
374
+ res = self.agent_builder_registry.upsert_agent(
375
+ name=final_name,
376
+ description=final_description,
377
+ instructions=instructions_payload,
378
+ tools=tool_dict,
379
+ )
380
+ except Exception:
381
+ # In case of error, add the code to the exit message content
382
+
383
+ mock_exit_tool_call = {"name": "exit_agent_builder_mode", "args": {}, "id": "exit_builder_1"}
384
+
385
+ # Create a minimal assistant message to maintain flow
386
+ mock_assistant_message = AIMessage(
387
+ content=json.dumps(response.model_dump()),
388
+ tool_calls=[mock_exit_tool_call],
389
+ additional_kwargs={
390
+ "type": "generating",
391
+ "id": "ignore",
392
+ "update": bool(self.agent),
393
+ "name": final_name.replace(" ", "_"),
394
+ "description": final_description,
395
+ },
396
+ )
397
+ mock_exit_tool_response = ToolMessage(
398
+ content=json.dumps(
399
+ f"An error occurred. Displaying the function code:\n\n{func_code}\nFinal Name: {final_name}\nDescription: {final_description}"
400
+ ),
401
+ name="exit_agent_builder_mode",
402
+ tool_call_id="exit_builder_1",
403
+ )
404
+ if self.eval_mode:
405
+ human_msg = HumanMessage(content="Call the generated agent function (without redeclaring it) and check whether it works as expected")
406
+ return Command(goto="call_model", update={"messages": [mock_assistant_message, mock_exit_tool_response, human_msg], "agent_builder_mode": "normal", "add_context": add_context})
407
+ else:
408
+ return Command(update={"messages": [mock_assistant_message, mock_exit_tool_response], "agent_builder_mode": "normal", "add_context": add_context})
409
+
410
+ writer(
411
+ {
412
+ "type": "custom",
413
+ id: self.meta_id,
414
+ "name": "generating",
415
+ "data": {
416
+ "id": str(res.id),
417
+ "update": bool(self.agent),
418
+ "name": final_name,
419
+ "description": final_description,
420
+ "add_context": add_context,
421
+ },
422
+ }
423
+ )
424
+ mock_exit_tool_call = {"name": "exit_agent_builder_mode", "args": {}, "id": "exit_builder_1"}
425
+ mock_assistant_message = AIMessage(
426
+ content=json.dumps(response.model_dump()),
427
+ tool_calls=[mock_exit_tool_call],
428
+ additional_kwargs={
429
+ "type": "generating",
430
+ "id": str(res.id),
431
+ "update": bool(self.agent),
432
+ "name": final_name.replace(" ", "_"),
433
+ "description": final_description,
137
434
  },
138
435
  )
139
436
 
437
+ mock_exit_tool_response = ToolMessage(
438
+ content=json.dumps(
439
+ "Exited Agent Builder Mode. Enter this mode again if you need to modify the saved agent."
440
+ ),
441
+ name="exit_agent_builder_mode",
442
+ tool_call_id="exit_builder_1",
443
+ )
444
+
445
+ return Command(
446
+ update={
447
+ "messages": [mock_assistant_message, mock_exit_tool_response],
448
+ "agent_builder_mode": "normal",
449
+ "add_context": add_context,
450
+ }
451
+ )
452
+
453
+ async def route_entry(state: CodeActState) -> Command[Literal["call_model", "agent_builder", "execute_tools"]]:
454
+ """Route to either normal mode or agent builder creation"""
455
+ pre_tools = await self.registry.export_tools(format=ToolFormat.NATIVE)
456
+
457
+ # Create the initial system prompt and tools_context in one go
458
+ self.final_instructions, self.tools_context = create_default_prompt(
459
+ pre_tools,
460
+ self.additional_tools,
461
+ self.instructions,
462
+ await get_connected_apps_string(self.registry),
463
+ self.agent,
464
+ is_initial_prompt=True,
465
+ )
466
+ self.preloaded_defs, _ = build_tool_definitions(pre_tools)
467
+ self.preloaded_defs = "\n".join(self.preloaded_defs)
468
+ await self.registry.load_tools(state["selected_tool_ids"])
469
+ exported_tools = await self.registry.export_tools(
470
+ state["selected_tool_ids"], ToolFormat.NATIVE
471
+ ) # Get definition for only the new tools
472
+ _, loaded_tools_context = build_tool_definitions(exported_tools)
473
+ self.tools_context.update(loaded_tools_context)
474
+
475
+ if (
476
+ len(state["messages"]) == 1 and self.agent
477
+ ): # Inject the agent's script function into add_context for execution
478
+ script = self.agent.instructions.get("script")
479
+ add_context = {"functions": [script]}
480
+ return Command(goto="call_model", update={"add_context": add_context})
481
+
482
+ if state.get("agent_builder_mode") in ["planning", "confirming", "generating"]:
483
+ return Command(goto="agent_builder")
484
+ return Command(goto="call_model")
485
+
140
486
  agent = StateGraph(state_schema=CodeActState)
141
487
  agent.add_node(call_model, retry_policy=RetryPolicy(max_attempts=3, retry_on=filter_retry_on))
142
- agent.add_node(sandbox)
143
- agent.add_edge(START, "call_model")
488
+ agent.add_node(agent_builder)
489
+ agent.add_node(execute_tools)
490
+ agent.add_node(route_entry)
491
+ agent.add_edge(START, "route_entry")
144
492
  return agent.compile(checkpointer=self.memory)
@@ -1,6 +1,6 @@
1
1
  from universal_mcp.agentr.registry import AgentrRegistry
2
2
 
3
- from universal_mcp.agents.codeact0.playbook_agent import CodeActPlaybookAgent
3
+ from universal_mcp.agents.codeact0 import CodeActPlaybookAgent
4
4
 
5
5
 
6
6
  async def agent():