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.
@@ -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
  ]
@@ -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
- return self.runtime.resume(
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:
@@ -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 ASK_USER_TOOL
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] = 32768,
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 = [ASK_USER_TOOL, *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(self, task: str) -> str:
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": {"inbox": []},
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
- return self.runtime.tick(workflow=self.workflow, run_id=self._current_run_id, max_steps=1)
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] = 32768,
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
+ )