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.
@@ -15,11 +15,20 @@ from __future__ import annotations
15
15
  from typing import Any, Callable, Dict, List, Optional
16
16
 
17
17
  from abstractcore.tools import ToolDefinition
18
- from abstractruntime import RunState, Runtime, WorkflowSpec
18
+ from abstractruntime import RunState, RunStatus, Runtime, WorkflowSpec
19
19
 
20
20
  from .base import BaseAgent
21
21
  from ..adapters.react_runtime import create_react_workflow
22
- from ..logic.builtins import ASK_USER_TOOL
22
+ from ..logic.builtins import (
23
+ ASK_USER_TOOL,
24
+ COMPACT_MEMORY_TOOL,
25
+ DELEGATE_AGENT_TOOL,
26
+ INSPECT_VARS_TOOL,
27
+ OPEN_ATTACHMENT_TOOL,
28
+ RECALL_MEMORY_TOOL,
29
+ REMEMBER_TOOL,
30
+ REMEMBER_NOTE_TOOL,
31
+ )
23
32
  from ..logic.react import ReActLogic
24
33
 
25
34
 
@@ -54,7 +63,10 @@ class ReactAgent(BaseAgent):
54
63
  on_step: Optional[Callable[[str, Dict[str, Any]], None]] = None,
55
64
  max_iterations: int = 25,
56
65
  max_history_messages: int = -1,
57
- max_tokens: Optional[int] = 32768,
66
+ max_tokens: Optional[int] = None,
67
+ plan_mode: bool = False,
68
+ review_mode: bool = True,
69
+ review_max_rounds: int = 3,
58
70
  actor_id: Optional[str] = None,
59
71
  session_id: Optional[str] = None,
60
72
  ):
@@ -66,6 +78,11 @@ class ReactAgent(BaseAgent):
66
78
  if self._max_history_messages != -1 and self._max_history_messages < 1:
67
79
  self._max_history_messages = 1
68
80
  self._max_tokens = max_tokens
81
+ self._plan_mode = bool(plan_mode)
82
+ self._review_mode = bool(review_mode)
83
+ self._review_max_rounds = int(review_max_rounds)
84
+ if self._review_max_rounds < 0:
85
+ self._review_max_rounds = 0
69
86
 
70
87
  self.logic: Optional[ReActLogic] = None
71
88
  super().__init__(
@@ -79,7 +96,17 @@ class ReactAgent(BaseAgent):
79
96
  def _create_workflow(self) -> WorkflowSpec:
80
97
  tool_defs = _tool_definitions_from_callables(self.tools)
81
98
  # Built-in ask_user is a schema-only tool (handled via ASK_USER effect in the adapter).
82
- tool_defs = [ASK_USER_TOOL, *tool_defs]
99
+ tool_defs = [
100
+ ASK_USER_TOOL,
101
+ OPEN_ATTACHMENT_TOOL,
102
+ RECALL_MEMORY_TOOL,
103
+ INSPECT_VARS_TOOL,
104
+ REMEMBER_TOOL,
105
+ REMEMBER_NOTE_TOOL,
106
+ COMPACT_MEMORY_TOOL,
107
+ DELEGATE_AGENT_TOOL,
108
+ *tool_defs,
109
+ ]
83
110
 
84
111
  logic = ReActLogic(
85
112
  tools=tool_defs,
@@ -89,27 +116,103 @@ class ReactAgent(BaseAgent):
89
116
  self.logic = logic
90
117
  return create_react_workflow(logic=logic, on_step=self.on_step)
91
118
 
92
- def start(self, task: str) -> str:
119
+ def start(
120
+ self,
121
+ task: str,
122
+ *,
123
+ plan_mode: Optional[bool] = None,
124
+ review_mode: Optional[bool] = None,
125
+ review_max_rounds: Optional[int] = None,
126
+ allowed_tools: Optional[List[str]] = None,
127
+ temperature: Optional[float] = None,
128
+ seed: Optional[int] = None,
129
+ attachments: Optional[List[Any]] = None,
130
+ ) -> str:
93
131
  task = str(task or "").strip()
94
132
  if not task:
95
133
  raise ValueError("task must be a non-empty string")
96
134
 
135
+ eff_plan_mode = self._plan_mode if plan_mode is None else bool(plan_mode)
136
+ eff_review_mode = self._review_mode if review_mode is None else bool(review_mode)
137
+ eff_review_max_rounds = self._review_max_rounds if review_max_rounds is None else int(review_max_rounds)
138
+ if eff_review_max_rounds < 0:
139
+ eff_review_max_rounds = 0
140
+
141
+ # Base limits come from the Runtime config so model capabilities (max context)
142
+ # are respected by default, unless explicitly overridden by the agent/session.
143
+ try:
144
+ base_limits = dict(self.runtime.config.to_limits_dict())
145
+ except Exception:
146
+ base_limits = {}
147
+ limits: Dict[str, Any] = dict(base_limits)
148
+ limits.setdefault("warn_iterations_pct", 80)
149
+ limits.setdefault("warn_tokens_pct", 80)
150
+ limits["max_iterations"] = int(self._max_iterations)
151
+ limits["current_iteration"] = 0
152
+ limits["max_history_messages"] = int(self._max_history_messages)
153
+ # Message-size guards for LLM-visible context (character-level).
154
+ # These are applied when building the provider payload; the durable run history
155
+ # still preserves full message text.
156
+ # Disabled by default (-1): enable by setting a positive character budget.
157
+ limits.setdefault("max_message_chars", -1)
158
+ limits.setdefault("max_tool_message_chars", -1)
159
+ limits["estimated_tokens_used"] = 0
160
+ # ReAct output-token capping is controlled via `_limits.max_output_tokens`.
161
+ # Policy: unset by default (None) to avoid artificial truncation.
162
+ try:
163
+ max_output_tokens_override = int(self._max_tokens) if self._max_tokens is not None else None
164
+ except Exception:
165
+ max_output_tokens_override = None
166
+ if isinstance(max_output_tokens_override, int) and max_output_tokens_override > 0:
167
+ limits["max_output_tokens"] = max_output_tokens_override
168
+ else:
169
+ limits.setdefault("max_output_tokens", None)
170
+
97
171
  vars: Dict[str, Any] = {
98
172
  "context": {"task": task, "messages": _copy_messages(self.session_messages)},
99
173
  "scratchpad": {"iteration": 0, "max_iterations": int(self._max_iterations)},
100
- "_runtime": {"inbox": []},
174
+ "_runtime": {
175
+ "inbox": [],
176
+ "plan_mode": eff_plan_mode,
177
+ "review_mode": eff_review_mode,
178
+ "review_max_rounds": eff_review_max_rounds,
179
+ },
101
180
  "_temp": {},
102
181
  # Canonical _limits namespace for runtime awareness
103
- "_limits": {
104
- "max_iterations": int(self._max_iterations),
105
- "current_iteration": 0,
106
- "max_tokens": self._max_tokens,
107
- "max_history_messages": int(self._max_history_messages),
108
- "estimated_tokens_used": 0,
109
- "warn_iterations_pct": 80,
110
- "warn_tokens_pct": 80,
111
- },
182
+ "_limits": limits,
112
183
  }
184
+ if attachments:
185
+ items: list[Any]
186
+ if isinstance(attachments, tuple):
187
+ items = list(attachments)
188
+ else:
189
+ items = attachments if isinstance(attachments, list) else []
190
+ normalized: list[Any] = []
191
+ for item in items:
192
+ if isinstance(item, str) and item.strip():
193
+ normalized.append(item.strip())
194
+ continue
195
+ if isinstance(item, dict):
196
+ aid = item.get("$artifact")
197
+ if not (isinstance(aid, str) and aid.strip()):
198
+ aid = item.get("artifact_id")
199
+ if isinstance(aid, str) and aid.strip():
200
+ normalized.append(dict(item))
201
+ if normalized:
202
+ vars["context"]["attachments"] = normalized
203
+ if temperature is not None:
204
+ try:
205
+ vars["_runtime"]["temperature"] = float(temperature)
206
+ except Exception:
207
+ pass
208
+ if seed is not None:
209
+ try:
210
+ vars["_runtime"]["seed"] = int(seed)
211
+ except Exception:
212
+ pass
213
+ if isinstance(allowed_tools, list):
214
+ normalized = [str(t).strip() for t in allowed_tools if isinstance(t, str) and t.strip()]
215
+ vars["_runtime"]["allowed_tools"] = normalized
113
216
 
114
217
  run_id = self.runtime.start(
115
218
  workflow=self.workflow,
@@ -154,7 +257,10 @@ class ReactAgent(BaseAgent):
154
257
  def step(self) -> RunState:
155
258
  if not self._current_run_id:
156
259
  raise RuntimeError("No active run. Call start() first.")
157
- return self.runtime.tick(workflow=self.workflow, run_id=self._current_run_id, max_steps=1)
260
+ state = self.runtime.tick(workflow=self.workflow, run_id=self._current_run_id, max_steps=1)
261
+ if state.status in (RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED):
262
+ self._sync_session_caches_from_state(state)
263
+ return state
158
264
 
159
265
 
160
266
  def create_react_agent(
@@ -165,7 +271,10 @@ def create_react_agent(
165
271
  on_step: Optional[Callable[[str, Dict[str, Any]], None]] = None,
166
272
  max_iterations: int = 25,
167
273
  max_history_messages: int = -1,
168
- max_tokens: Optional[int] = 32768,
274
+ max_tokens: Optional[int] = None,
275
+ plan_mode: bool = False,
276
+ review_mode: bool = True,
277
+ review_max_rounds: int = 3,
169
278
  llm_kwargs: Optional[Dict[str, Any]] = None,
170
279
  run_store: Optional[Any] = None,
171
280
  ledger_store: Optional[Any] = None,
@@ -197,6 +306,9 @@ def create_react_agent(
197
306
  max_iterations=max_iterations,
198
307
  max_history_messages=max_history_messages,
199
308
  max_tokens=max_tokens,
309
+ plan_mode=plan_mode,
310
+ review_mode=review_mode,
311
+ review_max_rounds=review_max_rounds,
200
312
  actor_id=actor_id,
201
313
  session_id=session_id,
202
314
  )
@@ -207,4 +319,3 @@ __all__ = [
207
319
  "create_react_workflow",
208
320
  "create_react_agent",
209
321
  ]
210
-
@@ -6,6 +6,7 @@ workflow wiring lives under `abstractagent.adapters`.
6
6
 
7
7
  from .builtins import ASK_USER_TOOL
8
8
  from .codeact import CodeActLogic
9
+ from .memact import MemActLogic
9
10
  from .react import ReActLogic
10
11
  from .types import AskUserAction, FinalAnswer, LLMRequest
11
12
 
@@ -16,4 +17,5 @@ __all__ = [
16
17
  "FinalAnswer",
17
18
  "ReActLogic",
18
19
  "CodeActLogic",
20
+ "MemActLogic",
19
21
  ]
@@ -9,10 +9,7 @@ from abstractcore.tools import ToolDefinition
9
9
 
10
10
  ASK_USER_TOOL = ToolDefinition(
11
11
  name="ask_user",
12
- description=(
13
- "Ask the user a question when you need clarification or input. "
14
- "Use this when the task is ambiguous or you need the user to make a choice."
15
- ),
12
+ description="Ask the user a question.",
16
13
  parameters={
17
14
  "question": {
18
15
  "type": "string",
@@ -22,8 +19,276 @@ ASK_USER_TOOL = ToolDefinition(
22
19
  "type": "array",
23
20
  "items": {"type": "string"},
24
21
  "description": "Optional list of choices for the user to pick from",
22
+ "default": None,
23
+ },
24
+ },
25
+ when_to_use="Use when the task is ambiguous or you need user input to proceed.",
26
+ )
27
+
28
+ OPEN_ATTACHMENT_TOOL = ToolDefinition(
29
+ name="open_attachment",
30
+ description="Read an attachment (artifact) from the current session with bounded output.",
31
+ parameters={
32
+ "artifact_id": {"type": "string", "description": "Attachment artifact id (preferred).", "default": None},
33
+ "handle": {"type": "string", "description": "Attachment handle (usually '@path').", "default": None},
34
+ "expected_sha256": {"type": "string", "description": "Optional sha256 to disambiguate versions.", "default": None},
35
+ "start_line": {"type": "integer", "description": "1-based start line (default 1).", "default": 1},
36
+ "end_line": {"type": "integer", "description": "Optional end line (inclusive).", "default": None},
37
+ "max_chars": {"type": "integer", "description": "Maximum characters to return (default 8000).", "default": 8000},
38
+ },
39
+ when_to_use=(
40
+ "Use only to open stored (non-active) session attachments or to fetch a bounded, line-numbered excerpt when the "
41
+ "attachment is not already provided in this call. Prefer artifact_id when available; otherwise use handle."
42
+ ),
43
+ )
44
+
45
+ RECALL_MEMORY_TOOL = ToolDefinition(
46
+ name="recall_memory",
47
+ description="Recall archived memory spans with provenance (by span_id/query/tags/time range).",
48
+ parameters={
49
+ "span_id": {
50
+ "type": "string",
51
+ "description": (
52
+ "Optional span identifier (artifact id) or 1-based index into archived spans. "
53
+ "If a summary includes span_id=..., use that exact value."
54
+ ),
55
+ "default": None,
56
+ },
57
+ "query": {
58
+ "type": "string",
59
+ "description": "Optional keyword query (topic/person/etc). Performs metadata-first search with bounded deep scan over archived messages.",
60
+ "default": None,
61
+ },
62
+ "since": {
63
+ "type": "string",
64
+ "description": "Optional ISO8601 start timestamp for time-range filtering.",
65
+ "default": None,
66
+ },
67
+ "until": {
68
+ "type": "string",
69
+ "description": "Optional ISO8601 end timestamp for time-range filtering.",
70
+ "default": None,
71
+ },
72
+ "tags": {
73
+ "type": "object",
74
+ "description": (
75
+ "Optional metadata tag filters.\n"
76
+ "- Values may be a string or a list of strings.\n"
77
+ "- Example: {\"topic\":\"api\",\"person\":[\"alice\",\"bob\"]}\n"
78
+ "Use tags_mode to control AND/OR across tag keys."
79
+ ),
80
+ "default": None,
81
+ },
82
+ "tags_mode": {
83
+ "type": "string",
84
+ "description": (
85
+ "How to combine tag keys: all (AND across keys) | any (OR across keys). "
86
+ "Within a key, list values are treated as OR."
87
+ ),
88
+ "default": "all",
89
+ },
90
+ "usernames": {
91
+ "type": "array",
92
+ "items": {"type": "string"},
93
+ "description": (
94
+ "Optional author filter (actor ids / usernames). Matches spans created_by case-insensitively. "
95
+ "Semantics: OR (any listed author)."
96
+ ),
97
+ "default": None,
98
+ },
99
+ "locations": {
100
+ "type": "array",
101
+ "items": {"type": "string"},
102
+ "description": (
103
+ "Optional location filter. Matches spans by explicit location metadata (or tags.location). "
104
+ "Semantics: OR (any listed location)."
105
+ ),
106
+ "default": None,
107
+ },
108
+ "limit_spans": {
109
+ "type": "integer",
110
+ "description": "Maximum number of spans to return (default 5).",
111
+ "default": 5,
112
+ },
113
+ "connected": {
114
+ "type": "boolean",
115
+ "description": "If true, also include connected spans (time neighbors and shared-tag neighbors).",
116
+ "default": False,
117
+ },
118
+ "neighbor_hops": {
119
+ "type": "integer",
120
+ "description": "When connected=true, include up to this many neighbor spans on each side (default 1).",
121
+ "default": 1,
122
+ },
123
+ "max_messages": {
124
+ "type": "integer",
125
+ "description": "Maximum total messages to render in the recall output across all spans (-1 = no truncation).",
126
+ "default": -1,
127
+ },
128
+ "scope": {
129
+ "type": "string",
130
+ "description": "Memory scope to query: run | session | global | all (default run).",
131
+ "default": "run",
132
+ },
133
+ "recall_level": {
134
+ "type": "string",
135
+ "enum": ["urgent", "standard", "deep"],
136
+ "description": (
137
+ "Optional recall effort policy.\n"
138
+ "- urgent: fastest, tight budgets\n"
139
+ "- standard: bounded but more thorough\n"
140
+ "- deep: may be slow/expensive\n"
141
+ "No silent downgrade: budgets/permissions are enforced by the runtime."
142
+ ),
143
+ "default": None,
25
144
  },
26
145
  },
27
- when_to_use="When the task is ambiguous or you need user input to proceed",
146
+ when_to_use="Use after compaction or when you need exact details from earlier context.",
28
147
  )
29
148
 
149
+ INSPECT_VARS_TOOL = ToolDefinition(
150
+ name="inspect_vars",
151
+ description="Inspect durable run-state variables by path (e.g., scratchpad/runtime vars).",
152
+ parameters={
153
+ "path": {
154
+ "type": "string",
155
+ "description": (
156
+ "Path to inspect (default 'scratchpad'). Supports dot paths like 'scratchpad.foo[0]' "
157
+ "or JSON pointer paths like '/scratchpad/foo/0'."
158
+ ),
159
+ "default": "scratchpad",
160
+ },
161
+ "keys_only": {
162
+ "type": "boolean",
163
+ "description": "If true, return keys/length instead of the full value (useful to navigate large objects).",
164
+ "default": False,
165
+ },
166
+ "target_run_id": {
167
+ "type": "string",
168
+ "description": "Optional run id to inspect (defaults to the current run).",
169
+ "default": None,
170
+ },
171
+ },
172
+ when_to_use=(
173
+ "Use to debug or inspect scratchpad/runtime vars (prefer keys_only=true first)."
174
+ ),
175
+ )
176
+
177
+ REMEMBER_TOOL = ToolDefinition(
178
+ name="remember",
179
+ description="Tag an archived memory span for later recall.",
180
+ parameters={
181
+ "span_id": {
182
+ "type": "string",
183
+ "description": (
184
+ "Span identifier (artifact id) or 1-based index into archived spans. "
185
+ "If a summary includes span_id=..., use that exact value."
186
+ ),
187
+ },
188
+ "tags": {
189
+ "type": "object",
190
+ "description": (
191
+ "Tags to set on the span (JSON-safe dict[str,str]), e.g. {\"topic\":\"api\",\"person\":\"alice\"}. "
192
+ "At least one tag is required."
193
+ ),
194
+ },
195
+ "merge": {
196
+ "type": "boolean",
197
+ "description": "If true (default), merges tags into existing tags. If false, replaces existing tags.",
198
+ "default": True,
199
+ },
200
+ },
201
+ when_to_use=(
202
+ "Use when you want to label a recalled/compacted span with durable tags."
203
+ ),
204
+ )
205
+
206
+ REMEMBER_NOTE_TOOL = ToolDefinition(
207
+ name="remember_note",
208
+ description="Store a durable memory note (decision/fact) with optional tags and sources.",
209
+ parameters={
210
+ "note": {
211
+ "type": "string",
212
+ "description": "The note to remember (required). Keep it short and specific.",
213
+ },
214
+ "tags": {
215
+ "type": "object",
216
+ "description": "Optional tags (dict[str,str]) to help recall later, e.g. {\"topic\":\"api\",\"person\":\"alice\"}.",
217
+ "default": None,
218
+ },
219
+ "sources": {
220
+ "type": "object",
221
+ "description": (
222
+ "Optional provenance sources for this note. Use span_ids/message_ids when available.\n"
223
+ "Example: {\"span_ids\":[\"span_...\"], \"message_ids\":[\"msg_...\"]}"
224
+ ),
225
+ "default": None,
226
+ },
227
+ "location": {
228
+ "type": "string",
229
+ "description": "Optional location for this memory note (user perspective).",
230
+ "default": None,
231
+ },
232
+ "scope": {
233
+ "type": "string",
234
+ "description": "Where to store this note: run | session | global (default run).",
235
+ "default": "run",
236
+ },
237
+ },
238
+ when_to_use=(
239
+ "When you want to persist a key insight/decision/fact for later recall by time/topic/person, "
240
+ "especially before any compaction span exists."
241
+ ),
242
+ )
243
+
244
+ COMPACT_MEMORY_TOOL = ToolDefinition(
245
+ name="compact_memory",
246
+ description="Compact older conversation context into an archived span and insert a summary handle.",
247
+ parameters={
248
+ "preserve_recent": {
249
+ "type": "integer",
250
+ "description": "Number of most recent non-system messages to keep verbatim (default 6).",
251
+ "default": 6,
252
+ },
253
+ "compression_mode": {
254
+ "type": "string",
255
+ "description": "Compression mode: light | standard | heavy (default standard).",
256
+ "default": "standard",
257
+ },
258
+ "focus": {
259
+ "type": "string",
260
+ "description": "Optional focus/topic to prioritize in the summary.",
261
+ "default": None,
262
+ },
263
+ },
264
+ when_to_use="Use when the active context is too large and you need to reduce it while keeping provenance.",
265
+ )
266
+
267
+ DELEGATE_AGENT_TOOL = ToolDefinition(
268
+ name="delegate_agent",
269
+ description="Delegate a subtask to a fresh agent run with smaller context and restricted tools.",
270
+ parameters={
271
+ "task": {
272
+ "type": "string",
273
+ "description": "The delegated task (required). Keep it specific and self-contained.",
274
+ },
275
+ "context": {
276
+ "type": "string",
277
+ "description": "Minimal supporting context for the delegated task (optional).",
278
+ "default": "",
279
+ },
280
+ "tools": {
281
+ "type": "array",
282
+ "items": {"type": "string"},
283
+ "description": (
284
+ "Tool allowlist for the delegated agent (optional). "
285
+ "When omitted, the delegated agent inherits the current allowlist."
286
+ ),
287
+ "default": None,
288
+ },
289
+ },
290
+ when_to_use=(
291
+ "Use when you can split off a subtask that can be completed with a small context (e.g., "
292
+ "inspect a few files, search for specific symbols, summarize findings)."
293
+ ),
294
+ )