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/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
|
@@ -49,11 +49,58 @@ class BaseAgent(ABC):
|
|
|
49
49
|
self.tools = tools or []
|
|
50
50
|
self.on_step = on_step
|
|
51
51
|
self.workflow = self._create_workflow()
|
|
52
|
+
self._ensure_workflow_registered()
|
|
52
53
|
self._current_run_id: Optional[str] = None
|
|
53
54
|
self.actor_id: Optional[str] = actor_id
|
|
54
55
|
self.session_id: Optional[str] = session_id
|
|
55
56
|
self.session_messages: List[Dict[str, Any]] = []
|
|
56
57
|
|
|
58
|
+
def _ensure_workflow_registered(self) -> None:
|
|
59
|
+
"""Ensure this agent's workflow is registered for subworkflow composition.
|
|
60
|
+
|
|
61
|
+
START_SUBWORKFLOW requires a runtime workflow_registry. Registering here keeps
|
|
62
|
+
agent-created runtimes usable for delegation/subflows without extra host wiring.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
wf = getattr(self, "workflow", None)
|
|
66
|
+
wid = getattr(wf, "workflow_id", None)
|
|
67
|
+
if not isinstance(wid, str) or not wid.strip():
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
reg = getattr(self.runtime, "workflow_registry", None)
|
|
71
|
+
if reg is None:
|
|
72
|
+
from abstractruntime.scheduler.registry import WorkflowRegistry
|
|
73
|
+
|
|
74
|
+
reg = WorkflowRegistry()
|
|
75
|
+
setter = getattr(self.runtime, "set_workflow_registry", None)
|
|
76
|
+
if callable(setter):
|
|
77
|
+
setter(reg)
|
|
78
|
+
getter = getattr(reg, "get", None)
|
|
79
|
+
if callable(getter) and getter(wid) is not None:
|
|
80
|
+
return
|
|
81
|
+
register = getattr(reg, "register", None)
|
|
82
|
+
if callable(register):
|
|
83
|
+
register(wf)
|
|
84
|
+
except Exception:
|
|
85
|
+
# Never block agent creation due to registry wiring; hosts can still inject their own registry.
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
def _sync_session_caches_from_state(self, state: Optional[RunState]) -> None:
|
|
89
|
+
if state is None or not hasattr(state, "vars") or not isinstance(state.vars, dict):
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
messages: Optional[List[Dict[str, Any]]] = None
|
|
93
|
+
output = getattr(state, "output", None)
|
|
94
|
+
if isinstance(output, dict) and isinstance(output.get("messages"), list):
|
|
95
|
+
messages = [dict(m) for m in output["messages"] if isinstance(m, dict)]
|
|
96
|
+
else:
|
|
97
|
+
ctx = state.vars.get("context")
|
|
98
|
+
if isinstance(ctx, dict) and isinstance(ctx.get("messages"), list):
|
|
99
|
+
messages = [dict(m) for m in ctx["messages"] if isinstance(m, dict)]
|
|
100
|
+
|
|
101
|
+
if messages is not None:
|
|
102
|
+
self.session_messages = list(messages)
|
|
103
|
+
|
|
57
104
|
def _ensure_actor_id(self) -> str:
|
|
58
105
|
if self.actor_id:
|
|
59
106
|
return self.actor_id
|
|
@@ -135,6 +182,43 @@ class BaseAgent(ABC):
|
|
|
135
182
|
if not self._current_run_id:
|
|
136
183
|
return None
|
|
137
184
|
return self.runtime.get_state(self._current_run_id)
|
|
185
|
+
|
|
186
|
+
def get_context(self) -> Dict[str, Any]:
|
|
187
|
+
"""Get the agent's current context namespace (runtime-owned persisted state)."""
|
|
188
|
+
state = self.get_state()
|
|
189
|
+
ctx = state.vars.get("context") if state and hasattr(state, "vars") else None
|
|
190
|
+
return dict(ctx) if isinstance(ctx, dict) else {}
|
|
191
|
+
|
|
192
|
+
def get_scratchpad(self) -> Dict[str, Any]:
|
|
193
|
+
"""Get the agent's current scratchpad namespace (agent-owned schema, runtime-owned storage)."""
|
|
194
|
+
state = self.get_state()
|
|
195
|
+
scratch = state.vars.get("scratchpad") if state and hasattr(state, "vars") else None
|
|
196
|
+
return dict(scratch) if isinstance(scratch, dict) else {}
|
|
197
|
+
|
|
198
|
+
def get_node_traces(self) -> Dict[str, Any]:
|
|
199
|
+
"""Get runtime-owned per-node traces for the current run (passthrough to Runtime)."""
|
|
200
|
+
if not self._current_run_id:
|
|
201
|
+
return {}
|
|
202
|
+
getter = getattr(self.runtime, "get_node_traces", None)
|
|
203
|
+
if callable(getter):
|
|
204
|
+
return getter(self._current_run_id)
|
|
205
|
+
state = self.get_state()
|
|
206
|
+
runtime_ns = state.vars.get("_runtime") if state and hasattr(state, "vars") else None
|
|
207
|
+
traces = runtime_ns.get("node_traces") if isinstance(runtime_ns, dict) else None
|
|
208
|
+
return dict(traces) if isinstance(traces, dict) else {}
|
|
209
|
+
|
|
210
|
+
def get_node_trace(self, node_id: str) -> Dict[str, Any]:
|
|
211
|
+
"""Get a single runtime-owned node trace for the current run."""
|
|
212
|
+
if not self._current_run_id:
|
|
213
|
+
return {"node_id": node_id, "steps": []}
|
|
214
|
+
getter = getattr(self.runtime, "get_node_trace", None)
|
|
215
|
+
if callable(getter):
|
|
216
|
+
return getter(self._current_run_id, node_id)
|
|
217
|
+
traces = self.get_node_traces()
|
|
218
|
+
trace = traces.get(node_id)
|
|
219
|
+
if isinstance(trace, dict):
|
|
220
|
+
return trace
|
|
221
|
+
return {"node_id": node_id, "steps": []}
|
|
138
222
|
|
|
139
223
|
def is_waiting(self) -> bool:
|
|
140
224
|
"""Check if agent is waiting for input.
|
|
@@ -199,12 +283,15 @@ class BaseAgent(ABC):
|
|
|
199
283
|
|
|
200
284
|
wait_key = state.waiting.wait_key if state.waiting else None
|
|
201
285
|
|
|
202
|
-
|
|
286
|
+
state2 = self.runtime.resume(
|
|
203
287
|
workflow=self.workflow,
|
|
204
288
|
run_id=self._current_run_id,
|
|
205
289
|
wait_key=wait_key,
|
|
206
290
|
payload={"response": response},
|
|
207
291
|
)
|
|
292
|
+
if state2.status in (RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED):
|
|
293
|
+
self._sync_session_caches_from_state(state2)
|
|
294
|
+
return state2
|
|
208
295
|
|
|
209
296
|
def attach(self, run_id: str) -> RunState:
|
|
210
297
|
"""Attach to an existing run for resume.
|
|
@@ -238,6 +325,7 @@ class BaseAgent(ABC):
|
|
|
238
325
|
self.session_id = state_session_id
|
|
239
326
|
|
|
240
327
|
self._current_run_id = run_id
|
|
328
|
+
self._sync_session_caches_from_state(state)
|
|
241
329
|
return state
|
|
242
330
|
|
|
243
331
|
def save_state(self, filepath: str) -> None:
|
abstractagent/agents/codeact.py
CHANGED
|
@@ -5,11 +5,20 @@ 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
|
+
DELEGATE_AGENT_TOOL,
|
|
16
|
+
INSPECT_VARS_TOOL,
|
|
17
|
+
OPEN_ATTACHMENT_TOOL,
|
|
18
|
+
RECALL_MEMORY_TOOL,
|
|
19
|
+
REMEMBER_TOOL,
|
|
20
|
+
REMEMBER_NOTE_TOOL,
|
|
21
|
+
)
|
|
13
22
|
from ..logic.codeact import CodeActLogic
|
|
14
23
|
|
|
15
24
|
|
|
@@ -44,7 +53,10 @@ class CodeActAgent(BaseAgent):
|
|
|
44
53
|
on_step: Optional[Callable[[str, Dict[str, Any]], None]] = None,
|
|
45
54
|
max_iterations: int = 25,
|
|
46
55
|
max_history_messages: int = -1,
|
|
47
|
-
max_tokens: Optional[int] =
|
|
56
|
+
max_tokens: Optional[int] = None,
|
|
57
|
+
plan_mode: bool = False,
|
|
58
|
+
review_mode: bool = True,
|
|
59
|
+
review_max_rounds: int = 3,
|
|
48
60
|
actor_id: Optional[str] = None,
|
|
49
61
|
session_id: Optional[str] = None,
|
|
50
62
|
):
|
|
@@ -56,6 +68,11 @@ class CodeActAgent(BaseAgent):
|
|
|
56
68
|
if self._max_history_messages != -1 and self._max_history_messages < 1:
|
|
57
69
|
self._max_history_messages = 1
|
|
58
70
|
self._max_tokens = max_tokens
|
|
71
|
+
self._plan_mode = bool(plan_mode)
|
|
72
|
+
self._review_mode = bool(review_mode)
|
|
73
|
+
self._review_max_rounds = int(review_max_rounds)
|
|
74
|
+
if self._review_max_rounds < 0:
|
|
75
|
+
self._review_max_rounds = 0
|
|
59
76
|
|
|
60
77
|
self.logic: Optional[CodeActLogic] = None
|
|
61
78
|
super().__init__(
|
|
@@ -68,7 +85,17 @@ class CodeActAgent(BaseAgent):
|
|
|
68
85
|
|
|
69
86
|
def _create_workflow(self) -> WorkflowSpec:
|
|
70
87
|
tool_defs = _tool_definitions_from_callables(self.tools)
|
|
71
|
-
tool_defs = [
|
|
88
|
+
tool_defs = [
|
|
89
|
+
ASK_USER_TOOL,
|
|
90
|
+
OPEN_ATTACHMENT_TOOL,
|
|
91
|
+
RECALL_MEMORY_TOOL,
|
|
92
|
+
INSPECT_VARS_TOOL,
|
|
93
|
+
REMEMBER_TOOL,
|
|
94
|
+
REMEMBER_NOTE_TOOL,
|
|
95
|
+
COMPACT_MEMORY_TOOL,
|
|
96
|
+
DELEGATE_AGENT_TOOL,
|
|
97
|
+
*tool_defs,
|
|
98
|
+
]
|
|
72
99
|
logic = CodeActLogic(
|
|
73
100
|
tools=tool_defs,
|
|
74
101
|
max_history_messages=self._max_history_messages,
|
|
@@ -77,27 +104,99 @@ class CodeActAgent(BaseAgent):
|
|
|
77
104
|
self.logic = logic
|
|
78
105
|
return create_codeact_workflow(logic=logic, on_step=self.on_step)
|
|
79
106
|
|
|
80
|
-
def start(
|
|
107
|
+
def start(
|
|
108
|
+
self,
|
|
109
|
+
task: str,
|
|
110
|
+
*,
|
|
111
|
+
plan_mode: Optional[bool] = None,
|
|
112
|
+
review_mode: Optional[bool] = None,
|
|
113
|
+
review_max_rounds: Optional[int] = None,
|
|
114
|
+
allowed_tools: Optional[List[str]] = None,
|
|
115
|
+
temperature: Optional[float] = None,
|
|
116
|
+
seed: Optional[int] = None,
|
|
117
|
+
attachments: Optional[List[Any]] = None,
|
|
118
|
+
) -> str:
|
|
81
119
|
task = str(task or "").strip()
|
|
82
120
|
if not task:
|
|
83
121
|
raise ValueError("task must be a non-empty string")
|
|
84
122
|
|
|
123
|
+
eff_plan_mode = self._plan_mode if plan_mode is None else bool(plan_mode)
|
|
124
|
+
eff_review_mode = self._review_mode if review_mode is None else bool(review_mode)
|
|
125
|
+
eff_review_max_rounds = self._review_max_rounds if review_max_rounds is None else int(review_max_rounds)
|
|
126
|
+
if eff_review_max_rounds < 0:
|
|
127
|
+
eff_review_max_rounds = 0
|
|
128
|
+
|
|
129
|
+
# Base limits come from the Runtime config so model capabilities (max context)
|
|
130
|
+
# are respected by default, unless explicitly overridden by the agent/session.
|
|
131
|
+
try:
|
|
132
|
+
base_limits = dict(self.runtime.config.to_limits_dict())
|
|
133
|
+
except Exception:
|
|
134
|
+
base_limits = {}
|
|
135
|
+
limits: Dict[str, Any] = dict(base_limits)
|
|
136
|
+
limits.setdefault("warn_iterations_pct", 80)
|
|
137
|
+
limits.setdefault("warn_tokens_pct", 80)
|
|
138
|
+
limits["max_iterations"] = int(self._max_iterations)
|
|
139
|
+
limits["current_iteration"] = 0
|
|
140
|
+
limits["max_history_messages"] = int(self._max_history_messages)
|
|
141
|
+
# Message-size guards for LLM-visible context (character-level).
|
|
142
|
+
# Disabled by default (-1): enable by setting a positive character budget.
|
|
143
|
+
limits.setdefault("max_message_chars", -1)
|
|
144
|
+
limits.setdefault("max_tool_message_chars", -1)
|
|
145
|
+
limits["estimated_tokens_used"] = 0
|
|
146
|
+
try:
|
|
147
|
+
max_tokens_override = int(self._max_tokens) if self._max_tokens is not None else None
|
|
148
|
+
except Exception:
|
|
149
|
+
max_tokens_override = None
|
|
150
|
+
if isinstance(max_tokens_override, int) and max_tokens_override > 0:
|
|
151
|
+
limits["max_tokens"] = max_tokens_override
|
|
152
|
+
if not isinstance(limits.get("max_tokens"), int) or int(limits.get("max_tokens") or 0) <= 0:
|
|
153
|
+
limits["max_tokens"] = 32768
|
|
154
|
+
|
|
85
155
|
vars: Dict[str, Any] = {
|
|
86
156
|
"context": {"task": task, "messages": _copy_messages(self.session_messages)},
|
|
87
157
|
"scratchpad": {"iteration": 0, "max_iterations": int(self._max_iterations)},
|
|
88
|
-
"_runtime": {
|
|
158
|
+
"_runtime": {
|
|
159
|
+
"inbox": [],
|
|
160
|
+
"plan_mode": eff_plan_mode,
|
|
161
|
+
"review_mode": eff_review_mode,
|
|
162
|
+
"review_max_rounds": eff_review_max_rounds,
|
|
163
|
+
},
|
|
89
164
|
"_temp": {},
|
|
90
165
|
# 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
|
-
},
|
|
166
|
+
"_limits": limits,
|
|
100
167
|
}
|
|
168
|
+
if temperature is not None:
|
|
169
|
+
try:
|
|
170
|
+
vars["_runtime"]["temperature"] = float(temperature)
|
|
171
|
+
except Exception:
|
|
172
|
+
pass
|
|
173
|
+
if seed is not None:
|
|
174
|
+
try:
|
|
175
|
+
vars["_runtime"]["seed"] = int(seed)
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
if isinstance(allowed_tools, list):
|
|
179
|
+
normalized = [str(t).strip() for t in allowed_tools if isinstance(t, str) and t.strip()]
|
|
180
|
+
vars["_runtime"]["allowed_tools"] = normalized
|
|
181
|
+
if attachments:
|
|
182
|
+
items: list[Any]
|
|
183
|
+
if isinstance(attachments, tuple):
|
|
184
|
+
items = list(attachments)
|
|
185
|
+
else:
|
|
186
|
+
items = attachments if isinstance(attachments, list) else []
|
|
187
|
+
normalized: list[Any] = []
|
|
188
|
+
for item in items:
|
|
189
|
+
if isinstance(item, str) and item.strip():
|
|
190
|
+
normalized.append(item.strip())
|
|
191
|
+
continue
|
|
192
|
+
if isinstance(item, dict):
|
|
193
|
+
aid = item.get("$artifact")
|
|
194
|
+
if not (isinstance(aid, str) and aid.strip()):
|
|
195
|
+
aid = item.get("artifact_id")
|
|
196
|
+
if isinstance(aid, str) and aid.strip():
|
|
197
|
+
normalized.append(dict(item))
|
|
198
|
+
if normalized:
|
|
199
|
+
vars["context"]["attachments"] = normalized
|
|
101
200
|
|
|
102
201
|
run_id = self.runtime.start(
|
|
103
202
|
workflow=self.workflow,
|
|
@@ -142,7 +241,10 @@ class CodeActAgent(BaseAgent):
|
|
|
142
241
|
def step(self) -> RunState:
|
|
143
242
|
if not self._current_run_id:
|
|
144
243
|
raise RuntimeError("No active run. Call start() first.")
|
|
145
|
-
|
|
244
|
+
state = self.runtime.tick(workflow=self.workflow, run_id=self._current_run_id, max_steps=1)
|
|
245
|
+
if state.status in (RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED):
|
|
246
|
+
self._sync_session_caches_from_state(state)
|
|
247
|
+
return state
|
|
146
248
|
|
|
147
249
|
|
|
148
250
|
def create_codeact_agent(
|
|
@@ -153,7 +255,10 @@ def create_codeact_agent(
|
|
|
153
255
|
on_step: Optional[Callable[[str, Dict[str, Any]], None]] = None,
|
|
154
256
|
max_iterations: int = 25,
|
|
155
257
|
max_history_messages: int = -1,
|
|
156
|
-
max_tokens: Optional[int] =
|
|
258
|
+
max_tokens: Optional[int] = None,
|
|
259
|
+
plan_mode: bool = False,
|
|
260
|
+
review_mode: bool = True,
|
|
261
|
+
review_max_rounds: int = 3,
|
|
157
262
|
llm_kwargs: Optional[Dict[str, Any]] = None,
|
|
158
263
|
run_store: Optional[Any] = None,
|
|
159
264
|
ledger_store: Optional[Any] = None,
|
|
@@ -185,10 +290,12 @@ def create_codeact_agent(
|
|
|
185
290
|
max_iterations=max_iterations,
|
|
186
291
|
max_history_messages=max_history_messages,
|
|
187
292
|
max_tokens=max_tokens,
|
|
293
|
+
plan_mode=plan_mode,
|
|
294
|
+
review_mode=review_mode,
|
|
295
|
+
review_max_rounds=review_max_rounds,
|
|
188
296
|
actor_id=actor_id,
|
|
189
297
|
session_id=session_id,
|
|
190
298
|
)
|
|
191
299
|
|
|
192
300
|
|
|
193
301
|
__all__ = ["CodeActAgent", "create_codeact_workflow", "create_codeact_agent"]
|
|
194
|
-
|
|
@@ -0,0 +1,280 @@
|
|
|
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
|
+
DELEGATE_AGENT_TOOL,
|
|
21
|
+
INSPECT_VARS_TOOL,
|
|
22
|
+
OPEN_ATTACHMENT_TOOL,
|
|
23
|
+
RECALL_MEMORY_TOOL,
|
|
24
|
+
REMEMBER_TOOL,
|
|
25
|
+
REMEMBER_NOTE_TOOL,
|
|
26
|
+
)
|
|
27
|
+
from ..logic.memact import MemActLogic
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _tool_definitions_from_callables(tools: List[Callable[..., Any]]) -> List[ToolDefinition]:
|
|
31
|
+
tool_defs: List[ToolDefinition] = []
|
|
32
|
+
for t in tools:
|
|
33
|
+
tool_def = getattr(t, "_tool_definition", None)
|
|
34
|
+
if tool_def is None:
|
|
35
|
+
tool_def = ToolDefinition.from_function(t)
|
|
36
|
+
tool_defs.append(tool_def)
|
|
37
|
+
return tool_defs
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _copy_messages(messages: Any) -> List[Dict[str, Any]]:
|
|
41
|
+
if not isinstance(messages, list):
|
|
42
|
+
return []
|
|
43
|
+
out: List[Dict[str, Any]] = []
|
|
44
|
+
for m in messages:
|
|
45
|
+
if isinstance(m, dict):
|
|
46
|
+
out.append(dict(m))
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _deepcopy_json(value: Any) -> Any:
|
|
51
|
+
try:
|
|
52
|
+
return json.loads(json.dumps(value))
|
|
53
|
+
except Exception:
|
|
54
|
+
return value
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class MemActAgent(BaseAgent):
|
|
58
|
+
"""Memory-enhanced agent with runtime-owned Active Memory blocks."""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
*,
|
|
63
|
+
runtime: Runtime,
|
|
64
|
+
tools: Optional[List[Callable[..., Any]]] = None,
|
|
65
|
+
on_step: Optional[Callable[[str, Dict[str, Any]], None]] = None,
|
|
66
|
+
max_iterations: int = 25,
|
|
67
|
+
max_history_messages: int = -1,
|
|
68
|
+
max_tokens: Optional[int] = None,
|
|
69
|
+
plan_mode: bool = False,
|
|
70
|
+
review_mode: bool = False,
|
|
71
|
+
review_max_rounds: int = 1,
|
|
72
|
+
actor_id: Optional[str] = None,
|
|
73
|
+
session_id: Optional[str] = None,
|
|
74
|
+
):
|
|
75
|
+
self._max_iterations = int(max_iterations)
|
|
76
|
+
if self._max_iterations < 1:
|
|
77
|
+
self._max_iterations = 1
|
|
78
|
+
self._max_history_messages = int(max_history_messages)
|
|
79
|
+
if self._max_history_messages != -1 and self._max_history_messages < 1:
|
|
80
|
+
self._max_history_messages = 1
|
|
81
|
+
self._max_tokens = max_tokens
|
|
82
|
+
self._plan_mode = bool(plan_mode)
|
|
83
|
+
self._review_mode = bool(review_mode)
|
|
84
|
+
self._review_max_rounds = int(review_max_rounds)
|
|
85
|
+
if self._review_max_rounds < 0:
|
|
86
|
+
self._review_max_rounds = 0
|
|
87
|
+
|
|
88
|
+
self.logic: Optional[MemActLogic] = None
|
|
89
|
+
self.session_active_memory: Optional[Dict[str, Any]] = None
|
|
90
|
+
super().__init__(
|
|
91
|
+
runtime=runtime,
|
|
92
|
+
tools=tools,
|
|
93
|
+
on_step=on_step,
|
|
94
|
+
actor_id=actor_id,
|
|
95
|
+
session_id=session_id,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def _create_workflow(self) -> WorkflowSpec:
|
|
99
|
+
tool_defs = _tool_definitions_from_callables(self.tools)
|
|
100
|
+
tool_defs = [
|
|
101
|
+
ASK_USER_TOOL,
|
|
102
|
+
OPEN_ATTACHMENT_TOOL,
|
|
103
|
+
RECALL_MEMORY_TOOL,
|
|
104
|
+
INSPECT_VARS_TOOL,
|
|
105
|
+
REMEMBER_TOOL,
|
|
106
|
+
REMEMBER_NOTE_TOOL,
|
|
107
|
+
COMPACT_MEMORY_TOOL,
|
|
108
|
+
DELEGATE_AGENT_TOOL,
|
|
109
|
+
*tool_defs,
|
|
110
|
+
]
|
|
111
|
+
logic = MemActLogic(
|
|
112
|
+
tools=tool_defs,
|
|
113
|
+
max_history_messages=self._max_history_messages,
|
|
114
|
+
max_tokens=self._max_tokens,
|
|
115
|
+
)
|
|
116
|
+
self.logic = logic
|
|
117
|
+
return create_memact_workflow(logic=logic, on_step=self.on_step)
|
|
118
|
+
|
|
119
|
+
def _sync_session_caches_from_state(self, state: Optional[RunState]) -> None:
|
|
120
|
+
super()._sync_session_caches_from_state(state)
|
|
121
|
+
if state is None or not hasattr(state, "vars") or not isinstance(state.vars, dict):
|
|
122
|
+
return
|
|
123
|
+
runtime_ns = state.vars.get("_runtime")
|
|
124
|
+
if not isinstance(runtime_ns, dict):
|
|
125
|
+
return
|
|
126
|
+
mem = runtime_ns.get("active_memory")
|
|
127
|
+
if isinstance(mem, dict):
|
|
128
|
+
self.session_active_memory = _deepcopy_json(mem)
|
|
129
|
+
|
|
130
|
+
def start(
|
|
131
|
+
self,
|
|
132
|
+
task: str,
|
|
133
|
+
*,
|
|
134
|
+
plan_mode: Optional[bool] = None,
|
|
135
|
+
review_mode: Optional[bool] = None,
|
|
136
|
+
review_max_rounds: Optional[int] = None,
|
|
137
|
+
allowed_tools: Optional[List[str]] = None,
|
|
138
|
+
temperature: Optional[float] = None,
|
|
139
|
+
seed: Optional[int] = None,
|
|
140
|
+
attachments: Optional[List[Any]] = None,
|
|
141
|
+
) -> str:
|
|
142
|
+
task = str(task or "").strip()
|
|
143
|
+
if not task:
|
|
144
|
+
raise ValueError("task must be a non-empty string")
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
base_limits = dict(self.runtime.config.to_limits_dict())
|
|
148
|
+
except Exception:
|
|
149
|
+
base_limits = {}
|
|
150
|
+
limits: Dict[str, Any] = dict(base_limits)
|
|
151
|
+
limits.setdefault("warn_iterations_pct", 80)
|
|
152
|
+
limits.setdefault("warn_tokens_pct", 80)
|
|
153
|
+
limits["max_iterations"] = int(self._max_iterations)
|
|
154
|
+
limits["current_iteration"] = 0
|
|
155
|
+
limits["max_history_messages"] = int(self._max_history_messages)
|
|
156
|
+
limits["estimated_tokens_used"] = 0
|
|
157
|
+
try:
|
|
158
|
+
max_tokens_override = int(self._max_tokens) if self._max_tokens is not None else None
|
|
159
|
+
except Exception:
|
|
160
|
+
max_tokens_override = None
|
|
161
|
+
if isinstance(max_tokens_override, int) and max_tokens_override > 0:
|
|
162
|
+
limits["max_tokens"] = max_tokens_override
|
|
163
|
+
if not isinstance(limits.get("max_tokens"), int) or int(limits.get("max_tokens") or 0) <= 0:
|
|
164
|
+
limits["max_tokens"] = 32768
|
|
165
|
+
|
|
166
|
+
eff_plan_mode = self._plan_mode if plan_mode is None else bool(plan_mode)
|
|
167
|
+
eff_review_mode = self._review_mode if review_mode is None else bool(review_mode)
|
|
168
|
+
eff_review_max_rounds = self._review_max_rounds if review_max_rounds is None else int(review_max_rounds)
|
|
169
|
+
if eff_review_max_rounds < 0:
|
|
170
|
+
eff_review_max_rounds = 0
|
|
171
|
+
|
|
172
|
+
runtime_ns: Dict[str, Any] = {
|
|
173
|
+
"inbox": [],
|
|
174
|
+
"plan_mode": eff_plan_mode,
|
|
175
|
+
"review_mode": eff_review_mode,
|
|
176
|
+
"review_max_rounds": eff_review_max_rounds,
|
|
177
|
+
}
|
|
178
|
+
if temperature is not None:
|
|
179
|
+
try:
|
|
180
|
+
runtime_ns["temperature"] = float(temperature)
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
if seed is not None:
|
|
184
|
+
try:
|
|
185
|
+
runtime_ns["seed"] = int(seed)
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
if isinstance(self.session_active_memory, dict):
|
|
189
|
+
runtime_ns["active_memory"] = _deepcopy_json(self.session_active_memory)
|
|
190
|
+
if isinstance(allowed_tools, list):
|
|
191
|
+
normalized = [str(t).strip() for t in allowed_tools if isinstance(t, str) and t.strip()]
|
|
192
|
+
runtime_ns["allowed_tools"] = normalized
|
|
193
|
+
|
|
194
|
+
vars: Dict[str, Any] = {
|
|
195
|
+
"context": {"task": task, "messages": _copy_messages(self.session_messages)},
|
|
196
|
+
"scratchpad": {"iteration": 0, "max_iterations": int(self._max_iterations)},
|
|
197
|
+
"_runtime": runtime_ns,
|
|
198
|
+
"_temp": {},
|
|
199
|
+
"_limits": limits,
|
|
200
|
+
}
|
|
201
|
+
if attachments:
|
|
202
|
+
items: list[Any]
|
|
203
|
+
if isinstance(attachments, tuple):
|
|
204
|
+
items = list(attachments)
|
|
205
|
+
else:
|
|
206
|
+
items = attachments if isinstance(attachments, list) else []
|
|
207
|
+
normalized: list[Any] = []
|
|
208
|
+
for item in items:
|
|
209
|
+
if isinstance(item, str) and item.strip():
|
|
210
|
+
normalized.append(item.strip())
|
|
211
|
+
continue
|
|
212
|
+
if isinstance(item, dict):
|
|
213
|
+
aid = item.get("$artifact")
|
|
214
|
+
if not (isinstance(aid, str) and aid.strip()):
|
|
215
|
+
aid = item.get("artifact_id")
|
|
216
|
+
if isinstance(aid, str) and aid.strip():
|
|
217
|
+
normalized.append(dict(item))
|
|
218
|
+
if normalized:
|
|
219
|
+
vars["context"]["attachments"] = normalized
|
|
220
|
+
|
|
221
|
+
run_id = self.runtime.start(
|
|
222
|
+
workflow=self.workflow,
|
|
223
|
+
vars=vars,
|
|
224
|
+
actor_id=self._ensure_actor_id(),
|
|
225
|
+
session_id=self._ensure_session_id(),
|
|
226
|
+
)
|
|
227
|
+
self._current_run_id = run_id
|
|
228
|
+
return run_id
|
|
229
|
+
|
|
230
|
+
def step(self) -> RunState:
|
|
231
|
+
if not self._current_run_id:
|
|
232
|
+
raise RuntimeError("No active run. Call start() first.")
|
|
233
|
+
state = self.runtime.tick(workflow=self.workflow, run_id=self._current_run_id, max_steps=1)
|
|
234
|
+
if state.status in (RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED):
|
|
235
|
+
self._sync_session_caches_from_state(state)
|
|
236
|
+
return state
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def create_memact_agent(
|
|
240
|
+
*,
|
|
241
|
+
provider: str = "ollama",
|
|
242
|
+
model: str = "qwen3:1.7b-q4_K_M",
|
|
243
|
+
tools: Optional[List[Callable[..., Any]]] = None,
|
|
244
|
+
on_step: Optional[Callable[[str, Dict[str, Any]], None]] = None,
|
|
245
|
+
max_iterations: int = 25,
|
|
246
|
+
max_history_messages: int = -1,
|
|
247
|
+
max_tokens: Optional[int] = None,
|
|
248
|
+
llm_kwargs: Optional[Dict[str, Any]] = None,
|
|
249
|
+
run_store: Optional[Any] = None,
|
|
250
|
+
ledger_store: Optional[Any] = None,
|
|
251
|
+
actor_id: Optional[str] = None,
|
|
252
|
+
session_id: Optional[str] = None,
|
|
253
|
+
) -> MemActAgent:
|
|
254
|
+
"""Factory: create a MemActAgent with a local AbstractCore-backed runtime."""
|
|
255
|
+
|
|
256
|
+
from abstractruntime.integrations.abstractcore import MappingToolExecutor, create_local_runtime
|
|
257
|
+
|
|
258
|
+
if tools is None:
|
|
259
|
+
from ..tools import ALL_TOOLS
|
|
260
|
+
|
|
261
|
+
tools = list(ALL_TOOLS)
|
|
262
|
+
|
|
263
|
+
runtime = create_local_runtime(
|
|
264
|
+
provider=provider,
|
|
265
|
+
model=model,
|
|
266
|
+
llm_kwargs=llm_kwargs,
|
|
267
|
+
tool_executor=MappingToolExecutor.from_tools(list(tools)),
|
|
268
|
+
run_store=run_store,
|
|
269
|
+
ledger_store=ledger_store,
|
|
270
|
+
)
|
|
271
|
+
return MemActAgent(
|
|
272
|
+
runtime=runtime,
|
|
273
|
+
tools=tools,
|
|
274
|
+
on_step=on_step,
|
|
275
|
+
max_iterations=max_iterations,
|
|
276
|
+
max_history_messages=max_history_messages,
|
|
277
|
+
max_tokens=max_tokens,
|
|
278
|
+
actor_id=actor_id,
|
|
279
|
+
session_id=session_id,
|
|
280
|
+
)
|