amrita_core 0.10.0.dev2__tar.gz → 0.10.2__tar.gz

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 (81) hide show
  1. {amrita_core-0.10.0.dev2/src/amrita_core.egg-info → amrita_core-0.10.2}/PKG-INFO +1 -1
  2. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/pyproject.toml +1 -1
  3. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/__init__.py +2 -0
  4. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/base/backend.py +3 -3
  5. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/builtins/agent.py +227 -119
  6. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/builtins/tools.py +2 -1
  7. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/chatmanager/chat_object.py +53 -12
  8. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2/src/amrita_core.egg-info}/PKG-INFO +1 -1
  9. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/LICENSE +0 -0
  10. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/README.md +0 -0
  11. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/setup.cfg +0 -0
  12. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/_env.py +0 -0
  13. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/adapters/anthropic.py +0 -0
  14. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/adapters/openai.py +0 -0
  15. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/agent/context.py +0 -0
  16. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/agent/functions.py +0 -0
  17. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/agent/strategy.py +0 -0
  18. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/base/adapter.py +0 -0
  19. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/base/tokenizer.py +0 -0
  20. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/builtins/__init__.py +0 -0
  21. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/builtins/backends.py +0 -0
  22. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/builtins/consts.py +0 -0
  23. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/builtins/hooks.py +0 -0
  24. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/builtins/types.py +0 -0
  25. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/chatmanager/__init__.py +0 -0
  26. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/chatmanager/chat_libs.py +0 -0
  27. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/chatmanager/chat_obj_meta.py +0 -0
  28. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/chatmanager/enums.py +0 -0
  29. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/chatmanager/memory_limiter.py +0 -0
  30. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/config.py +0 -0
  31. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/consts.py +0 -0
  32. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/contents.py +0 -0
  33. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/contexts.py +0 -0
  34. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/dirty.py +0 -0
  35. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/hook/event.py +0 -0
  36. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/hook/exception.py +0 -0
  37. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/hook/on.py +0 -0
  38. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/libchat.py +0 -0
  39. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/preset.py +0 -0
  40. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/tokenizer.py +0 -0
  41. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/tokenizers/jieba_based.py +0 -0
  42. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/tokenizers/simple.py +0 -0
  43. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/tools/manager.py +0 -0
  44. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/tools/mcp.py +0 -0
  45. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/tools/models.py +0 -0
  46. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/types/__init__.py +0 -0
  47. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/types/base.py +0 -0
  48. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/types/content.py +0 -0
  49. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/types/embedding.py +0 -0
  50. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/types/memory.py +0 -0
  51. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/types/message.py +0 -0
  52. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/types/preset.py +0 -0
  53. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/types/response.py +0 -0
  54. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/types/tool.py +0 -0
  55. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core/utils.py +0 -0
  56. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core.egg-info/SOURCES.txt +0 -0
  57. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core.egg-info/dependency_links.txt +0 -0
  58. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core.egg-info/requires.txt +0 -0
  59. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/src/amrita_core.egg-info/top_level.txt +0 -0
  60. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_adapter.py +0 -0
  61. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_agent.py +0 -0
  62. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_chatmanager.py +0 -0
  63. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_chatobject.py +0 -0
  64. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_complete_content_validation.py +0 -0
  65. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_content_deserialization.py +0 -0
  66. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_functions.py +0 -0
  67. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_hooks.py +0 -0
  68. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_integration.py +0 -0
  69. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_libchat.py +0 -0
  70. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_manager.py +0 -0
  71. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_mcp.py +0 -0
  72. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_message_deserialization.py +0 -0
  73. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_message_edge_cases.py +0 -0
  74. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_preset.py +0 -0
  75. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_protocol.py +0 -0
  76. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_register_content.py +0 -0
  77. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_serialization_integrity.py +0 -0
  78. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_simple_tool_extended.py +0 -0
  79. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_tools.py +0 -0
  80. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_types.py +0 -0
  81. {amrita_core-0.10.0.dev2 → amrita_core-0.10.2}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amrita_core
3
- Version: 0.10.0.dev2
3
+ Version: 0.10.2
4
4
  Summary: High performance, flexible, lightweight agent framework.
5
5
  Project-URL: Homepage, https://github.com/AmritaBot/AmritaCore
6
6
  Project-URL: Source, https://github.com/AmritaBot/AmritaCore
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "amrita_core"
3
- version = "0.10.0.dev2"
3
+ version = "0.10.2"
4
4
  description = "High performance, flexible, lightweight agent framework."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10,<3.15"
@@ -113,7 +113,9 @@ __all__ = [
113
113
  "get_config",
114
114
  "get_last_response",
115
115
  "get_tokens",
116
+ "load_amrita",
116
117
  "mcp",
118
+ "minimal_init",
117
119
  "on_completion",
118
120
  "on_event",
119
121
  "on_precompletion",
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from abc import abstractmethod
3
+ from abc import ABC, abstractmethod
4
4
  from dataclasses import dataclass
5
5
  from typing import TYPE_CHECKING
6
6
 
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
12
12
  from amrita_core.types import MemoryModel
13
13
 
14
14
 
15
- class AbilityBackend:
15
+ class AbilityBackend(ABC):
16
16
  @abstractmethod
17
17
  async def load_ability_all(self, session_id: str) -> AbilityContext:
18
18
  """Load ability"""
@@ -28,7 +28,7 @@ class AbilityBackend:
28
28
  async def load_presets(self, session_id: str) -> MultiPresetManager: ...
29
29
 
30
30
 
31
- class MemoryBackend:
31
+ class MemoryBackend(ABC):
32
32
  @abstractmethod
33
33
  async def load_memory(self, session_id: str) -> MemoryModel:
34
34
  """Load memory"""
@@ -21,6 +21,7 @@ from amrita_core.libchat import (
21
21
  )
22
22
  from amrita_core.types import (
23
23
  CONTENT_LIST_TYPE_ITEM,
24
+ Function,
24
25
  Message,
25
26
  SendMessageWrap,
26
27
  TextContent,
@@ -578,158 +579,230 @@ class BaseReActAgentStrategy(AgentStrategy, ABC):
578
579
  """
579
580
  pass
580
581
 
582
+ async def _run_tool_calls_concurrently(
583
+ self,
584
+ tool_calls: list[ToolCall],
585
+ ) -> list[tuple[ToolCall, str, BaseException | None]]:
586
+ """Execute multiple tool calls concurrently via asyncio.gather.
587
+
588
+ Built-in flow-control tools (REASONING, STOP) are dispatched to their
589
+ specialised handlers inside each coroutine. Regular tools use
590
+ :meth:`call_tool`. One failure does not cancel the others.
591
+ Results are returned in the same order as the input list.
592
+
593
+ Subclasses may override this to change the execution strategy (e.g. add
594
+ throttling, retries, or switch to sequential fallback).
595
+
596
+ Args:
597
+ tool_calls: ToolCall objects to execute concurrently. Callers should
598
+ order them so that built-in tools appear **after** regular tools
599
+ so that the side-effects of built-in handlers do not race with
600
+ the context modifications produced by regular-tool result
601
+ processing.
602
+
603
+ Returns:
604
+ List of ``(tool_call, result_string, original_exception)`` tuples.
605
+ ``original_exception`` is the exception instance when an error
606
+ occurred, ``None`` otherwise. On error ``result_string`` is
607
+ formatted as ``"ERR: Tool {name} execution failed\\n{error}"``.
608
+ """
609
+
610
+ async def _exec_one(tc: ToolCall) -> tuple[ToolCall, str, BaseException | None]:
611
+ fn = tc.function.name
612
+ try:
613
+ if fn == REASONING_TOOL.function.name:
614
+ content: UniResponse[
615
+ str, None
616
+ ] = await self._generate_reasoning_content(
617
+ tc, self.ctx.original_context.unwrap()
618
+ )
619
+ await self._append_reasoning(tc, content)
620
+ return (tc, content.content, None)
621
+ if fn == STOP_TOOL.function.name:
622
+ args: dict[str, Any] = json.loads(tc.function.arguments)
623
+ self.agent_last_step = "Stopped"
624
+ self.reasoning_pc = 0
625
+ self._suggested_stop = True
626
+ logger.info("Agent work has been terminated.")
627
+
628
+ if self._should_enable_reflection():
629
+ logger.debug("Running post-reasoning reflection...")
630
+ reason_ctx = self.ctx.original_context.unwrap()
631
+ reflection_results = await self._run_reflection(reason_ctx)
632
+ failures = [
633
+ r for r in reflection_results if r["result"] == "fail"
634
+ ]
635
+ if failures:
636
+ correction_msg = (
637
+ "<BEGIN_OF_REFLECTION_CORRECTION>\n"
638
+ + "Your reasoning was checked and the following issues were found:\n"
639
+ + "\n".join(
640
+ f"- [{r['check_type']}] {r['detail']}"
641
+ for r in failures
642
+ )
643
+ + "\nPlease re-examine your reasoning and correct these issues "
644
+ + "before providing the final answer.\n"
645
+ + "<END_OF_REFLECTION_CORRECTION>"
646
+ )
647
+ self.ctx.message.append(
648
+ Message(role="user", content=correction_msg)
649
+ )
650
+ # Empty result → caller skips stop response.
651
+ return (tc, "", None)
652
+
653
+ result = self._build_stop_response(args)
654
+ return (tc, result, None)
655
+
656
+ # Regular tool
657
+ result = await self.call_tool(tc)
658
+ return (tc, result, None)
659
+ except Exception as e:
660
+ error_content = await self._handle_tool_error_common(fn, e, tc.id)
661
+ return (tc, error_content, e)
662
+
663
+ return await asyncio.gather(*[_exec_one(tc) for tc in tool_calls])
664
+
581
665
  async def _execute_tool_loop(
582
666
  self,
583
667
  response_msg: UniResponse[None, list[ToolCall] | None],
584
668
  ) -> bool:
585
- """Execute the main tool calling loop with strategy-specific behaviors.
669
+ """Execute the main tool calling loop.
586
670
 
587
- This is a template method that defines the common execution flow while
588
- delegating strategy-specific behaviors to abstract methods.
671
+ All tool calls are executed concurrently via
672
+ :meth:`_run_tool_calls_concurrently`. Built-in tools are ordered last
673
+ so that their message-list side-effects (reasoning append, stop
674
+ response) are applied after regular-tool results.
589
675
 
590
676
  Args:
591
- response_msg: The response from tools_caller containing tool calls
677
+ response_msg: The response from tools_caller containing tool calls.
592
678
 
593
679
  Returns:
594
- True if execution should continue, False if it should stop
680
+ True if execution should continue, False if it should stop.
595
681
  """
596
682
  if not (tool_calls := response_msg.tool_calls):
597
683
  return False
598
684
 
599
- result_msg_list: list[ToolResult] = []
600
- ret: bool = True
601
- for tool_call in tool_calls:
602
- function_name = tool_call.function.name
603
- function_args: dict[str, Any] = json.loads(tool_call.function.arguments)
604
- debug_log(f"Function arguments are {tool_call.function.arguments}")
605
- logger.info(f"Calling function {function_name}")
685
+ # Built-in tools last so their side-effects don't race with regular tools.
686
+ tool_calls.sort(
687
+ key=lambda tc: (
688
+ tc.function.name
689
+ in (REASONING_TOOL.function.name, STOP_TOOL.function.name)
690
+ )
691
+ )
692
+
693
+ # Notify "calling" for every tool
694
+ for tc in tool_calls:
606
695
  await self.chat_object.io_stream.yield_response(
607
696
  MessageWithMetadata(
608
- content=f"Calling function {function_name}\n",
697
+ content=f"Calling function {tc.function.name}\n",
609
698
  metadata=AgentToolCallMetadata(
610
699
  type="function_call",
611
700
  extra_type=None,
612
- function_name=function_name,
701
+ function_name=tc.function.name,
613
702
  is_done=False,
614
- tool_id=tool_call.id,
703
+ tool_id=tc.id,
615
704
  err=None,
616
705
  ),
617
706
  )
618
707
  )
619
708
 
620
- func_response: str = ""
621
- try:
622
- match function_name:
623
- case REASONING_TOOL.function.name:
624
- logger.debug("Generating task summary and reason.")
625
- content: UniResponse[
626
- str, None
627
- ] = await self._generate_reasoning_content(
628
- tool_call, self.ctx.original_context.unwrap()
629
- )
630
- await self._append_reasoning(tool_call, content)
631
- return True
632
- case STOP_TOOL.function.name:
633
- self.agent_last_step = "Stopped"
634
- self.reasoning_pc = 0
635
- self._suggested_stop = True
636
- logger.info("Agent work has been terminated.")
637
-
638
- if self._should_enable_reflection():
639
- logger.debug("Running post-reasoning reflection...")
640
- reason_ctx = self.ctx.original_context.unwrap()
641
- reflection_results = await self._run_reflection(reason_ctx)
642
- # Check for failures and inject correction
643
- failures = [
644
- r for r in reflection_results if r["result"] == "fail"
645
- ]
646
- if failures:
647
- correction_msg = (
648
- "<BEGIN_OF_REFLECTION_CORRECTION>\n"
649
- + "Your reasoning was checked and the following issues were found:\n"
650
- + "\n".join(
651
- f"- [{r['check_type']}] {r['detail']}"
652
- for r in failures
653
- )
654
- + "\nPlease re-examine your reasoning and correct these issues "
655
- + "before providing the final answer.\n"
656
- + "<END_OF_REFLECTION_CORRECTION>"
657
- )
658
- self.ctx.message.append(
659
- Message(
660
- role="user",
661
- content=correction_msg,
662
- )
663
- )
664
- # Don't build stop response yet — let the agent re-reason
665
- continue
666
-
667
- func_response = self._build_stop_response(function_args)
668
- await self._build_stop_response_and_append(
669
- function_args,
670
- response_msg,
671
- function_name,
672
- tool_call.id,
673
- func_response,
674
- )
675
- case _:
676
- self.reasoning_pc = 0
677
- func_response = await self.call_tool(tool_call)
678
- await self._append_tool_result_to_context(
679
- tool_call, func_response, response_msg
680
- )
681
- except Exception as err:
682
- error_content = await self._handle_tool_error_common(
683
- function_name, err, tool_call.id
709
+ concurrent_results: list[
710
+ tuple[ToolCall, str, BaseException | None]
711
+ ] = await self._run_tool_calls_concurrently(tool_calls)
712
+
713
+ # Append results sequentially
714
+ result_msg_list: list[ToolResult] = []
715
+ should_continue = True # default: keep looping (old `ret` semantics)
716
+ for tc, func_response, exc in concurrent_results:
717
+ function_name = tc.function.name
718
+ is_reasoning = function_name == REASONING_TOOL.function.name
719
+ is_stop = function_name == STOP_TOOL.function.name
720
+
721
+ if is_reasoning:
722
+ # _run_tool_calls_concurrently already called _append_reasoning.
723
+ should_continue = True
724
+ continue
725
+
726
+ if is_stop:
727
+ if not func_response:
728
+ # Reflection failed — correction already injected, continue.
729
+ continue
730
+ await self._build_stop_response_and_append(
731
+ json.loads(tc.function.arguments),
732
+ response_msg,
733
+ function_name,
734
+ tc.id,
735
+ func_response,
684
736
  )
685
- # Strategy-specific error handling with original exception
737
+ elif func_response.startswith("ERR:"):
738
+ self.reasoning_pc = 0
686
739
  await self._handle_error_append(
687
740
  function_name,
688
- error_content,
689
- tool_call.id,
690
- original_exception=err,
741
+ func_response,
742
+ tc.id,
743
+ original_exception=exc,
691
744
  )
692
745
  else:
693
- logger.debug(f"Function {function_name} returned: {func_response}")
694
- msg: ToolResult = ToolResult(
746
+ self.reasoning_pc = 0
747
+ await self._append_tool_result_to_context(
748
+ tc, func_response, response_msg
749
+ )
750
+
751
+ logger.debug(f"Function {function_name} returned: {func_response}")
752
+ result_msg_list.append(
753
+ ToolResult(
695
754
  role="tool",
696
755
  content=func_response,
697
756
  name=function_name,
698
- tool_call_id=tool_call.id,
757
+ tool_call_id=tc.id,
699
758
  )
700
- result_msg_list.append(msg)
759
+ )
760
+ self.call_count += 1
701
761
 
702
- finally:
703
- self.call_count += 1
704
- prompt = self._check_and_handle_loop_reasoning()
705
- if prompt is not None:
706
- await self._handle_loop_reasoning_cleanup(prompt)
707
- await self.chat_object.io_stream.yield_response(
708
- MessageWithMetadata(
709
- content=prompt,
710
- metadata=AgentLoopErrorMetadata(
711
- type="error",
712
- extra_type="loop_reasoning",
713
- chat_object_id=self.chat_object.stream_id,
714
- error=prompt,
715
- ),
716
- )
762
+ prompt = self._check_and_handle_loop_reasoning()
763
+ if prompt is not None:
764
+ await self._handle_loop_reasoning_cleanup(prompt)
765
+ await self.chat_object.io_stream.yield_response(
766
+ MessageWithMetadata(
767
+ content=prompt,
768
+ metadata=AgentLoopErrorMetadata(
769
+ type="error",
770
+ extra_type="loop_reasoning",
771
+ chat_object_id=self.chat_object.stream_id,
772
+ error=prompt,
773
+ ),
717
774
  )
718
- ret = False
775
+ )
776
+ # Stop early — don't process remaining results.
777
+ should_continue = False
778
+ break
719
779
 
720
- # Send tool call info to user
721
- await self._notify_tool_calls(result_msg_list, function_name, tool_call.id)
780
+ # Notify once with the complete result list.
781
+ if result_msg_list:
782
+ await self._notify_tool_calls(
783
+ result_msg_list,
784
+ result_msg_list[-1].name,
785
+ result_msg_list[-1].tool_call_id,
786
+ )
722
787
 
723
- return ret
788
+ return should_continue
724
789
 
725
790
  async def _handle_error_append(
726
791
  self,
727
792
  function_name: str,
728
793
  error_content: str,
729
794
  tool_call_id: str,
730
- original_exception: BaseException,
795
+ original_exception: BaseException | None = None,
731
796
  ):
732
- """Handle appending error messages to context (strategy-specific)."""
797
+ """Handle appending error messages to context (strategy-specific).
798
+
799
+ Args:
800
+ function_name: Name of the failed function.
801
+ error_content: Formatted error message to append.
802
+ tool_call_id: ID of the failed tool call.
803
+ original_exception: The original exception, or ``None`` when the error
804
+ was captured as a string during concurrent execution.
805
+ """
733
806
  ...
734
807
 
735
808
  @abstractmethod
@@ -1127,9 +1200,25 @@ class ReActAgentStrategy(BaseReActAgentStrategy):
1127
1200
  function_call_id: str,
1128
1201
  function_response: str,
1129
1202
  ):
1130
- """ReAct strategy: append assistant message before stop."""
1203
+ """ReAct strategy: append assistant message with only this STOP tool_call before its ToolResult.
1204
+
1205
+ Only a single ToolCall is included in the assistant message to avoid the
1206
+ "insufficient tool messages following tool_calls message" API error.
1207
+ """
1131
1208
  self.ctx.message.append(
1132
- Message.model_validate(response_msg, from_attributes=True)
1209
+ Message(
1210
+ role="assistant",
1211
+ content=None,
1212
+ tool_calls=[
1213
+ ToolCall(
1214
+ id=function_call_id,
1215
+ function=Function(
1216
+ name=function_name,
1217
+ arguments=json.dumps(function_args),
1218
+ ),
1219
+ )
1220
+ ],
1221
+ )
1133
1222
  )
1134
1223
  self.ctx.message.append(
1135
1224
  ToolResult(
@@ -1147,14 +1236,16 @@ class ReActAgentStrategy(BaseReActAgentStrategy):
1147
1236
  func_response: str,
1148
1237
  response_msg: UniResponse[None, list[ToolCall] | None],
1149
1238
  ):
1150
- """ReAct strategy: append both assistant message and tool result as a pair.
1239
+ """ReAct strategy: append assistant message with only this tool_call paired with its ToolResult.
1151
1240
 
1152
1241
  This follows OpenAI's ToolCall-ToolResult pairing requirement where every
1153
1242
  assistant message with tool_calls must be followed by corresponding tool messages.
1243
+ Only a single ToolCall is included per assistant message to prevent the
1244
+ "insufficient tool messages following tool_calls message" API error when the
1245
+ model returns multiple tool_calls in one response.
1154
1246
  """
1155
- # First, append the assistant message containing the tool_calls
1156
1247
  msg_list = self.ctx.message
1157
- msg_list.append(Message.model_validate(response_msg, from_attributes=True))
1248
+ msg_list.append(Message(role="assistant", content=None, tool_calls=[tool_call]))
1158
1249
  msg_list.append(
1159
1250
  ToolResult(
1160
1251
  role="tool",
@@ -1170,20 +1261,36 @@ class ReActAgentStrategy(BaseReActAgentStrategy):
1170
1261
  function_name: str,
1171
1262
  error_content: str,
1172
1263
  tool_call_id: str,
1173
- original_exception: BaseException,
1264
+ original_exception: BaseException | None = None,
1174
1265
  ):
1175
- """ReAct strategy: append error as ToolResult with optional exception context.
1266
+ """ReAct strategy: append error as an assistant+tool message pair.
1176
1267
 
1177
- The original_exception parameter allows for advanced error handling strategies,
1178
- such as different recovery behaviors based on exception type (e.g., retry on
1179
- timeout, abort on authentication errors).
1268
+ An assistant message with a single ToolCall is prepended to satisfy the
1269
+ OpenAI API requirement that every ToolResult must follow an assistant
1270
+ message containing the corresponding tool_call.
1180
1271
 
1181
1272
  Args:
1182
1273
  function_name: Name of the failed function
1183
1274
  error_content: Formatted error message to append
1184
1275
  tool_call_id: ID of the tool call
1185
- original_exception: The original exception object for type-based handling
1276
+ original_exception: The original exception, or ``None`` when the
1277
+ error was captured as a string during concurrent execution.
1186
1278
  """
1279
+ self.ctx.message.append(
1280
+ Message(
1281
+ role="assistant",
1282
+ content=None,
1283
+ tool_calls=[
1284
+ ToolCall(
1285
+ id=tool_call_id,
1286
+ function=Function(
1287
+ name=function_name,
1288
+ arguments="{}",
1289
+ ),
1290
+ )
1291
+ ],
1292
+ )
1293
+ )
1187
1294
  self.ctx.message.append(
1188
1295
  ToolResult(
1189
1296
  role="tool",
@@ -1193,6 +1300,7 @@ class ReActAgentStrategy(BaseReActAgentStrategy):
1193
1300
  )
1194
1301
  )
1195
1302
 
1303
+ @override
1196
1304
  async def single_execute(
1197
1305
  self,
1198
1306
  ) -> bool:
@@ -15,7 +15,8 @@ from .types import AgentMiddleMessageMetadata
15
15
 
16
16
  PROCESS_MESSAGE_TOOL = FunctionDefinitionSchema(
17
17
  name="processing_message",
18
- description="Describe what the agent is currently doing and express the agent's internal thoughts to the user. Use this when you need to communicate your current actions or internal reasoning to the user, not for general completion.",
18
+ description="Describe what the agent is currently doing and express the agent's **internal ideas** to the user."
19
+ + " Use this when you need to communicate your current actions or internal ideas to the user, **not** for general completion.",
19
20
  parameters=FunctionParametersSchema(
20
21
  type="object",
21
22
  properties={
@@ -115,9 +115,9 @@ class ChatObject:
115
115
  # Timing
116
116
  timestamp: str # Timestamp (for LLM)
117
117
  time: datetime # Time
118
- end_at: datetime | None = None
118
+ end_at: datetime | None
119
119
  last_call: datetime # Last internal function call time
120
- now_calling: str | None = None # currently calling function name
120
+ now_calling: str | None # currently calling function name
121
121
 
122
122
  # Config & Preset
123
123
  config: AmritaConfig # config used in this call
@@ -143,10 +143,10 @@ class ChatObject:
143
143
  extra_usage: UniResponseUsage[int]
144
144
 
145
145
  # Runtime State
146
- _is_running: bool = False # Whether it is running
147
- _is_done: bool = False # Whether it has completed
146
+ _is_running: bool # Whether it is running
147
+ _is_done: bool # Whether it has completed
148
148
  _task: Task[None] # (lateinit) set on runtime
149
- _err: BaseException | None = None # Exception in runtime
149
+ _err: BaseException | None # Exception in runtime
150
150
 
151
151
  # Options
152
152
  _bke_opt: DatabackendOptions
@@ -156,21 +156,53 @@ class ChatObject:
156
156
  _raised_exc: tuple[type[BaseException], ...]
157
157
 
158
158
  # Workflow / Interpreter
159
- _workflow: (
160
- NodeComposeRendered # (lateinit) ChatObject's runtime, will be set in __init__.
161
- )
162
- _interpreter: (
163
- WorkflowInterpreter # (lateinit) When _entry is called, this will be set.
164
- )
159
+ _workflow: NodeComposeRendered
160
+ _interpreter: WorkflowInterpreter
165
161
  _middleware: (
166
162
  Callable[[Self], Awaitable[Any]] | None
167
163
  ) # Middleware for the whole workflow, will be set in __init__.
168
164
 
169
165
  # Agent loop state
170
- _agent_loop: AgentLoopState | None = None
166
+ _agent_loop: AgentLoopState | None
171
167
 
172
168
  # ChatObject temp storage
173
169
  _chatman: ChatManager
170
+ __slots__ = (
171
+ "_agent_loop",
172
+ "_bke_opt",
173
+ "_chatman",
174
+ "_err",
175
+ "_hook_args",
176
+ "_hook_kwargs",
177
+ "_interpreter",
178
+ "_is_done",
179
+ "_is_running",
180
+ "_middleware",
181
+ "_raised_exc",
182
+ "_s_id",
183
+ "_task",
184
+ "_workflow",
185
+ "config",
186
+ "context_wrap",
187
+ "end_at",
188
+ "extra_usage",
189
+ "io_stream",
190
+ "jinja2_vars",
191
+ "last_call",
192
+ "now_calling",
193
+ "preset",
194
+ "response",
195
+ "slot",
196
+ "state",
197
+ "strategy",
198
+ "stream_id",
199
+ "template",
200
+ "time",
201
+ "timestamp",
202
+ "train",
203
+ "user_input",
204
+ "user_message",
205
+ )
174
206
 
175
207
  def __init__(
176
208
  self,
@@ -226,6 +258,13 @@ class ChatObject:
226
258
  backend_options: Fine-grained control over which backend
227
259
  fetch/commit operations are performed.
228
260
  """
261
+ # Init as None in slots
262
+ self._agent_loop = None
263
+ self._err = None
264
+ self._is_done = False
265
+ self._is_running = False
266
+ self.now_calling = None
267
+ self.end_at = None
229
268
  # Special flags
230
269
  self._raised_exc = (
231
270
  exception_ignored if not __flags__.DISABLE_EXC_IGNORED else ()
@@ -530,6 +569,8 @@ async def _load_state(chat_obj: ChatObject):
530
569
  )
531
570
  if not (opt.skip_memory_fetch):
532
571
  chat_obj.state.memory = await slot.memory.load_memory(chat_obj.state.session_id)
572
+ if not hasattr(chat_obj, "preset"):
573
+ chat_obj.preset = chat_obj.state.ability.presets.get_default_preset()
533
574
 
534
575
 
535
576
  @Node(SuspendEnum.TRAIN_RENDER)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amrita_core
3
- Version: 0.10.0.dev2
3
+ Version: 0.10.2
4
4
  Summary: High performance, flexible, lightweight agent framework.
5
5
  Project-URL: Homepage, https://github.com/AmritaBot/AmritaCore
6
6
  Project-URL: Source, https://github.com/AmritaBot/AmritaCore
File without changes