abstractagent 0.2.0__py3-none-any.whl → 0.3.0__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 +823 -57
- abstractagent/adapters/memact_runtime.py +721 -0
- abstractagent/adapters/react_runtime.py +1114 -67
- abstractagent/agents/__init__.py +4 -0
- abstractagent/agents/base.py +58 -1
- abstractagent/agents/codeact.py +89 -18
- abstractagent/agents/memact.py +244 -0
- abstractagent/agents/react.py +91 -18
- abstractagent/logic/__init__.py +2 -0
- abstractagent/logic/builtins.py +212 -5
- abstractagent/logic/codeact.py +87 -80
- abstractagent/logic/memact.py +127 -0
- abstractagent/logic/react.py +108 -48
- abstractagent/repl.py +24 -447
- abstractagent/scripts/__init__.py +5 -0
- abstractagent/scripts/lmstudio_tool_eval.py +426 -0
- abstractagent/tools/__init__.py +3 -0
- {abstractagent-0.2.0.dist-info → abstractagent-0.3.0.dist-info}/METADATA +10 -11
- abstractagent-0.3.0.dist-info/RECORD +31 -0
- abstractagent/ui/__init__.py +0 -5
- abstractagent/ui/question.py +0 -197
- abstractagent-0.2.0.dist-info/RECORD +0 -28
- {abstractagent-0.2.0.dist-info → abstractagent-0.3.0.dist-info}/WHEEL +0 -0
- {abstractagent-0.2.0.dist-info → abstractagent-0.3.0.dist-info}/entry_points.txt +0 -0
- {abstractagent-0.2.0.dist-info → abstractagent-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {abstractagent-0.2.0.dist-info → abstractagent-0.3.0.dist-info}/top_level.txt +0 -0
abstractagent/agents/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from .base import BaseAgent
|
|
4
4
|
from .react import ReactAgent, create_react_workflow, create_react_agent
|
|
5
5
|
from .codeact import CodeActAgent, create_codeact_workflow, create_codeact_agent
|
|
6
|
+
from .memact import MemActAgent, create_memact_workflow, create_memact_agent
|
|
6
7
|
|
|
7
8
|
__all__ = [
|
|
8
9
|
"BaseAgent",
|
|
@@ -12,4 +13,7 @@ __all__ = [
|
|
|
12
13
|
"CodeActAgent",
|
|
13
14
|
"create_codeact_workflow",
|
|
14
15
|
"create_codeact_agent",
|
|
16
|
+
"MemActAgent",
|
|
17
|
+
"create_memact_workflow",
|
|
18
|
+
"create_memact_agent",
|
|
15
19
|
]
|
abstractagent/agents/base.py
CHANGED
|
@@ -54,6 +54,22 @@ class BaseAgent(ABC):
|
|
|
54
54
|
self.session_id: Optional[str] = session_id
|
|
55
55
|
self.session_messages: List[Dict[str, Any]] = []
|
|
56
56
|
|
|
57
|
+
def _sync_session_caches_from_state(self, state: Optional[RunState]) -> None:
|
|
58
|
+
if state is None or not hasattr(state, "vars") or not isinstance(state.vars, dict):
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
messages: Optional[List[Dict[str, Any]]] = None
|
|
62
|
+
output = getattr(state, "output", None)
|
|
63
|
+
if isinstance(output, dict) and isinstance(output.get("messages"), list):
|
|
64
|
+
messages = [dict(m) for m in output["messages"] if isinstance(m, dict)]
|
|
65
|
+
else:
|
|
66
|
+
ctx = state.vars.get("context")
|
|
67
|
+
if isinstance(ctx, dict) and isinstance(ctx.get("messages"), list):
|
|
68
|
+
messages = [dict(m) for m in ctx["messages"] if isinstance(m, dict)]
|
|
69
|
+
|
|
70
|
+
if messages is not None:
|
|
71
|
+
self.session_messages = list(messages)
|
|
72
|
+
|
|
57
73
|
def _ensure_actor_id(self) -> str:
|
|
58
74
|
if self.actor_id:
|
|
59
75
|
return self.actor_id
|
|
@@ -135,6 +151,43 @@ class BaseAgent(ABC):
|
|
|
135
151
|
if not self._current_run_id:
|
|
136
152
|
return None
|
|
137
153
|
return self.runtime.get_state(self._current_run_id)
|
|
154
|
+
|
|
155
|
+
def get_context(self) -> Dict[str, Any]:
|
|
156
|
+
"""Get the agent's current context namespace (runtime-owned persisted state)."""
|
|
157
|
+
state = self.get_state()
|
|
158
|
+
ctx = state.vars.get("context") if state and hasattr(state, "vars") else None
|
|
159
|
+
return dict(ctx) if isinstance(ctx, dict) else {}
|
|
160
|
+
|
|
161
|
+
def get_scratchpad(self) -> Dict[str, Any]:
|
|
162
|
+
"""Get the agent's current scratchpad namespace (agent-owned schema, runtime-owned storage)."""
|
|
163
|
+
state = self.get_state()
|
|
164
|
+
scratch = state.vars.get("scratchpad") if state and hasattr(state, "vars") else None
|
|
165
|
+
return dict(scratch) if isinstance(scratch, dict) else {}
|
|
166
|
+
|
|
167
|
+
def get_node_traces(self) -> Dict[str, Any]:
|
|
168
|
+
"""Get runtime-owned per-node traces for the current run (passthrough to Runtime)."""
|
|
169
|
+
if not self._current_run_id:
|
|
170
|
+
return {}
|
|
171
|
+
getter = getattr(self.runtime, "get_node_traces", None)
|
|
172
|
+
if callable(getter):
|
|
173
|
+
return getter(self._current_run_id)
|
|
174
|
+
state = self.get_state()
|
|
175
|
+
runtime_ns = state.vars.get("_runtime") if state and hasattr(state, "vars") else None
|
|
176
|
+
traces = runtime_ns.get("node_traces") if isinstance(runtime_ns, dict) else None
|
|
177
|
+
return dict(traces) if isinstance(traces, dict) else {}
|
|
178
|
+
|
|
179
|
+
def get_node_trace(self, node_id: str) -> Dict[str, Any]:
|
|
180
|
+
"""Get a single runtime-owned node trace for the current run."""
|
|
181
|
+
if not self._current_run_id:
|
|
182
|
+
return {"node_id": node_id, "steps": []}
|
|
183
|
+
getter = getattr(self.runtime, "get_node_trace", None)
|
|
184
|
+
if callable(getter):
|
|
185
|
+
return getter(self._current_run_id, node_id)
|
|
186
|
+
traces = self.get_node_traces()
|
|
187
|
+
trace = traces.get(node_id)
|
|
188
|
+
if isinstance(trace, dict):
|
|
189
|
+
return trace
|
|
190
|
+
return {"node_id": node_id, "steps": []}
|
|
138
191
|
|
|
139
192
|
def is_waiting(self) -> bool:
|
|
140
193
|
"""Check if agent is waiting for input.
|
|
@@ -199,12 +252,15 @@ class BaseAgent(ABC):
|
|
|
199
252
|
|
|
200
253
|
wait_key = state.waiting.wait_key if state.waiting else None
|
|
201
254
|
|
|
202
|
-
|
|
255
|
+
state2 = self.runtime.resume(
|
|
203
256
|
workflow=self.workflow,
|
|
204
257
|
run_id=self._current_run_id,
|
|
205
258
|
wait_key=wait_key,
|
|
206
259
|
payload={"response": response},
|
|
207
260
|
)
|
|
261
|
+
if state2.status in (RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED):
|
|
262
|
+
self._sync_session_caches_from_state(state2)
|
|
263
|
+
return state2
|
|
208
264
|
|
|
209
265
|
def attach(self, run_id: str) -> RunState:
|
|
210
266
|
"""Attach to an existing run for resume.
|
|
@@ -238,6 +294,7 @@ class BaseAgent(ABC):
|
|
|
238
294
|
self.session_id = state_session_id
|
|
239
295
|
|
|
240
296
|
self._current_run_id = run_id
|
|
297
|
+
self._sync_session_caches_from_state(state)
|
|
241
298
|
return state
|
|
242
299
|
|
|
243
300
|
def save_state(self, filepath: str) -> None:
|
abstractagent/agents/codeact.py
CHANGED
|
@@ -5,11 +5,18 @@ from __future__ import annotations
|
|
|
5
5
|
from typing import Any, Callable, Dict, List, Optional
|
|
6
6
|
|
|
7
7
|
from abstractcore.tools import ToolDefinition
|
|
8
|
-
from abstractruntime import RunState, Runtime, WorkflowSpec
|
|
8
|
+
from abstractruntime import RunState, RunStatus, Runtime, WorkflowSpec
|
|
9
9
|
|
|
10
10
|
from .base import BaseAgent
|
|
11
11
|
from ..adapters.codeact_runtime import create_codeact_workflow
|
|
12
|
-
from ..logic.builtins import
|
|
12
|
+
from ..logic.builtins import (
|
|
13
|
+
ASK_USER_TOOL,
|
|
14
|
+
COMPACT_MEMORY_TOOL,
|
|
15
|
+
INSPECT_VARS_TOOL,
|
|
16
|
+
RECALL_MEMORY_TOOL,
|
|
17
|
+
REMEMBER_TOOL,
|
|
18
|
+
REMEMBER_NOTE_TOOL,
|
|
19
|
+
)
|
|
13
20
|
from ..logic.codeact import CodeActLogic
|
|
14
21
|
|
|
15
22
|
|
|
@@ -44,7 +51,10 @@ class CodeActAgent(BaseAgent):
|
|
|
44
51
|
on_step: Optional[Callable[[str, Dict[str, Any]], None]] = None,
|
|
45
52
|
max_iterations: int = 25,
|
|
46
53
|
max_history_messages: int = -1,
|
|
47
|
-
max_tokens: Optional[int] =
|
|
54
|
+
max_tokens: Optional[int] = None,
|
|
55
|
+
plan_mode: bool = False,
|
|
56
|
+
review_mode: bool = True,
|
|
57
|
+
review_max_rounds: int = 3,
|
|
48
58
|
actor_id: Optional[str] = None,
|
|
49
59
|
session_id: Optional[str] = None,
|
|
50
60
|
):
|
|
@@ -56,6 +66,11 @@ class CodeActAgent(BaseAgent):
|
|
|
56
66
|
if self._max_history_messages != -1 and self._max_history_messages < 1:
|
|
57
67
|
self._max_history_messages = 1
|
|
58
68
|
self._max_tokens = max_tokens
|
|
69
|
+
self._plan_mode = bool(plan_mode)
|
|
70
|
+
self._review_mode = bool(review_mode)
|
|
71
|
+
self._review_max_rounds = int(review_max_rounds)
|
|
72
|
+
if self._review_max_rounds < 0:
|
|
73
|
+
self._review_max_rounds = 0
|
|
59
74
|
|
|
60
75
|
self.logic: Optional[CodeActLogic] = None
|
|
61
76
|
super().__init__(
|
|
@@ -68,7 +83,15 @@ class CodeActAgent(BaseAgent):
|
|
|
68
83
|
|
|
69
84
|
def _create_workflow(self) -> WorkflowSpec:
|
|
70
85
|
tool_defs = _tool_definitions_from_callables(self.tools)
|
|
71
|
-
tool_defs = [
|
|
86
|
+
tool_defs = [
|
|
87
|
+
ASK_USER_TOOL,
|
|
88
|
+
RECALL_MEMORY_TOOL,
|
|
89
|
+
INSPECT_VARS_TOOL,
|
|
90
|
+
REMEMBER_TOOL,
|
|
91
|
+
REMEMBER_NOTE_TOOL,
|
|
92
|
+
COMPACT_MEMORY_TOOL,
|
|
93
|
+
*tool_defs,
|
|
94
|
+
]
|
|
72
95
|
logic = CodeActLogic(
|
|
73
96
|
tools=tool_defs,
|
|
74
97
|
max_history_messages=self._max_history_messages,
|
|
@@ -77,27 +100,67 @@ class CodeActAgent(BaseAgent):
|
|
|
77
100
|
self.logic = logic
|
|
78
101
|
return create_codeact_workflow(logic=logic, on_step=self.on_step)
|
|
79
102
|
|
|
80
|
-
def start(
|
|
103
|
+
def start(
|
|
104
|
+
self,
|
|
105
|
+
task: str,
|
|
106
|
+
*,
|
|
107
|
+
plan_mode: Optional[bool] = None,
|
|
108
|
+
review_mode: Optional[bool] = None,
|
|
109
|
+
review_max_rounds: Optional[int] = None,
|
|
110
|
+
allowed_tools: Optional[List[str]] = None,
|
|
111
|
+
) -> str:
|
|
81
112
|
task = str(task or "").strip()
|
|
82
113
|
if not task:
|
|
83
114
|
raise ValueError("task must be a non-empty string")
|
|
84
115
|
|
|
116
|
+
eff_plan_mode = self._plan_mode if plan_mode is None else bool(plan_mode)
|
|
117
|
+
eff_review_mode = self._review_mode if review_mode is None else bool(review_mode)
|
|
118
|
+
eff_review_max_rounds = self._review_max_rounds if review_max_rounds is None else int(review_max_rounds)
|
|
119
|
+
if eff_review_max_rounds < 0:
|
|
120
|
+
eff_review_max_rounds = 0
|
|
121
|
+
|
|
122
|
+
# Base limits come from the Runtime config so model capabilities (max context)
|
|
123
|
+
# are respected by default, unless explicitly overridden by the agent/session.
|
|
124
|
+
try:
|
|
125
|
+
base_limits = dict(self.runtime.config.to_limits_dict())
|
|
126
|
+
except Exception:
|
|
127
|
+
base_limits = {}
|
|
128
|
+
limits: Dict[str, Any] = dict(base_limits)
|
|
129
|
+
limits.setdefault("warn_iterations_pct", 80)
|
|
130
|
+
limits.setdefault("warn_tokens_pct", 80)
|
|
131
|
+
limits["max_iterations"] = int(self._max_iterations)
|
|
132
|
+
limits["current_iteration"] = 0
|
|
133
|
+
limits["max_history_messages"] = int(self._max_history_messages)
|
|
134
|
+
# Message-size guards for LLM-visible context (character-level).
|
|
135
|
+
# Disabled by default (-1): enable by setting a positive character budget.
|
|
136
|
+
limits.setdefault("max_message_chars", -1)
|
|
137
|
+
limits.setdefault("max_tool_message_chars", -1)
|
|
138
|
+
limits["estimated_tokens_used"] = 0
|
|
139
|
+
try:
|
|
140
|
+
max_tokens_override = int(self._max_tokens) if self._max_tokens is not None else None
|
|
141
|
+
except Exception:
|
|
142
|
+
max_tokens_override = None
|
|
143
|
+
if isinstance(max_tokens_override, int) and max_tokens_override > 0:
|
|
144
|
+
limits["max_tokens"] = max_tokens_override
|
|
145
|
+
if not isinstance(limits.get("max_tokens"), int) or int(limits.get("max_tokens") or 0) <= 0:
|
|
146
|
+
limits["max_tokens"] = 32768
|
|
147
|
+
|
|
85
148
|
vars: Dict[str, Any] = {
|
|
86
149
|
"context": {"task": task, "messages": _copy_messages(self.session_messages)},
|
|
87
150
|
"scratchpad": {"iteration": 0, "max_iterations": int(self._max_iterations)},
|
|
88
|
-
"_runtime": {
|
|
151
|
+
"_runtime": {
|
|
152
|
+
"inbox": [],
|
|
153
|
+
"plan_mode": eff_plan_mode,
|
|
154
|
+
"review_mode": eff_review_mode,
|
|
155
|
+
"review_max_rounds": eff_review_max_rounds,
|
|
156
|
+
},
|
|
89
157
|
"_temp": {},
|
|
90
158
|
# Canonical _limits namespace for runtime awareness
|
|
91
|
-
"_limits":
|
|
92
|
-
"max_iterations": int(self._max_iterations),
|
|
93
|
-
"current_iteration": 0,
|
|
94
|
-
"max_tokens": self._max_tokens,
|
|
95
|
-
"max_history_messages": int(self._max_history_messages),
|
|
96
|
-
"estimated_tokens_used": 0,
|
|
97
|
-
"warn_iterations_pct": 80,
|
|
98
|
-
"warn_tokens_pct": 80,
|
|
99
|
-
},
|
|
159
|
+
"_limits": limits,
|
|
100
160
|
}
|
|
161
|
+
if isinstance(allowed_tools, list):
|
|
162
|
+
normalized = [str(t).strip() for t in allowed_tools if isinstance(t, str) and t.strip()]
|
|
163
|
+
vars["_runtime"]["allowed_tools"] = normalized
|
|
101
164
|
|
|
102
165
|
run_id = self.runtime.start(
|
|
103
166
|
workflow=self.workflow,
|
|
@@ -142,7 +205,10 @@ class CodeActAgent(BaseAgent):
|
|
|
142
205
|
def step(self) -> RunState:
|
|
143
206
|
if not self._current_run_id:
|
|
144
207
|
raise RuntimeError("No active run. Call start() first.")
|
|
145
|
-
|
|
208
|
+
state = self.runtime.tick(workflow=self.workflow, run_id=self._current_run_id, max_steps=1)
|
|
209
|
+
if state.status in (RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED):
|
|
210
|
+
self._sync_session_caches_from_state(state)
|
|
211
|
+
return state
|
|
146
212
|
|
|
147
213
|
|
|
148
214
|
def create_codeact_agent(
|
|
@@ -153,7 +219,10 @@ def create_codeact_agent(
|
|
|
153
219
|
on_step: Optional[Callable[[str, Dict[str, Any]], None]] = None,
|
|
154
220
|
max_iterations: int = 25,
|
|
155
221
|
max_history_messages: int = -1,
|
|
156
|
-
max_tokens: Optional[int] =
|
|
222
|
+
max_tokens: Optional[int] = None,
|
|
223
|
+
plan_mode: bool = False,
|
|
224
|
+
review_mode: bool = True,
|
|
225
|
+
review_max_rounds: int = 3,
|
|
157
226
|
llm_kwargs: Optional[Dict[str, Any]] = None,
|
|
158
227
|
run_store: Optional[Any] = None,
|
|
159
228
|
ledger_store: Optional[Any] = None,
|
|
@@ -185,10 +254,12 @@ def create_codeact_agent(
|
|
|
185
254
|
max_iterations=max_iterations,
|
|
186
255
|
max_history_messages=max_history_messages,
|
|
187
256
|
max_tokens=max_tokens,
|
|
257
|
+
plan_mode=plan_mode,
|
|
258
|
+
review_mode=review_mode,
|
|
259
|
+
review_max_rounds=review_max_rounds,
|
|
188
260
|
actor_id=actor_id,
|
|
189
261
|
session_id=session_id,
|
|
190
262
|
)
|
|
191
263
|
|
|
192
264
|
|
|
193
265
|
__all__ = ["CodeActAgent", "create_codeact_workflow", "create_codeact_agent"]
|
|
194
|
-
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""MemAct agent implementation (memory-enhanced).
|
|
2
|
+
|
|
3
|
+
MemAct is the only agent that uses `abstractruntime.memory.active_memory`.
|
|
4
|
+
ReAct and CodeAct remain conventional SOTA agents.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from abstractcore.tools import ToolDefinition
|
|
13
|
+
from abstractruntime import RunState, RunStatus, Runtime, WorkflowSpec
|
|
14
|
+
|
|
15
|
+
from .base import BaseAgent
|
|
16
|
+
from ..adapters.memact_runtime import create_memact_workflow
|
|
17
|
+
from ..logic.builtins import (
|
|
18
|
+
ASK_USER_TOOL,
|
|
19
|
+
COMPACT_MEMORY_TOOL,
|
|
20
|
+
INSPECT_VARS_TOOL,
|
|
21
|
+
RECALL_MEMORY_TOOL,
|
|
22
|
+
REMEMBER_TOOL,
|
|
23
|
+
REMEMBER_NOTE_TOOL,
|
|
24
|
+
)
|
|
25
|
+
from ..logic.memact import MemActLogic
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _tool_definitions_from_callables(tools: List[Callable[..., Any]]) -> List[ToolDefinition]:
|
|
29
|
+
tool_defs: List[ToolDefinition] = []
|
|
30
|
+
for t in tools:
|
|
31
|
+
tool_def = getattr(t, "_tool_definition", None)
|
|
32
|
+
if tool_def is None:
|
|
33
|
+
tool_def = ToolDefinition.from_function(t)
|
|
34
|
+
tool_defs.append(tool_def)
|
|
35
|
+
return tool_defs
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _copy_messages(messages: Any) -> List[Dict[str, Any]]:
|
|
39
|
+
if not isinstance(messages, list):
|
|
40
|
+
return []
|
|
41
|
+
out: List[Dict[str, Any]] = []
|
|
42
|
+
for m in messages:
|
|
43
|
+
if isinstance(m, dict):
|
|
44
|
+
out.append(dict(m))
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _deepcopy_json(value: Any) -> Any:
|
|
49
|
+
try:
|
|
50
|
+
return json.loads(json.dumps(value))
|
|
51
|
+
except Exception:
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class MemActAgent(BaseAgent):
|
|
56
|
+
"""Memory-enhanced agent with runtime-owned Active Memory blocks."""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
*,
|
|
61
|
+
runtime: Runtime,
|
|
62
|
+
tools: Optional[List[Callable[..., Any]]] = None,
|
|
63
|
+
on_step: Optional[Callable[[str, Dict[str, Any]], None]] = None,
|
|
64
|
+
max_iterations: int = 25,
|
|
65
|
+
max_history_messages: int = -1,
|
|
66
|
+
max_tokens: Optional[int] = None,
|
|
67
|
+
plan_mode: bool = False,
|
|
68
|
+
review_mode: bool = False,
|
|
69
|
+
review_max_rounds: int = 1,
|
|
70
|
+
actor_id: Optional[str] = None,
|
|
71
|
+
session_id: Optional[str] = None,
|
|
72
|
+
):
|
|
73
|
+
self._max_iterations = int(max_iterations)
|
|
74
|
+
if self._max_iterations < 1:
|
|
75
|
+
self._max_iterations = 1
|
|
76
|
+
self._max_history_messages = int(max_history_messages)
|
|
77
|
+
if self._max_history_messages != -1 and self._max_history_messages < 1:
|
|
78
|
+
self._max_history_messages = 1
|
|
79
|
+
self._max_tokens = max_tokens
|
|
80
|
+
self._plan_mode = bool(plan_mode)
|
|
81
|
+
self._review_mode = bool(review_mode)
|
|
82
|
+
self._review_max_rounds = int(review_max_rounds)
|
|
83
|
+
if self._review_max_rounds < 0:
|
|
84
|
+
self._review_max_rounds = 0
|
|
85
|
+
|
|
86
|
+
self.logic: Optional[MemActLogic] = None
|
|
87
|
+
self.session_active_memory: Optional[Dict[str, Any]] = None
|
|
88
|
+
super().__init__(
|
|
89
|
+
runtime=runtime,
|
|
90
|
+
tools=tools,
|
|
91
|
+
on_step=on_step,
|
|
92
|
+
actor_id=actor_id,
|
|
93
|
+
session_id=session_id,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def _create_workflow(self) -> WorkflowSpec:
|
|
97
|
+
tool_defs = _tool_definitions_from_callables(self.tools)
|
|
98
|
+
tool_defs = [
|
|
99
|
+
ASK_USER_TOOL,
|
|
100
|
+
RECALL_MEMORY_TOOL,
|
|
101
|
+
INSPECT_VARS_TOOL,
|
|
102
|
+
REMEMBER_TOOL,
|
|
103
|
+
REMEMBER_NOTE_TOOL,
|
|
104
|
+
COMPACT_MEMORY_TOOL,
|
|
105
|
+
*tool_defs,
|
|
106
|
+
]
|
|
107
|
+
logic = MemActLogic(
|
|
108
|
+
tools=tool_defs,
|
|
109
|
+
max_history_messages=self._max_history_messages,
|
|
110
|
+
max_tokens=self._max_tokens,
|
|
111
|
+
)
|
|
112
|
+
self.logic = logic
|
|
113
|
+
return create_memact_workflow(logic=logic, on_step=self.on_step)
|
|
114
|
+
|
|
115
|
+
def _sync_session_caches_from_state(self, state: Optional[RunState]) -> None:
|
|
116
|
+
super()._sync_session_caches_from_state(state)
|
|
117
|
+
if state is None or not hasattr(state, "vars") or not isinstance(state.vars, dict):
|
|
118
|
+
return
|
|
119
|
+
runtime_ns = state.vars.get("_runtime")
|
|
120
|
+
if not isinstance(runtime_ns, dict):
|
|
121
|
+
return
|
|
122
|
+
mem = runtime_ns.get("active_memory")
|
|
123
|
+
if isinstance(mem, dict):
|
|
124
|
+
self.session_active_memory = _deepcopy_json(mem)
|
|
125
|
+
|
|
126
|
+
def start(
|
|
127
|
+
self,
|
|
128
|
+
task: str,
|
|
129
|
+
*,
|
|
130
|
+
plan_mode: Optional[bool] = None,
|
|
131
|
+
review_mode: Optional[bool] = None,
|
|
132
|
+
review_max_rounds: Optional[int] = None,
|
|
133
|
+
allowed_tools: Optional[List[str]] = None,
|
|
134
|
+
) -> str:
|
|
135
|
+
task = str(task or "").strip()
|
|
136
|
+
if not task:
|
|
137
|
+
raise ValueError("task must be a non-empty string")
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
base_limits = dict(self.runtime.config.to_limits_dict())
|
|
141
|
+
except Exception:
|
|
142
|
+
base_limits = {}
|
|
143
|
+
limits: Dict[str, Any] = dict(base_limits)
|
|
144
|
+
limits.setdefault("warn_iterations_pct", 80)
|
|
145
|
+
limits.setdefault("warn_tokens_pct", 80)
|
|
146
|
+
limits["max_iterations"] = int(self._max_iterations)
|
|
147
|
+
limits["current_iteration"] = 0
|
|
148
|
+
limits["max_history_messages"] = int(self._max_history_messages)
|
|
149
|
+
limits["estimated_tokens_used"] = 0
|
|
150
|
+
try:
|
|
151
|
+
max_tokens_override = int(self._max_tokens) if self._max_tokens is not None else None
|
|
152
|
+
except Exception:
|
|
153
|
+
max_tokens_override = None
|
|
154
|
+
if isinstance(max_tokens_override, int) and max_tokens_override > 0:
|
|
155
|
+
limits["max_tokens"] = max_tokens_override
|
|
156
|
+
if not isinstance(limits.get("max_tokens"), int) or int(limits.get("max_tokens") or 0) <= 0:
|
|
157
|
+
limits["max_tokens"] = 32768
|
|
158
|
+
|
|
159
|
+
eff_plan_mode = self._plan_mode if plan_mode is None else bool(plan_mode)
|
|
160
|
+
eff_review_mode = self._review_mode if review_mode is None else bool(review_mode)
|
|
161
|
+
eff_review_max_rounds = self._review_max_rounds if review_max_rounds is None else int(review_max_rounds)
|
|
162
|
+
if eff_review_max_rounds < 0:
|
|
163
|
+
eff_review_max_rounds = 0
|
|
164
|
+
|
|
165
|
+
runtime_ns: Dict[str, Any] = {
|
|
166
|
+
"inbox": [],
|
|
167
|
+
"plan_mode": eff_plan_mode,
|
|
168
|
+
"review_mode": eff_review_mode,
|
|
169
|
+
"review_max_rounds": eff_review_max_rounds,
|
|
170
|
+
}
|
|
171
|
+
if isinstance(self.session_active_memory, dict):
|
|
172
|
+
runtime_ns["active_memory"] = _deepcopy_json(self.session_active_memory)
|
|
173
|
+
if isinstance(allowed_tools, list):
|
|
174
|
+
normalized = [str(t).strip() for t in allowed_tools if isinstance(t, str) and t.strip()]
|
|
175
|
+
runtime_ns["allowed_tools"] = normalized
|
|
176
|
+
|
|
177
|
+
vars: Dict[str, Any] = {
|
|
178
|
+
"context": {"task": task, "messages": _copy_messages(self.session_messages)},
|
|
179
|
+
"scratchpad": {"iteration": 0, "max_iterations": int(self._max_iterations)},
|
|
180
|
+
"_runtime": runtime_ns,
|
|
181
|
+
"_temp": {},
|
|
182
|
+
"_limits": limits,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
run_id = self.runtime.start(
|
|
186
|
+
workflow=self.workflow,
|
|
187
|
+
vars=vars,
|
|
188
|
+
actor_id=self._ensure_actor_id(),
|
|
189
|
+
session_id=self._ensure_session_id(),
|
|
190
|
+
)
|
|
191
|
+
self._current_run_id = run_id
|
|
192
|
+
return run_id
|
|
193
|
+
|
|
194
|
+
def step(self) -> RunState:
|
|
195
|
+
if not self._current_run_id:
|
|
196
|
+
raise RuntimeError("No active run. Call start() first.")
|
|
197
|
+
state = self.runtime.tick(workflow=self.workflow, run_id=self._current_run_id, max_steps=1)
|
|
198
|
+
if state.status in (RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED):
|
|
199
|
+
self._sync_session_caches_from_state(state)
|
|
200
|
+
return state
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def create_memact_agent(
|
|
204
|
+
*,
|
|
205
|
+
provider: str = "ollama",
|
|
206
|
+
model: str = "qwen3:1.7b-q4_K_M",
|
|
207
|
+
tools: Optional[List[Callable[..., Any]]] = None,
|
|
208
|
+
on_step: Optional[Callable[[str, Dict[str, Any]], None]] = None,
|
|
209
|
+
max_iterations: int = 25,
|
|
210
|
+
max_history_messages: int = -1,
|
|
211
|
+
max_tokens: Optional[int] = None,
|
|
212
|
+
llm_kwargs: Optional[Dict[str, Any]] = None,
|
|
213
|
+
run_store: Optional[Any] = None,
|
|
214
|
+
ledger_store: Optional[Any] = None,
|
|
215
|
+
actor_id: Optional[str] = None,
|
|
216
|
+
session_id: Optional[str] = None,
|
|
217
|
+
) -> MemActAgent:
|
|
218
|
+
"""Factory: create a MemActAgent with a local AbstractCore-backed runtime."""
|
|
219
|
+
|
|
220
|
+
from abstractruntime.integrations.abstractcore import MappingToolExecutor, create_local_runtime
|
|
221
|
+
|
|
222
|
+
if tools is None:
|
|
223
|
+
from ..tools import ALL_TOOLS
|
|
224
|
+
|
|
225
|
+
tools = list(ALL_TOOLS)
|
|
226
|
+
|
|
227
|
+
runtime = create_local_runtime(
|
|
228
|
+
provider=provider,
|
|
229
|
+
model=model,
|
|
230
|
+
llm_kwargs=llm_kwargs,
|
|
231
|
+
tools=MappingToolExecutor.from_tools(tools),
|
|
232
|
+
run_store=run_store,
|
|
233
|
+
ledger_store=ledger_store,
|
|
234
|
+
)
|
|
235
|
+
return MemActAgent(
|
|
236
|
+
runtime=runtime,
|
|
237
|
+
tools=tools,
|
|
238
|
+
on_step=on_step,
|
|
239
|
+
max_iterations=max_iterations,
|
|
240
|
+
max_history_messages=max_history_messages,
|
|
241
|
+
max_tokens=max_tokens,
|
|
242
|
+
actor_id=actor_id,
|
|
243
|
+
session_id=session_id,
|
|
244
|
+
)
|