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.
- abstractagent/adapters/__init__.py +2 -1
- abstractagent/adapters/codeact_runtime.py +907 -60
- abstractagent/adapters/generation_params.py +82 -0
- abstractagent/adapters/media.py +45 -0
- abstractagent/adapters/memact_runtime.py +959 -0
- abstractagent/adapters/react_runtime.py +1357 -135
- abstractagent/agents/__init__.py +4 -0
- abstractagent/agents/base.py +89 -1
- abstractagent/agents/codeact.py +125 -18
- abstractagent/agents/memact.py +280 -0
- abstractagent/agents/react.py +129 -18
- abstractagent/logic/__init__.py +2 -0
- abstractagent/logic/builtins.py +270 -5
- abstractagent/logic/codeact.py +91 -81
- abstractagent/logic/memact.py +128 -0
- abstractagent/logic/react.py +91 -50
- abstractagent/repl.py +24 -447
- abstractagent/scripts/__init__.py +5 -0
- abstractagent/scripts/lmstudio_tool_eval.py +426 -0
- abstractagent/tools/__init__.py +9 -0
- abstractagent-0.3.1.dist-info/METADATA +112 -0
- abstractagent-0.3.1.dist-info/RECORD +33 -0
- {abstractagent-0.2.0.dist-info → abstractagent-0.3.1.dist-info}/WHEEL +1 -1
- abstractagent/ui/__init__.py +0 -5
- abstractagent/ui/question.py +0 -197
- abstractagent-0.2.0.dist-info/METADATA +0 -134
- abstractagent-0.2.0.dist-info/RECORD +0 -28
- {abstractagent-0.2.0.dist-info → abstractagent-0.3.1.dist-info}/entry_points.txt +0 -0
- {abstractagent-0.2.0.dist-info → abstractagent-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {abstractagent-0.2.0.dist-info → abstractagent-0.3.1.dist-info}/top_level.txt +0 -0
abstractagent/logic/codeact.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"""CodeAct logic (pure; no runtime imports).
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
142
|
-
|
|
143
|
-
return f"[{name}]: {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}"
|
abstractagent/logic/react.py
CHANGED
|
@@ -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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
80
|
+
# Output token cap (provider max_tokens) comes from `_limits.max_output_tokens`.
|
|
56
81
|
limits = (vars or {}).get("_limits", {})
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
123
|
+
system_prompt = f"{system_prompt}\n\nGuidance:\n{guidance}".strip()
|
|
87
124
|
|
|
88
|
-
|
|
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
|
-
|