letta-nightly 0.5.4.dev20241126104249__py3-none-any.whl → 0.5.4.dev20241128000451__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.

Potentially problematic release.


This version of letta-nightly might be problematic. Click here for more details.

Files changed (46) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +102 -140
  3. letta/agent_store/chroma.py +2 -0
  4. letta/cli/cli.py +3 -5
  5. letta/client/client.py +360 -117
  6. letta/config.py +2 -2
  7. letta/constants.py +5 -0
  8. letta/errors.py +12 -0
  9. letta/functions/function_sets/base.py +38 -1
  10. letta/functions/functions.py +4 -6
  11. letta/functions/schema_generator.py +6 -5
  12. letta/helpers/tool_rule_solver.py +6 -5
  13. letta/main.py +1 -1
  14. letta/metadata.py +45 -42
  15. letta/o1_agent.py +1 -4
  16. letta/orm/block.py +2 -1
  17. letta/orm/blocks_agents.py +4 -1
  18. letta/orm/sqlalchemy_base.py +13 -0
  19. letta/persistence_manager.py +1 -0
  20. letta/schemas/agent.py +57 -52
  21. letta/schemas/block.py +70 -26
  22. letta/schemas/enums.py +14 -0
  23. letta/schemas/letta_base.py +1 -1
  24. letta/schemas/letta_request.py +11 -23
  25. letta/schemas/letta_response.py +1 -2
  26. letta/schemas/memory.py +31 -100
  27. letta/schemas/message.py +3 -3
  28. letta/schemas/tool_rule.py +13 -5
  29. letta/server/rest_api/interface.py +12 -19
  30. letta/server/rest_api/routers/openai/assistants/threads.py +2 -3
  31. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -2
  32. letta/server/rest_api/routers/v1/agents.py +100 -94
  33. letta/server/rest_api/routers/v1/blocks.py +50 -5
  34. letta/server/rest_api/routers/v1/tools.py +14 -3
  35. letta/server/server.py +246 -460
  36. letta/server/static_files/assets/index-9fa459a2.js +1 -1
  37. letta/services/block_manager.py +23 -4
  38. letta/services/blocks_agents_manager.py +23 -1
  39. letta/services/per_agent_lock_manager.py +18 -0
  40. letta/services/tool_execution_sandbox.py +1 -1
  41. letta/services/tool_manager.py +2 -1
  42. {letta_nightly-0.5.4.dev20241126104249.dist-info → letta_nightly-0.5.4.dev20241128000451.dist-info}/METADATA +1 -1
  43. {letta_nightly-0.5.4.dev20241126104249.dist-info → letta_nightly-0.5.4.dev20241128000451.dist-info}/RECORD +46 -45
  44. {letta_nightly-0.5.4.dev20241126104249.dist-info → letta_nightly-0.5.4.dev20241128000451.dist-info}/LICENSE +0 -0
  45. {letta_nightly-0.5.4.dev20241126104249.dist-info → letta_nightly-0.5.4.dev20241128000451.dist-info}/WHEEL +0 -0
  46. {letta_nightly-0.5.4.dev20241126104249.dist-info → letta_nightly-0.5.4.dev20241128000451.dist-info}/entry_points.txt +0 -0
letta/__init__.py CHANGED
@@ -4,7 +4,7 @@ __version__ = "0.5.4"
4
4
  from letta.client.client import LocalClient, RESTClient, create_client
5
5
 
6
6
  # imports for easier access
7
- from letta.schemas.agent import AgentState
7
+ from letta.schemas.agent import AgentState, PersistedAgentState
8
8
  from letta.schemas.block import Block
9
9
  from letta.schemas.embedding_config import EmbeddingConfig
10
10
  from letta.schemas.enums import JobStatus
letta/agent.py CHANGED
@@ -31,7 +31,7 @@ from letta.metadata import MetadataStore
31
31
  from letta.orm import User
32
32
  from letta.persistence_manager import LocalStateManager
33
33
  from letta.schemas.agent import AgentState, AgentStepResponse
34
- from letta.schemas.block import Block
34
+ from letta.schemas.block import BlockUpdate
35
35
  from letta.schemas.embedding_config import EmbeddingConfig
36
36
  from letta.schemas.enums import MessageRole
37
37
  from letta.schemas.memory import ContextWindowOverview, Memory
@@ -235,11 +235,8 @@ class Agent(BaseAgent):
235
235
  def __init__(
236
236
  self,
237
237
  interface: Optional[Union[AgentInterface, StreamingRefreshCLIInterface]],
238
- # agents can be created from providing agent_state
239
- agent_state: AgentState,
240
- tools: List[Tool],
238
+ agent_state: AgentState, # in-memory representation of the agent state (read from multiple tables)
241
239
  user: User,
242
- # memory: Memory,
243
240
  # extras
244
241
  messages_total: Optional[int] = None, # TODO remove?
245
242
  first_message_verify_mono: bool = True, # TODO move to config?
@@ -253,7 +250,7 @@ class Agent(BaseAgent):
253
250
  self.user = user
254
251
 
255
252
  # link tools
256
- self.link_tools(tools)
253
+ self.link_tools(agent_state.tools)
257
254
 
258
255
  # initialize a tool rules solver
259
256
  if agent_state.tool_rules:
@@ -265,26 +262,14 @@ class Agent(BaseAgent):
265
262
  # add default rule for having send_message be a terminal tool
266
263
  if agent_state.tool_rules is None:
267
264
  agent_state.tool_rules = []
268
- # Define the rule to add
269
- send_message_terminal_rule = TerminalToolRule(tool_name="send_message")
270
- # Check if an equivalent rule is already present
271
- if not any(
272
- isinstance(rule, TerminalToolRule) and rule.tool_name == send_message_terminal_rule.tool_name for rule in agent_state.tool_rules
273
- ):
274
- agent_state.tool_rules.append(send_message_terminal_rule)
275
265
 
276
266
  self.tool_rules_solver = ToolRulesSolver(tool_rules=agent_state.tool_rules)
277
267
 
278
268
  # gpt-4, gpt-3.5-turbo, ...
279
269
  self.model = self.agent_state.llm_config.model
280
270
 
281
- # Store the system instructions (used to rebuild memory)
282
- self.system = self.agent_state.system
283
-
284
- # Initialize the memory object
285
- self.memory = self.agent_state.memory
286
- assert isinstance(self.memory, Memory), f"Memory object is not of type Memory: {type(self.memory)}"
287
- printd("Initialized memory object", self.memory.compile())
271
+ # state managers
272
+ self.block_manager = BlockManager()
288
273
 
289
274
  # Interface must implement:
290
275
  # - internal_monologue
@@ -322,8 +307,8 @@ class Agent(BaseAgent):
322
307
  # Generate a sequence of initial messages to put in the buffer
323
308
  init_messages = initialize_message_sequence(
324
309
  model=self.model,
325
- system=self.system,
326
- memory=self.memory,
310
+ system=self.agent_state.system,
311
+ memory=self.agent_state.memory,
327
312
  archival_memory=None,
328
313
  recall_memory=None,
329
314
  memory_edit_timestamp=get_utc_time(),
@@ -345,8 +330,8 @@ class Agent(BaseAgent):
345
330
  # Basic "more human than human" initial message sequence
346
331
  init_messages = initialize_message_sequence(
347
332
  model=self.model,
348
- system=self.system,
349
- memory=self.memory,
333
+ system=self.agent_state.system,
334
+ memory=self.agent_state.memory,
350
335
  archival_memory=None,
351
336
  recall_memory=None,
352
337
  memory_edit_timestamp=get_utc_time(),
@@ -380,6 +365,76 @@ class Agent(BaseAgent):
380
365
  # Create the agent in the DB
381
366
  self.update_state()
382
367
 
368
+ def update_memory_if_change(self, new_memory: Memory) -> bool:
369
+ """
370
+ Update internal memory object and system prompt if there have been modifications.
371
+
372
+ Args:
373
+ new_memory (Memory): the new memory object to compare to the current memory object
374
+
375
+ Returns:
376
+ modified (bool): whether the memory was updated
377
+ """
378
+ if self.agent_state.memory.compile() != new_memory.compile():
379
+ # update the blocks (LRW) in the DB
380
+ for label in self.agent_state.memory.list_block_labels():
381
+ updated_value = new_memory.get_block(label).value
382
+ if updated_value != self.agent_state.memory.get_block(label).value:
383
+ # update the block if it's changed
384
+ block_id = self.agent_state.memory.get_block(label).id
385
+ block = self.block_manager.update_block(
386
+ block_id=block_id, block_update=BlockUpdate(value=updated_value), actor=self.user
387
+ )
388
+
389
+ # refresh memory from DB (using block ids)
390
+ self.agent_state.memory = Memory(
391
+ blocks=[self.block_manager.get_block_by_id(block.id, actor=self.user) for block in self.agent_state.memory.get_blocks()]
392
+ )
393
+
394
+ # NOTE: don't do this since re-buildin the memory is handled at the start of the step
395
+ # rebuild memory - this records the last edited timestamp of the memory
396
+ # TODO: pass in update timestamp from block edit time
397
+ self.rebuild_system_prompt()
398
+
399
+ return True
400
+ return False
401
+
402
+ def execute_tool_and_persist_state(self, function_name, function_to_call, function_args):
403
+ """
404
+ Execute tool modifications and persist the state of the agent.
405
+ Note: only some agent state modifications will be persisted, such as data in the AgentState ORM and block data
406
+ """
407
+ # TODO: add agent manager here
408
+ orig_memory_str = self.agent_state.memory.compile()
409
+
410
+ # TODO: need to have an AgentState object that actually has full access to the block data
411
+ # this is because the sandbox tools need to be able to access block.value to edit this data
412
+ try:
413
+ if function_name in BASE_TOOLS:
414
+ # base tools are allowed to access the `Agent` object and run on the database
415
+ function_args["self"] = self # need to attach self to arg since it's dynamically linked
416
+ function_response = function_to_call(**function_args)
417
+ else:
418
+ # execute tool in a sandbox
419
+ # TODO: allow agent_state to specify which sandbox to execute tools in
420
+ sandbox_run_result = ToolExecutionSandbox(function_name, function_args, self.agent_state.user_id).run(
421
+ agent_state=self.agent_state.__deepcopy__()
422
+ )
423
+ function_response, updated_agent_state = sandbox_run_result.func_return, sandbox_run_result.agent_state
424
+ assert orig_memory_str == self.agent_state.memory.compile(), "Memory should not be modified in a sandbox tool"
425
+ self.update_memory_if_change(updated_agent_state.memory)
426
+ except Exception as e:
427
+ # Need to catch error here, or else trunction wont happen
428
+ # TODO: modify to function execution error
429
+ from letta.constants import MAX_ERROR_MESSAGE_CHAR_LIMIT
430
+
431
+ error_msg = f"Error executing tool {function_name}: {e}"
432
+ if len(error_msg) > MAX_ERROR_MESSAGE_CHAR_LIMIT:
433
+ error_msg = error_msg[:MAX_ERROR_MESSAGE_CHAR_LIMIT]
434
+ raise ValueError(error_msg)
435
+
436
+ return function_response
437
+
383
438
  @property
384
439
  def messages(self) -> List[dict]:
385
440
  """Getter method that converts the internal Message list into OpenAI-style dicts"""
@@ -392,16 +447,6 @@ class Agent(BaseAgent):
392
447
  def link_tools(self, tools: List[Tool]):
393
448
  """Bind a tool object (schema + python function) to the agent object"""
394
449
 
395
- # tools
396
- for tool in tools:
397
- assert tool, f"Tool is None - must be error in querying tool from DB"
398
- assert tool.name in self.agent_state.tools, f"Tool {tool} not found in agent_state.tools"
399
- for tool_name in self.agent_state.tools:
400
- assert tool_name in [tool.name for tool in tools], f"Tool name {tool_name} not included in agent tool list"
401
-
402
- # Update tools
403
- self.tools = tools
404
-
405
450
  # Store the functions schemas (this is passed as an argument to ChatCompletion)
406
451
  self.functions = []
407
452
  self.functions_python = {}
@@ -416,9 +461,8 @@ class Agent(BaseAgent):
416
461
  exec(tool.source_code, env)
417
462
  self.functions_python[tool.json_schema["name"]] = env[tool.json_schema["name"]]
418
463
  self.functions.append(tool.json_schema)
419
- except Exception as e:
464
+ except Exception:
420
465
  warnings.warn(f"WARNING: tool {tool.name} failed to link")
421
- print(e)
422
466
  assert all([callable(f) for k, f in self.functions_python.items()]), self.functions_python
423
467
 
424
468
  def _load_messages_from_recall(self, message_ids: List[str]) -> List[Message]:
@@ -727,27 +771,10 @@ class Agent(BaseAgent):
727
771
  if isinstance(function_args[name], dict):
728
772
  function_args[name] = spec[name](**function_args[name])
729
773
 
730
- # TODO: This needs to be rethought, how do we allow functions that modify agent state/db?
731
- # TODO: There should probably be two types of tools: stateless/stateful
732
-
733
- if function_name in BASE_TOOLS:
734
- function_args["self"] = self # need to attach self to arg since it's dynamically linked
735
- function_response = function_to_call(**function_args)
736
- else:
737
- # execute tool in a sandbox
738
- # TODO: allow agent_state to specify which sandbox to execute tools in
739
- sandbox_run_result = ToolExecutionSandbox(function_name, function_args, self.agent_state.user_id).run(
740
- agent_state=self.agent_state
741
- )
742
- function_response, updated_agent_state = sandbox_run_result.func_return, sandbox_run_result.agent_state
743
- # update agent state
744
- if self.agent_state != updated_agent_state and updated_agent_state is not None:
745
- self.agent_state = updated_agent_state
746
- self.memory = self.agent_state.memory # TODO: don't duplicate
747
-
748
- # rebuild memory
749
- self.rebuild_memory()
774
+ # handle tool execution (sandbox) and state updates
775
+ function_response = self.execute_tool_and_persist_state(function_name, function_to_call, function_args)
750
776
 
777
+ # handle trunction
751
778
  if function_name in ["conversation_search", "conversation_search_date", "archival_memory_search"]:
752
779
  # with certain functions we rely on the paging mechanism to handle overflow
753
780
  truncate = False
@@ -820,7 +847,7 @@ class Agent(BaseAgent):
820
847
 
821
848
  # rebuild memory
822
849
  # TODO: @charles please check this
823
- self.rebuild_memory()
850
+ self.rebuild_system_prompt()
824
851
 
825
852
  # Update ToolRulesSolver state with last called function
826
853
  self.tool_rules_solver.update_tool_usage(function_name)
@@ -936,17 +963,10 @@ class Agent(BaseAgent):
936
963
 
937
964
  # Step 0: update core memory
938
965
  # only pulling latest block data if shared memory is being used
939
- # TODO: ensure we're passing in metadata store from all surfaces
940
- if ms is not None:
941
- should_update = False
942
- for block in self.agent_state.memory.to_dict()["memory"].values():
943
- if not block.get("template", False):
944
- should_update = True
945
- if should_update:
946
- # TODO: the force=True can be optimized away
947
- # once we ensure we're correctly comparing whether in-memory core
948
- # data is different than persisted core data.
949
- self.rebuild_memory(force=True, ms=ms)
966
+ current_persisted_memory = Memory(
967
+ blocks=[self.block_manager.get_block_by_id(block.id, actor=self.user) for block in self.agent_state.memory.get_blocks()]
968
+ ) # read blocks from DB
969
+ self.update_memory_if_change(current_persisted_memory)
950
970
 
951
971
  # Step 1: add user message
952
972
  if isinstance(messages, Message):
@@ -1229,43 +1249,10 @@ class Agent(BaseAgent):
1229
1249
  new_messages = [new_system_message_obj] + self._messages[1:] # swap index 0 (system)
1230
1250
  self._messages = new_messages
1231
1251
 
1232
- def update_memory_blocks_from_db(self):
1233
- for block in self.memory.to_dict()["memory"].values():
1234
- if block.get("templates", False):
1235
- # we don't expect to update shared memory blocks that
1236
- # are templates. this is something we could update in the
1237
- # future if we expect templates to change often.
1238
- continue
1239
- block_id = block.get("id")
1240
-
1241
- # TODO: This is really hacky and we should probably figure out how to
1242
- db_block = BlockManager().get_block_by_id(block_id=block_id, actor=self.user)
1243
- if db_block is None:
1244
- # this case covers if someone has deleted a shared block by interacting
1245
- # with some other agent.
1246
- # in that case we should remove this shared block from the agent currently being
1247
- # evaluated.
1248
- printd(f"removing block: {block_id=}")
1249
- continue
1250
- if not isinstance(db_block.value, str):
1251
- printd(f"skipping block update, unexpected value: {block_id=}")
1252
- continue
1253
- # TODO: we may want to update which columns we're updating from shared memory e.g. the limit
1254
- self.memory.update_block_value(label=block.get("label", ""), value=db_block.value)
1255
-
1256
- def rebuild_memory(self, force=False, update_timestamp=True, ms: Optional[MetadataStore] = None):
1252
+ def rebuild_system_prompt(self, force=False, update_timestamp=True):
1257
1253
  """Rebuilds the system message with the latest memory object and any shared memory block updates"""
1258
1254
  curr_system_message = self.messages[0] # this is the system + memory bank, not just the system prompt
1259
1255
 
1260
- # NOTE: This is a hacky way to check if the memory has changed
1261
- memory_repr = self.memory.compile()
1262
- if not force and memory_repr == curr_system_message["content"][-(len(memory_repr)) :]:
1263
- printd(f"Memory has not changed, not rebuilding system")
1264
- return
1265
-
1266
- if ms:
1267
- self.update_memory_blocks_from_db()
1268
-
1269
1256
  # If the memory didn't update, we probably don't want to update the timestamp inside
1270
1257
  # For example, if we're doing a system prompt swap, this should probably be False
1271
1258
  if update_timestamp:
@@ -1276,8 +1263,8 @@ class Agent(BaseAgent):
1276
1263
 
1277
1264
  # update memory (TODO: potentially update recall/archival stats seperately)
1278
1265
  new_system_message_str = compile_system_message(
1279
- system_prompt=self.system,
1280
- in_context_memory=self.memory,
1266
+ system_prompt=self.agent_state.system,
1267
+ in_context_memory=self.agent_state.memory,
1281
1268
  in_context_memory_last_edit=memory_edit_timestamp,
1282
1269
  archival_memory=self.persistence_manager.archival_memory,
1283
1270
  recall_memory=self.persistence_manager.recall_memory,
@@ -1304,13 +1291,13 @@ class Agent(BaseAgent):
1304
1291
  """Update the system prompt of the agent (requires rebuilding the memory block if there's a difference)"""
1305
1292
  assert isinstance(new_system_prompt, str)
1306
1293
 
1307
- if new_system_prompt == self.system:
1294
+ if new_system_prompt == self.agent_state.system:
1308
1295
  return
1309
1296
 
1310
- self.system = new_system_prompt
1297
+ self.agent_state.system = new_system_prompt
1311
1298
 
1312
1299
  # updating the system prompt requires rebuilding the memory block inside the compiled system message
1313
- self.rebuild_memory(force=True, update_timestamp=False)
1300
+ self.rebuild_system_prompt(force=True, update_timestamp=False)
1314
1301
 
1315
1302
  # make sure to persist the change
1316
1303
  _ = self.update_state()
@@ -1324,6 +1311,7 @@ class Agent(BaseAgent):
1324
1311
  raise NotImplementedError
1325
1312
 
1326
1313
  def update_state(self) -> AgentState:
1314
+ # TODO: this should be removed and self._messages should be moved into self.agent_state.in_context_messages
1327
1315
  message_ids = [msg.id for msg in self._messages]
1328
1316
 
1329
1317
  # Assert that these are all strings
@@ -1331,12 +1319,8 @@ class Agent(BaseAgent):
1331
1319
  warnings.warn(f"Non-string message IDs found in agent state: {message_ids}")
1332
1320
  message_ids = [m_id for m_id in message_ids if isinstance(m_id, str)]
1333
1321
 
1334
- assert isinstance(self.memory, Memory), f"Memory is not a Memory object: {type(self.memory)}"
1335
-
1336
1322
  # override any fields that may have been updated
1337
1323
  self.agent_state.message_ids = message_ids
1338
- self.agent_state.memory = self.memory
1339
- self.agent_state.system = self.system
1340
1324
 
1341
1325
  return self.agent_state
1342
1326
 
@@ -1537,7 +1521,7 @@ class Agent(BaseAgent):
1537
1521
 
1538
1522
  system_prompt = self.agent_state.system # TODO is this the current system or the initial system?
1539
1523
  num_tokens_system = count_tokens(system_prompt)
1540
- core_memory = self.memory.compile()
1524
+ core_memory = self.agent_state.memory.compile()
1541
1525
  num_tokens_core_memory = count_tokens(core_memory)
1542
1526
 
1543
1527
  # conversion of messages to OpenAI dict format, which is passed to the token counter
@@ -1629,37 +1613,15 @@ def save_agent(agent: Agent, ms: MetadataStore):
1629
1613
 
1630
1614
  agent.update_state()
1631
1615
  agent_state = agent.agent_state
1632
- agent_id = agent_state.id
1633
1616
  assert isinstance(agent_state.memory, Memory), f"Memory is not a Memory object: {type(agent_state.memory)}"
1634
1617
 
1635
- # NOTE: we're saving agent memory before persisting the agent to ensure
1636
- # that allocated block_ids for each memory block are present in the agent model
1637
- save_agent_memory(agent=agent)
1638
-
1639
- if ms.get_agent(agent_id=agent.agent_state.id):
1640
- ms.update_agent(agent_state)
1618
+ # TODO: move this to agent manager
1619
+ # convert to persisted model
1620
+ persisted_agent_state = agent.agent_state.to_persisted_agent_state()
1621
+ if ms.get_agent(agent_id=persisted_agent_state.id):
1622
+ ms.update_agent(persisted_agent_state)
1641
1623
  else:
1642
- ms.create_agent(agent_state)
1643
-
1644
- agent.agent_state = ms.get_agent(agent_id=agent_id)
1645
- assert isinstance(agent.agent_state.memory, Memory), f"Memory is not a Memory object: {type(agent_state.memory)}"
1646
-
1647
-
1648
- def save_agent_memory(agent: Agent):
1649
- """
1650
- Save agent memory to metadata store. Memory is a collection of blocks and each block is persisted to the block table.
1651
-
1652
- NOTE: we are assuming agent.update_state has already been called.
1653
- """
1654
-
1655
- for block_dict in agent.memory.to_dict()["memory"].values():
1656
- # TODO: block creation should happen in one place to enforce these sort of constraints consistently.
1657
- block = Block(**block_dict)
1658
- # FIXME: should we expect for block values to be None? If not, we need to figure out why that is
1659
- # the case in some tests, if so we should relax the DB constraint.
1660
- if block.value is None:
1661
- block.value = ""
1662
- BlockManager().create_or_update_block(block, actor=agent.user)
1624
+ ms.create_agent(persisted_agent_state)
1663
1625
 
1664
1626
 
1665
1627
  def strip_name_field_from_user_message(user_message_text: str) -> Tuple[str, Optional[str]]:
@@ -125,6 +125,8 @@ class ChromaStorageConnector(StorageConnector):
125
125
  ids, filters = self.get_filters(filters)
126
126
  if self.collection.count() == 0:
127
127
  return []
128
+ if ids == []:
129
+ ids = None
128
130
  if limit:
129
131
  results = self.collection.get(ids=ids, include=self.include, where=filters, limit=limit)
130
132
  else:
letta/cli/cli.py CHANGED
@@ -219,7 +219,7 @@ def run(
219
219
  )
220
220
 
221
221
  # create agent
222
- tools = [server.tool_manager.get_tool_by_name(tool_name=tool_name, actor=client.user) for tool_name in agent_state.tools]
222
+ tools = [server.tool_manager.get_tool_by_name(tool_name=tool_name, actor=client.user) for tool_name in agent_state.tool_names]
223
223
  letta_agent = Agent(agent_state=agent_state, interface=interface(), tools=tools, user=client.user)
224
224
 
225
225
  else: # create new agent
@@ -311,13 +311,11 @@ def run(
311
311
  metadata=metadata,
312
312
  )
313
313
  assert isinstance(agent_state.memory, Memory), f"Expected Memory, got {type(agent_state.memory)}"
314
- typer.secho(f"-> 🛠️ {len(agent_state.tools)} tools: {', '.join([t for t in agent_state.tools])}", fg=typer.colors.WHITE)
315
- tools = [server.tool_manager.get_tool_by_name(tool_name, actor=client.user) for tool_name in agent_state.tools]
314
+ typer.secho(f"-> 🛠️ {len(agent_state.tools)} tools: {', '.join([t for t in agent_state.tool_names])}", fg=typer.colors.WHITE)
316
315
 
317
316
  letta_agent = Agent(
318
317
  interface=interface(),
319
- agent_state=agent_state,
320
- tools=tools,
318
+ agent_state=client.get_agent(agent_state.id),
321
319
  # gpt-3.5-turbo tends to omit inner monologue, relax this requirement for now
322
320
  first_message_verify_mono=True if (model is not None and "gpt-4" in model) else False,
323
321
  user=client.user,