abstractagent 0.2.0__py3-none-any.whl → 0.3.1__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.
@@ -1,7 +1,11 @@
1
1
  """CodeAct logic (pure; no runtime imports).
2
2
 
3
- CodeAct is a ReAct-like loop where the main action is executing Python code
4
- instead of calling many specialized tools.
3
+ This module implements a conventional CodeAct loop:
4
+ - the model primarily acts by producing Python code (or calling execute_python)
5
+ - tool results are appended to chat history
6
+ - the model iterates until it can answer directly
7
+
8
+ CodeAct is intentionally *not* a memory-enhanced agent.
5
9
  """
6
10
 
7
11
  from __future__ import annotations
@@ -26,7 +30,6 @@ class CodeActLogic:
26
30
  ):
27
31
  self._tools = list(tools)
28
32
  self._max_history_messages = int(max_history_messages)
29
- # -1 means unlimited (send all messages), otherwise must be >= 1
30
33
  if self._max_history_messages != -1 and self._max_history_messages < 1:
31
34
  self._max_history_messages = 1
32
35
  self._max_tokens = max_tokens
@@ -35,6 +38,23 @@ class CodeActLogic:
35
38
  def tools(self) -> List[ToolDefinition]:
36
39
  return list(self._tools)
37
40
 
41
+ def add_tools(self, tools: List[ToolDefinition]) -> int:
42
+ if not isinstance(tools, list) or not tools:
43
+ return 0
44
+
45
+ existing = {str(t.name) for t in self._tools if getattr(t, "name", None)}
46
+ added = 0
47
+ for t in tools:
48
+ name = getattr(t, "name", None)
49
+ if not isinstance(name, str) or not name.strip():
50
+ continue
51
+ if name in existing:
52
+ continue
53
+ self._tools.append(t)
54
+ existing.add(name)
55
+ added += 1
56
+ return added
57
+
38
58
  def build_request(
39
59
  self,
40
60
  *,
@@ -45,58 +65,74 @@ class CodeActLogic:
45
65
  max_iterations: int = 20,
46
66
  vars: Optional[Dict[str, Any]] = None,
47
67
  ) -> LLMRequest:
48
- """Build an LLM request for the CodeAct agent.
49
-
50
- Args:
51
- task: The task to perform
52
- messages: Conversation history
53
- guidance: Optional guidance text to inject
54
- iteration: Current iteration number
55
- max_iterations: Maximum allowed iterations
56
- vars: Optional run.vars dict. If provided, limits are read from
57
- vars["_limits"] (canonical) with fallback to instance defaults.
58
- """
68
+ _ = messages # history is carried out-of-band via chat messages
69
+
59
70
  task = str(task or "")
60
71
  guidance = str(guidance or "").strip()
61
72
 
62
- # Get limits from vars if available, else use instance defaults
63
73
  limits = (vars or {}).get("_limits", {})
64
- max_history = int(limits.get("max_history_messages", self._max_history_messages) or self._max_history_messages)
65
- max_tokens = limits.get("max_tokens", self._max_tokens)
66
- if max_tokens is not None:
67
- max_tokens = int(max_tokens)
68
-
69
- # -1 means unlimited (use all messages)
70
- if max_history == -1:
71
- history = messages if messages else []
72
- else:
73
- history = messages[-max_history:] if messages else []
74
- history_text = "\n".join(
75
- [f"{m.get('role', 'unknown')}: {m.get('content', '')}" for m in history]
76
- )
77
-
78
- prompt = (
79
- "You are CodeAct: you can solve tasks by writing and executing Python code.\n"
80
- "Use the tool `execute_python` to run Python snippets. Prefer small, focused scripts.\n"
81
- "Print any intermediate results you need.\n"
82
- "When you are confident, provide the final answer without calling tools.\n\n"
74
+ max_output_tokens = limits.get("max_output_tokens", None)
75
+ if max_output_tokens is not None:
76
+ try:
77
+ max_output_tokens = int(max_output_tokens)
78
+ except Exception:
79
+ max_output_tokens = None
80
+
81
+ runtime_ns = (vars or {}).get("_runtime", {})
82
+ scratchpad = (vars or {}).get("scratchpad", {})
83
+ plan_mode = bool(runtime_ns.get("plan_mode")) if isinstance(runtime_ns, dict) else False
84
+ plan_text = scratchpad.get("plan") if isinstance(scratchpad, dict) else None
85
+ plan = str(plan_text).strip() if isinstance(plan_text, str) and plan_text.strip() else ""
86
+
87
+ prompt = task.strip()
88
+
89
+ output_budget_line = ""
90
+ if isinstance(max_output_tokens, int) and max_output_tokens > 0:
91
+ output_budget_line = f"- Output token limit for this response: {max_output_tokens}.\n"
92
+
93
+ system_prompt = (
83
94
  f"Iteration: {int(iteration)}/{int(max_iterations)}\n\n"
84
- f"Task: {task}\n\n"
85
- )
86
- if history_text:
87
- prompt += f"History:\n{history_text}\n\n"
95
+ "You are CodeAct: you solve tasks by writing and executing Python when needed.\n\n"
96
+ "Evidence & action (IMPORTANT):\n"
97
+ "- Be truthful: only claim actions supported by tool outputs.\n"
98
+ "- If the task requires code execution or file edits, do it now (call a tool or output a fenced ```python``` block).\n"
99
+ "- Do not “announce” actions without executing them.\n\n"
100
+ "Rules:\n"
101
+ "- Be truthful: only claim actions supported by tool outputs.\n"
102
+ "- Be autonomous: do not ask the user for confirmation to proceed; keep going until the task is done.\n"
103
+ "- If you need to run code, call `execute_python` (preferred) or output a fenced ```python code block.\n"
104
+ "- Efficiency: batch independent read-only tool calls into a single turn (multiple tool calls) to reduce iterations.\n"
105
+ " Examples: read_file for multiple files/ranges, search_files with different queries, list_files across folders, analyze_code on multiple targets.\n"
106
+ " Only split tool calls across turns when later calls depend on earlier outputs; avoid batching side-effectful tools (write/edit/execute).\n"
107
+ "- When context is getting large, use delegate_agent(task, context, tools) to offload an independent subtask with minimal context.\n"
108
+ "- Never fabricate tool outputs.\n"
109
+ "- Only ask the user a question when required information is missing.\n"
110
+ f"{output_budget_line}"
111
+ ).strip()
88
112
 
89
113
  if guidance:
90
- prompt += f"[User guidance]: {guidance}\n\n"
91
-
92
- prompt += (
93
- "If you need to run code, either:\n"
94
- "- Call `execute_python` with the Python code, or\n"
95
- "- If tool calling is unavailable, include a fenced ```python code block.\n"
114
+ system_prompt = (system_prompt + "\n\nGuidance:\n" + guidance).strip()
115
+
116
+ if plan_mode and plan:
117
+ system_prompt = (system_prompt + "\n\nCurrent plan:\n" + plan).strip()
118
+
119
+ if plan_mode:
120
+ system_prompt = (
121
+ system_prompt
122
+ + "\n\nPlan mode:\n"
123
+ "- Maintain and update the plan as you work.\n"
124
+ "- If the plan changes, include a final section at the END of your message:\n"
125
+ " Plan Update:\n"
126
+ " <markdown checklist>\n"
127
+ ).strip()
128
+
129
+ return LLMRequest(
130
+ prompt=prompt,
131
+ system_prompt=system_prompt,
132
+ tools=self.tools,
133
+ max_tokens=max_output_tokens,
96
134
  )
97
135
 
98
- return LLMRequest(prompt=prompt, tools=self.tools, max_tokens=max_tokens)
99
-
100
136
  def parse_response(self, response: Any) -> Tuple[str, List[ToolCall]]:
101
137
  if not isinstance(response, dict):
102
138
  return "", []
@@ -104,6 +140,11 @@ class CodeActLogic:
104
140
  content = response.get("content")
105
141
  content = "" if content is None else str(content)
106
142
 
143
+ if not content.strip():
144
+ reasoning = response.get("reasoning")
145
+ if isinstance(reasoning, str) and reasoning.strip():
146
+ content = reasoning.strip()
147
+
107
148
  tool_calls_raw = response.get("tool_calls") or []
108
149
  tool_calls: List[ToolCall] = []
109
150
  if isinstance(tool_calls_raw, list):
@@ -118,15 +159,6 @@ class CodeActLogic:
118
159
  if isinstance(args, dict):
119
160
  tool_calls.append(ToolCall(name=name, arguments=dict(args), call_id=call_id))
120
161
 
121
- # FALLBACK: Parse from content if no native tool calls
122
- # Handles <|tool_call|>, <function_call>, ```tool_code, etc.
123
- if not tool_calls and content:
124
- from abstractcore.tools.parser import parse_tool_calls, detect_tool_calls
125
- if detect_tool_calls(content):
126
- # Pass model name for architecture-specific parsing
127
- model_name = response.get("model")
128
- tool_calls = parse_tool_calls(content, model_name=model_name)
129
-
130
162
  return content, tool_calls
131
163
 
132
164
  def extract_code(self, text: str) -> str | None:
@@ -138,29 +170,7 @@ class CodeActLogic:
138
170
  return code.strip() or None
139
171
 
140
172
  def format_observation(self, *, name: str, output: Any, success: bool) -> str:
141
- if name != "execute_python":
142
- out = "" if output is None else str(output)
143
- return f"[{name}]: {out}" if success else f"[{name}]: Error: {out}"
144
-
145
- if not isinstance(output, dict):
146
- out = "" if output is None else str(output)
147
- return f"[execute_python]: {out}" if success else f"[execute_python]: Error: {out}"
148
-
149
- stdout = str(output.get("stdout") or "")
150
- stderr = str(output.get("stderr") or "")
151
- exit_code = output.get("exit_code")
152
- error = output.get("error")
153
-
154
- parts: List[str] = []
155
- if error:
156
- parts.append(f"error={error}")
157
- if exit_code is not None:
158
- parts.append(f"exit_code={exit_code}")
159
- if stdout:
160
- parts.append("stdout:\n" + stdout)
161
- if stderr:
162
- parts.append("stderr:\n" + stderr)
163
-
164
- rendered = "\n".join(parts).strip() or "(no output)"
165
- return f"[execute_python]: {rendered}"
166
-
173
+ out = "" if output is None else str(output)
174
+ if success:
175
+ return f"[{name}]: {out}"
176
+ return f"[{name}]: Error: {out}"
@@ -0,0 +1,128 @@
1
+ """MemAct logic (pure; no runtime imports).
2
+
3
+ MemAct is a memory-enhanced agent (Letta-like) that relies on a separate, runtime-owned
4
+ Active Memory system. This logic layer stays conventional:
5
+ - tool calling is the only way to have an effect
6
+ - tool results are appended to chat history by the runtime adapter
7
+
8
+ The memory system is injected by the MemAct runtime adapter via the system prompt and
9
+ updated via a structured JSON envelope at finalization.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any, Dict, List, Optional, Tuple
15
+
16
+ from abstractcore.tools import ToolCall, ToolDefinition
17
+
18
+ from .types import LLMRequest
19
+
20
+
21
+ class MemActLogic:
22
+ def __init__(
23
+ self,
24
+ *,
25
+ tools: List[ToolDefinition],
26
+ max_history_messages: int = -1,
27
+ max_tokens: Optional[int] = None,
28
+ ):
29
+ self._tools = list(tools)
30
+ self._max_history_messages = int(max_history_messages)
31
+ if self._max_history_messages != -1 and self._max_history_messages < 1:
32
+ self._max_history_messages = 1
33
+ self._max_tokens = max_tokens
34
+
35
+ @property
36
+ def tools(self) -> List[ToolDefinition]:
37
+ return list(self._tools)
38
+
39
+ def build_request(
40
+ self,
41
+ *,
42
+ task: str,
43
+ messages: List[Dict[str, Any]],
44
+ guidance: str = "",
45
+ iteration: int = 1,
46
+ max_iterations: int = 20,
47
+ vars: Optional[Dict[str, Any]] = None,
48
+ ) -> LLMRequest:
49
+ """Build a base LLM request (adapter injects memory blocks separately)."""
50
+ _ = messages # history is carried via chat messages by the adapter
51
+
52
+ task = str(task or "").strip()
53
+ guidance = str(guidance or "").strip()
54
+
55
+ limits = (vars or {}).get("_limits", {})
56
+ max_output_tokens = limits.get("max_output_tokens", None)
57
+ if max_output_tokens is not None:
58
+ try:
59
+ max_output_tokens = int(max_output_tokens)
60
+ except Exception:
61
+ max_output_tokens = None
62
+
63
+ output_budget_line = ""
64
+ if isinstance(max_output_tokens, int) and max_output_tokens > 0:
65
+ output_budget_line = f"- Output token limit for this response: {max_output_tokens}.\n"
66
+
67
+ system_prompt = (
68
+ f"Iteration: {int(iteration)}/{int(max_iterations)}\n\n"
69
+ "You are an autonomous MemAct agent.\n"
70
+ "Taking action / having an effect means calling a tool.\n\n"
71
+ "Rules:\n"
72
+ "- Be truthful: only claim actions supported by tool outputs.\n"
73
+ "- Be autonomous: do not ask the user for confirmation to proceed; keep going until the task is done.\n"
74
+ "- If you need to create/edit files, run commands, fetch URLs, or search, you MUST call an appropriate tool.\n"
75
+ "- Efficiency: batch independent read-only tool calls into a single turn (multiple tool calls) when possible.\n"
76
+ "- When context is getting large, use delegate_agent(task, context, tools) to offload an independent subtask with minimal context.\n"
77
+ "- Never fabricate tool outputs.\n"
78
+ "- Only ask the user a question when required information is missing.\n"
79
+ f"{output_budget_line}"
80
+ ).strip()
81
+
82
+ if guidance:
83
+ system_prompt = (system_prompt + "\n\nGuidance:\n" + guidance).strip()
84
+
85
+ return LLMRequest(
86
+ prompt=task,
87
+ system_prompt=system_prompt,
88
+ tools=self.tools,
89
+ max_tokens=max_output_tokens,
90
+ )
91
+
92
+ def parse_response(self, response: Any) -> Tuple[str, List[ToolCall]]:
93
+ if not isinstance(response, dict):
94
+ return "", []
95
+
96
+ content = response.get("content")
97
+ content = "" if content is None else str(content)
98
+ content = content.lstrip()
99
+ for prefix in ("assistant:", "assistant:"):
100
+ if content.lower().startswith(prefix):
101
+ content = content[len(prefix) :].lstrip()
102
+ break
103
+
104
+ if not content.strip():
105
+ reasoning = response.get("reasoning")
106
+ if isinstance(reasoning, str) and reasoning.strip():
107
+ content = reasoning.strip()
108
+
109
+ tool_calls_raw = response.get("tool_calls") or []
110
+ tool_calls: List[ToolCall] = []
111
+ if isinstance(tool_calls_raw, list):
112
+ for tc in tool_calls_raw:
113
+ if isinstance(tc, ToolCall):
114
+ tool_calls.append(tc)
115
+ continue
116
+ if isinstance(tc, dict):
117
+ name = str(tc.get("name", "") or "")
118
+ args = tc.get("arguments", {})
119
+ call_id = tc.get("call_id")
120
+ if isinstance(args, dict):
121
+ tool_calls.append(ToolCall(name=name, arguments=dict(args), call_id=call_id))
122
+
123
+ return content, tool_calls
124
+
125
+ def format_observation(self, *, name: str, output: str, success: bool) -> str:
126
+ if success:
127
+ return f"[{name}]: {output}"
128
+ return f"[{name}]: Error: {output}"
@@ -1,4 +1,13 @@
1
- """ReAct logic (pure; no runtime imports)."""
1
+ """ReAct logic (pure; no runtime imports).
2
+
3
+ This module implements the classic ReAct loop:
4
+ - the model decides whether to call tools
5
+ - tool results are appended to chat history
6
+ - the model iterates until it can answer directly
7
+
8
+ ReAct is intentionally *not* a memory-enhanced agent. Long-term memory and
9
+ structured memory blocks belong in a separate agent (MemAct).
10
+ """
2
11
 
3
12
  from __future__ import annotations
4
13
 
@@ -28,6 +37,24 @@ class ReActLogic:
28
37
  def tools(self) -> List[ToolDefinition]:
29
38
  return list(self._tools)
30
39
 
40
+ def add_tools(self, tools: List[ToolDefinition]) -> int:
41
+ """Add tool definitions to this logic instance (deduped by name)."""
42
+ if not isinstance(tools, list) or not tools:
43
+ return 0
44
+
45
+ existing = {str(t.name) for t in self._tools if getattr(t, "name", None)}
46
+ added = 0
47
+ for t in tools:
48
+ name = getattr(t, "name", None)
49
+ if not isinstance(name, str) or not name.strip():
50
+ continue
51
+ if name in existing:
52
+ continue
53
+ self._tools.append(t)
54
+ existing.add(name)
55
+ added += 1
56
+ return added
57
+
31
58
  def build_request(
32
59
  self,
33
60
  *,
@@ -40,52 +67,63 @@ class ReActLogic:
40
67
  ) -> LLMRequest:
41
68
  """Build an LLM request for the ReAct agent.
42
69
 
43
- Args:
44
- task: The task to perform
45
- messages: Conversation history
46
- guidance: Optional guidance text to inject
47
- iteration: Current iteration number
48
- max_iterations: Maximum allowed iterations
49
- vars: Optional run.vars dict. If provided, limits are read from
50
- vars["_limits"] (canonical) with fallback to instance defaults.
70
+ Notes:
71
+ - The user request belongs in the user-role message (prompt), not in the system prompt.
72
+ - Conversation + tool history is provided via `messages` by the runtime adapter.
51
73
  """
52
- task = str(task or "")
74
+ # History is carried out-of-band via `messages`; keep logic pure.
75
+ _ = messages
76
+
77
+ task = str(task or "").strip()
53
78
  guidance = str(guidance or "").strip()
54
79
 
55
- # Get limits from vars if available, else use instance defaults
80
+ # Output token cap (provider max_tokens) comes from `_limits.max_output_tokens`.
56
81
  limits = (vars or {}).get("_limits", {})
57
- max_history = int(limits.get("max_history_messages", self._max_history_messages) or self._max_history_messages)
58
- max_tokens = limits.get("max_tokens", self._max_tokens)
59
- if max_tokens is not None:
60
- max_tokens = int(max_tokens)
61
-
62
- if len(messages) <= 1:
63
- prompt = (
64
- f"Task: {task}\n\n"
65
- "Use the available tools to complete this task. When done, provide your final answer."
66
- )
67
- else:
68
- # -1 means unlimited (use all messages)
69
- if max_history == -1:
70
- history = messages
71
- else:
72
- history = messages[-max_history:]
73
- history_text = "\n".join(
74
- [f"{m.get('role', 'unknown')}: {m.get('content', '')}" for m in history]
75
- )
76
- prompt = (
77
- "You have access to the conversation history below as context.\n"
78
- "Do not claim you have no memory of it; it is provided to you here.\n\n"
79
- f"Iteration: {int(iteration)}/{int(max_iterations)}\n\n"
80
- f"History:\n{history_text}\n\n"
81
- "Continue the conversation and work on the user's latest request.\n"
82
- "Use tools when needed, or provide a final answer."
83
- )
82
+ max_output_tokens = limits.get("max_output_tokens", None)
83
+ if max_output_tokens is not None:
84
+ try:
85
+ max_output_tokens = int(max_output_tokens)
86
+ except Exception:
87
+ max_output_tokens = None
88
+ if not isinstance(max_output_tokens, int) or max_output_tokens <= 0:
89
+ max_output_tokens = None
90
+
91
+ system_prompt = (
92
+ f"Iteration: {int(iteration)}/{int(max_iterations)}\n\n"
93
+ "## MY PERSONA\n"
94
+ "You are an autonomous ReAct agent (Reason → Act → Observe).\n\n"
95
+ "Loop contract:\n"
96
+ "- THINK briefly using the full transcript and prior observations.\n"
97
+ "- If you need to ACT, CALL one or more tools (function calls).\n"
98
+ "- If you are DONE, respond with the final answer and NO tool calls.\n\n"
99
+ "Rules:\n"
100
+ "- Choose tools yourself; never ask the user which tool to run.\n"
101
+ "- Do not write a long plan before tool calls.\n"
102
+ "- Keep non-final responses short; do not draft large deliverables in chat when tools can build them.\n"
103
+ "- Efficiency (important): the runtime supports MULTIPLE tool calls in one response.\n"
104
+ " Batch independent read-only tool calls to reduce iterations.\n"
105
+ " Example: read multiple files/ranges or run multiple searches in one response.\n"
106
+ " If reading nearby ranges of the same file, prefer ONE call with a wider range.\n"
107
+ " Only split tool calls across turns when later calls depend on earlier outputs; do NOT batch side-effectful tools (write_file/edit_file/execute_command/send_email/send_whatsapp_message/send_telegram_message/send_telegram_artifact).\n"
108
+ "- When context is getting large, use delegate_agent(task, context, tools) to offload an independent subtask with minimal context.\n"
109
+ "- Keep tool call arguments small and valid; avoid embedding huge blobs (large file contents / giant JSON) directly in arguments.\n"
110
+ "- Attachments:\n"
111
+ " - If you see an 'Active attachments' message or inline 'Content from <file>' blocks, treat those attachments as already available in-context.\n"
112
+ " Do NOT call tools just to re-open/read them.\n"
113
+ " - If you see 'Stored session attachments', those may not be included in the current call.\n"
114
+ " Only if you truly need it, use the attachment-open tool with artifact_id and a bounded line range.\n"
115
+ " - Never use filesystem tools on attachment filenames/paths or absolute paths outside the workspace.\n"
116
+ "- For fetch_url: use include_full_content=False for shorter previews; set keep_links=False to strip links when not needed.\n"
117
+ "- For large files, create a small skeleton first, then refine via multiple smaller edits/tool calls.\n"
118
+ "- Use tool outputs as evidence; do not claim actions without tool outputs.\n"
119
+ "- Continue iterating until the task is complete.\n"
120
+ ).strip()
84
121
 
85
122
  if guidance:
86
- prompt += "\n\n[User guidance]: " + guidance
123
+ system_prompt = f"{system_prompt}\n\nGuidance:\n{guidance}".strip()
87
124
 
88
- return LLMRequest(prompt=prompt, tools=self.tools, max_tokens=max_tokens)
125
+ # Note: prompt is unused by the runtime adapter (we supply chat `messages`).
126
+ return LLMRequest(prompt=task, system_prompt=system_prompt, tools=self.tools, max_tokens=max_output_tokens)
89
127
 
90
128
  def parse_response(self, response: Any) -> Tuple[str, List[ToolCall]]:
91
129
  if not isinstance(response, dict):
@@ -93,6 +131,19 @@ class ReActLogic:
93
131
 
94
132
  content = response.get("content")
95
133
  content = "" if content is None else str(content)
134
+ # Some OSS models echo role labels; strip common prefixes to keep UI/history clean.
135
+ content = content.lstrip()
136
+ for prefix in ("assistant:", "assistant:"):
137
+ if content.lower().startswith(prefix):
138
+ content = content[len(prefix) :].lstrip()
139
+ break
140
+
141
+ # Some providers return a separate `reasoning` field. If content is empty, fall back
142
+ # to reasoning so iterative loops don't lose context.
143
+ if not content.strip():
144
+ reasoning = response.get("reasoning")
145
+ if isinstance(reasoning, str) and reasoning.strip():
146
+ content = reasoning.strip()
96
147
 
97
148
  tool_calls_raw = response.get("tool_calls") or []
98
149
  tool_calls: List[ToolCall] = []
@@ -108,19 +159,9 @@ class ReActLogic:
108
159
  if isinstance(args, dict):
109
160
  tool_calls.append(ToolCall(name=name, arguments=dict(args), call_id=call_id))
110
161
 
111
- # FALLBACK: Parse from content if no native tool calls
112
- # Handles <|tool_call|>, <function_call>, ```tool_code, etc.
113
- if not tool_calls and content:
114
- from abstractcore.tools.parser import parse_tool_calls, detect_tool_calls
115
- if detect_tool_calls(content):
116
- # Pass model name for architecture-specific parsing
117
- model_name = response.get("model")
118
- tool_calls = parse_tool_calls(content, model_name=model_name)
119
-
120
162
  return content, tool_calls
121
163
 
122
164
  def format_observation(self, *, name: str, output: str, success: bool) -> str:
123
165
  if success:
124
166
  return f"[{name}]: {output}"
125
167
  return f"[{name}]: Error: {output}"
126
-