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/react.py
CHANGED
|
@@ -15,11 +15,18 @@ 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
|
|
22
|
+
from ..logic.builtins import (
|
|
23
|
+
ASK_USER_TOOL,
|
|
24
|
+
COMPACT_MEMORY_TOOL,
|
|
25
|
+
INSPECT_VARS_TOOL,
|
|
26
|
+
RECALL_MEMORY_TOOL,
|
|
27
|
+
REMEMBER_TOOL,
|
|
28
|
+
REMEMBER_NOTE_TOOL,
|
|
29
|
+
)
|
|
23
30
|
from ..logic.react import ReActLogic
|
|
24
31
|
|
|
25
32
|
|
|
@@ -54,7 +61,10 @@ class ReactAgent(BaseAgent):
|
|
|
54
61
|
on_step: Optional[Callable[[str, Dict[str, Any]], None]] = None,
|
|
55
62
|
max_iterations: int = 25,
|
|
56
63
|
max_history_messages: int = -1,
|
|
57
|
-
max_tokens: Optional[int] =
|
|
64
|
+
max_tokens: Optional[int] = None,
|
|
65
|
+
plan_mode: bool = False,
|
|
66
|
+
review_mode: bool = True,
|
|
67
|
+
review_max_rounds: int = 3,
|
|
58
68
|
actor_id: Optional[str] = None,
|
|
59
69
|
session_id: Optional[str] = None,
|
|
60
70
|
):
|
|
@@ -66,6 +76,11 @@ class ReactAgent(BaseAgent):
|
|
|
66
76
|
if self._max_history_messages != -1 and self._max_history_messages < 1:
|
|
67
77
|
self._max_history_messages = 1
|
|
68
78
|
self._max_tokens = max_tokens
|
|
79
|
+
self._plan_mode = bool(plan_mode)
|
|
80
|
+
self._review_mode = bool(review_mode)
|
|
81
|
+
self._review_max_rounds = int(review_max_rounds)
|
|
82
|
+
if self._review_max_rounds < 0:
|
|
83
|
+
self._review_max_rounds = 0
|
|
69
84
|
|
|
70
85
|
self.logic: Optional[ReActLogic] = None
|
|
71
86
|
super().__init__(
|
|
@@ -79,7 +94,15 @@ class ReactAgent(BaseAgent):
|
|
|
79
94
|
def _create_workflow(self) -> WorkflowSpec:
|
|
80
95
|
tool_defs = _tool_definitions_from_callables(self.tools)
|
|
81
96
|
# Built-in ask_user is a schema-only tool (handled via ASK_USER effect in the adapter).
|
|
82
|
-
tool_defs = [
|
|
97
|
+
tool_defs = [
|
|
98
|
+
ASK_USER_TOOL,
|
|
99
|
+
RECALL_MEMORY_TOOL,
|
|
100
|
+
INSPECT_VARS_TOOL,
|
|
101
|
+
REMEMBER_TOOL,
|
|
102
|
+
REMEMBER_NOTE_TOOL,
|
|
103
|
+
COMPACT_MEMORY_TOOL,
|
|
104
|
+
*tool_defs,
|
|
105
|
+
]
|
|
83
106
|
|
|
84
107
|
logic = ReActLogic(
|
|
85
108
|
tools=tool_defs,
|
|
@@ -89,27 +112,69 @@ class ReactAgent(BaseAgent):
|
|
|
89
112
|
self.logic = logic
|
|
90
113
|
return create_react_workflow(logic=logic, on_step=self.on_step)
|
|
91
114
|
|
|
92
|
-
def start(
|
|
115
|
+
def start(
|
|
116
|
+
self,
|
|
117
|
+
task: str,
|
|
118
|
+
*,
|
|
119
|
+
plan_mode: Optional[bool] = None,
|
|
120
|
+
review_mode: Optional[bool] = None,
|
|
121
|
+
review_max_rounds: Optional[int] = None,
|
|
122
|
+
allowed_tools: Optional[List[str]] = None,
|
|
123
|
+
) -> str:
|
|
93
124
|
task = str(task or "").strip()
|
|
94
125
|
if not task:
|
|
95
126
|
raise ValueError("task must be a non-empty string")
|
|
96
127
|
|
|
128
|
+
eff_plan_mode = self._plan_mode if plan_mode is None else bool(plan_mode)
|
|
129
|
+
eff_review_mode = self._review_mode if review_mode is None else bool(review_mode)
|
|
130
|
+
eff_review_max_rounds = self._review_max_rounds if review_max_rounds is None else int(review_max_rounds)
|
|
131
|
+
if eff_review_max_rounds < 0:
|
|
132
|
+
eff_review_max_rounds = 0
|
|
133
|
+
|
|
134
|
+
# Base limits come from the Runtime config so model capabilities (max context)
|
|
135
|
+
# are respected by default, unless explicitly overridden by the agent/session.
|
|
136
|
+
try:
|
|
137
|
+
base_limits = dict(self.runtime.config.to_limits_dict())
|
|
138
|
+
except Exception:
|
|
139
|
+
base_limits = {}
|
|
140
|
+
limits: Dict[str, Any] = dict(base_limits)
|
|
141
|
+
limits.setdefault("warn_iterations_pct", 80)
|
|
142
|
+
limits.setdefault("warn_tokens_pct", 80)
|
|
143
|
+
limits["max_iterations"] = int(self._max_iterations)
|
|
144
|
+
limits["current_iteration"] = 0
|
|
145
|
+
limits["max_history_messages"] = int(self._max_history_messages)
|
|
146
|
+
# Message-size guards for LLM-visible context (character-level).
|
|
147
|
+
# These are applied when building the provider payload; the durable run history
|
|
148
|
+
# still preserves full message text.
|
|
149
|
+
# Disabled by default (-1): enable by setting a positive character budget.
|
|
150
|
+
limits.setdefault("max_message_chars", -1)
|
|
151
|
+
limits.setdefault("max_tool_message_chars", -1)
|
|
152
|
+
limits["estimated_tokens_used"] = 0
|
|
153
|
+
try:
|
|
154
|
+
max_tokens_override = int(self._max_tokens) if self._max_tokens is not None else None
|
|
155
|
+
except Exception:
|
|
156
|
+
max_tokens_override = None
|
|
157
|
+
if isinstance(max_tokens_override, int) and max_tokens_override > 0:
|
|
158
|
+
limits["max_tokens"] = max_tokens_override
|
|
159
|
+
if not isinstance(limits.get("max_tokens"), int) or int(limits.get("max_tokens") or 0) <= 0:
|
|
160
|
+
limits["max_tokens"] = 32768
|
|
161
|
+
|
|
97
162
|
vars: Dict[str, Any] = {
|
|
98
163
|
"context": {"task": task, "messages": _copy_messages(self.session_messages)},
|
|
99
164
|
"scratchpad": {"iteration": 0, "max_iterations": int(self._max_iterations)},
|
|
100
|
-
"_runtime": {
|
|
165
|
+
"_runtime": {
|
|
166
|
+
"inbox": [],
|
|
167
|
+
"plan_mode": eff_plan_mode,
|
|
168
|
+
"review_mode": eff_review_mode,
|
|
169
|
+
"review_max_rounds": eff_review_max_rounds,
|
|
170
|
+
},
|
|
101
171
|
"_temp": {},
|
|
102
172
|
# 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
|
-
},
|
|
173
|
+
"_limits": limits,
|
|
112
174
|
}
|
|
175
|
+
if isinstance(allowed_tools, list):
|
|
176
|
+
normalized = [str(t).strip() for t in allowed_tools if isinstance(t, str) and t.strip()]
|
|
177
|
+
vars["_runtime"]["allowed_tools"] = normalized
|
|
113
178
|
|
|
114
179
|
run_id = self.runtime.start(
|
|
115
180
|
workflow=self.workflow,
|
|
@@ -154,7 +219,10 @@ class ReactAgent(BaseAgent):
|
|
|
154
219
|
def step(self) -> RunState:
|
|
155
220
|
if not self._current_run_id:
|
|
156
221
|
raise RuntimeError("No active run. Call start() first.")
|
|
157
|
-
|
|
222
|
+
state = self.runtime.tick(workflow=self.workflow, run_id=self._current_run_id, max_steps=1)
|
|
223
|
+
if state.status in (RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED):
|
|
224
|
+
self._sync_session_caches_from_state(state)
|
|
225
|
+
return state
|
|
158
226
|
|
|
159
227
|
|
|
160
228
|
def create_react_agent(
|
|
@@ -165,7 +233,10 @@ def create_react_agent(
|
|
|
165
233
|
on_step: Optional[Callable[[str, Dict[str, Any]], None]] = None,
|
|
166
234
|
max_iterations: int = 25,
|
|
167
235
|
max_history_messages: int = -1,
|
|
168
|
-
max_tokens: Optional[int] =
|
|
236
|
+
max_tokens: Optional[int] = None,
|
|
237
|
+
plan_mode: bool = False,
|
|
238
|
+
review_mode: bool = True,
|
|
239
|
+
review_max_rounds: int = 3,
|
|
169
240
|
llm_kwargs: Optional[Dict[str, Any]] = None,
|
|
170
241
|
run_store: Optional[Any] = None,
|
|
171
242
|
ledger_store: Optional[Any] = None,
|
|
@@ -197,6 +268,9 @@ def create_react_agent(
|
|
|
197
268
|
max_iterations=max_iterations,
|
|
198
269
|
max_history_messages=max_history_messages,
|
|
199
270
|
max_tokens=max_tokens,
|
|
271
|
+
plan_mode=plan_mode,
|
|
272
|
+
review_mode=review_mode,
|
|
273
|
+
review_max_rounds=review_max_rounds,
|
|
200
274
|
actor_id=actor_id,
|
|
201
275
|
session_id=session_id,
|
|
202
276
|
)
|
|
@@ -207,4 +281,3 @@ __all__ = [
|
|
|
207
281
|
"create_react_workflow",
|
|
208
282
|
"create_react_agent",
|
|
209
283
|
]
|
|
210
|
-
|
abstractagent/logic/__init__.py
CHANGED
|
@@ -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
|
]
|
abstractagent/logic/builtins.py
CHANGED
|
@@ -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,218 @@ 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
|
+
RECALL_MEMORY_TOOL = ToolDefinition(
|
|
29
|
+
name="recall_memory",
|
|
30
|
+
description="Recall archived memory spans with provenance (by span_id/query/tags/time range).",
|
|
31
|
+
parameters={
|
|
32
|
+
"span_id": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": (
|
|
35
|
+
"Optional span identifier (artifact id) or 1-based index into archived spans. "
|
|
36
|
+
"If a summary includes span_id=..., use that exact value."
|
|
37
|
+
),
|
|
38
|
+
"default": None,
|
|
39
|
+
},
|
|
40
|
+
"query": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "Optional keyword query (topic/person/etc). Performs metadata-first search with bounded deep scan over archived messages.",
|
|
43
|
+
"default": None,
|
|
44
|
+
},
|
|
45
|
+
"since": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"description": "Optional ISO8601 start timestamp for time-range filtering.",
|
|
48
|
+
"default": None,
|
|
49
|
+
},
|
|
50
|
+
"until": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"description": "Optional ISO8601 end timestamp for time-range filtering.",
|
|
53
|
+
"default": None,
|
|
54
|
+
},
|
|
55
|
+
"tags": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"description": (
|
|
58
|
+
"Optional metadata tag filters.\n"
|
|
59
|
+
"- Values may be a string or a list of strings.\n"
|
|
60
|
+
"- Example: {\"topic\":\"api\",\"person\":[\"alice\",\"bob\"]}\n"
|
|
61
|
+
"Use tags_mode to control AND/OR across tag keys."
|
|
62
|
+
),
|
|
63
|
+
"default": None,
|
|
64
|
+
},
|
|
65
|
+
"tags_mode": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"description": (
|
|
68
|
+
"How to combine tag keys: all (AND across keys) | any (OR across keys). "
|
|
69
|
+
"Within a key, list values are treated as OR."
|
|
70
|
+
),
|
|
71
|
+
"default": "all",
|
|
72
|
+
},
|
|
73
|
+
"usernames": {
|
|
74
|
+
"type": "array",
|
|
75
|
+
"items": {"type": "string"},
|
|
76
|
+
"description": (
|
|
77
|
+
"Optional author filter (actor ids / usernames). Matches spans created_by case-insensitively. "
|
|
78
|
+
"Semantics: OR (any listed author)."
|
|
79
|
+
),
|
|
80
|
+
"default": None,
|
|
81
|
+
},
|
|
82
|
+
"locations": {
|
|
83
|
+
"type": "array",
|
|
84
|
+
"items": {"type": "string"},
|
|
85
|
+
"description": (
|
|
86
|
+
"Optional location filter. Matches spans by explicit location metadata (or tags.location). "
|
|
87
|
+
"Semantics: OR (any listed location)."
|
|
88
|
+
),
|
|
89
|
+
"default": None,
|
|
90
|
+
},
|
|
91
|
+
"limit_spans": {
|
|
92
|
+
"type": "integer",
|
|
93
|
+
"description": "Maximum number of spans to return (default 5).",
|
|
94
|
+
"default": 5,
|
|
95
|
+
},
|
|
96
|
+
"connected": {
|
|
97
|
+
"type": "boolean",
|
|
98
|
+
"description": "If true, also include connected spans (time neighbors and shared-tag neighbors).",
|
|
99
|
+
"default": False,
|
|
100
|
+
},
|
|
101
|
+
"neighbor_hops": {
|
|
102
|
+
"type": "integer",
|
|
103
|
+
"description": "When connected=true, include up to this many neighbor spans on each side (default 1).",
|
|
104
|
+
"default": 1,
|
|
105
|
+
},
|
|
106
|
+
"max_messages": {
|
|
107
|
+
"type": "integer",
|
|
108
|
+
"description": "Maximum total messages to render in the recall output across all spans (-1 = no truncation).",
|
|
109
|
+
"default": -1,
|
|
110
|
+
},
|
|
111
|
+
"scope": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"description": "Memory scope to query: run | session | global | all (default run).",
|
|
114
|
+
"default": "run",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
when_to_use="Use after compaction or when you need exact details from earlier context.",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
INSPECT_VARS_TOOL = ToolDefinition(
|
|
121
|
+
name="inspect_vars",
|
|
122
|
+
description="Inspect durable run-state variables by path (e.g., scratchpad/runtime vars).",
|
|
123
|
+
parameters={
|
|
124
|
+
"path": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"description": (
|
|
127
|
+
"Path to inspect (default 'scratchpad'). Supports dot paths like 'scratchpad.foo[0]' "
|
|
128
|
+
"or JSON pointer paths like '/scratchpad/foo/0'."
|
|
129
|
+
),
|
|
130
|
+
"default": "scratchpad",
|
|
131
|
+
},
|
|
132
|
+
"keys_only": {
|
|
133
|
+
"type": "boolean",
|
|
134
|
+
"description": "If true, return keys/length instead of the full value (useful to navigate large objects).",
|
|
135
|
+
"default": False,
|
|
136
|
+
},
|
|
137
|
+
"target_run_id": {
|
|
138
|
+
"type": "string",
|
|
139
|
+
"description": "Optional run id to inspect (defaults to the current run).",
|
|
140
|
+
"default": None,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
when_to_use=(
|
|
144
|
+
"Use to debug or inspect scratchpad/runtime vars (prefer keys_only=true first)."
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
REMEMBER_TOOL = ToolDefinition(
|
|
149
|
+
name="remember",
|
|
150
|
+
description="Tag an archived memory span for later recall.",
|
|
151
|
+
parameters={
|
|
152
|
+
"span_id": {
|
|
153
|
+
"type": "string",
|
|
154
|
+
"description": (
|
|
155
|
+
"Span identifier (artifact id) or 1-based index into archived spans. "
|
|
156
|
+
"If a summary includes span_id=..., use that exact value."
|
|
157
|
+
),
|
|
158
|
+
},
|
|
159
|
+
"tags": {
|
|
160
|
+
"type": "object",
|
|
161
|
+
"description": (
|
|
162
|
+
"Tags to set on the span (JSON-safe dict[str,str]), e.g. {\"topic\":\"api\",\"person\":\"alice\"}. "
|
|
163
|
+
"At least one tag is required."
|
|
164
|
+
),
|
|
165
|
+
},
|
|
166
|
+
"merge": {
|
|
167
|
+
"type": "boolean",
|
|
168
|
+
"description": "If true (default), merges tags into existing tags. If false, replaces existing tags.",
|
|
169
|
+
"default": True,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
when_to_use=(
|
|
173
|
+
"Use when you want to label a recalled/compacted span with durable tags."
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
REMEMBER_NOTE_TOOL = ToolDefinition(
|
|
178
|
+
name="remember_note",
|
|
179
|
+
description="Store a durable memory note (decision/fact) with optional tags and sources.",
|
|
180
|
+
parameters={
|
|
181
|
+
"note": {
|
|
182
|
+
"type": "string",
|
|
183
|
+
"description": "The note to remember (required). Keep it short and specific.",
|
|
184
|
+
},
|
|
185
|
+
"tags": {
|
|
186
|
+
"type": "object",
|
|
187
|
+
"description": "Optional tags (dict[str,str]) to help recall later, e.g. {\"topic\":\"api\",\"person\":\"alice\"}.",
|
|
188
|
+
"default": None,
|
|
189
|
+
},
|
|
190
|
+
"sources": {
|
|
191
|
+
"type": "object",
|
|
192
|
+
"description": (
|
|
193
|
+
"Optional provenance sources for this note. Use span_ids/message_ids when available.\n"
|
|
194
|
+
"Example: {\"span_ids\":[\"span_...\"], \"message_ids\":[\"msg_...\"]}"
|
|
195
|
+
),
|
|
196
|
+
"default": None,
|
|
197
|
+
},
|
|
198
|
+
"location": {
|
|
199
|
+
"type": "string",
|
|
200
|
+
"description": "Optional location for this memory note (user perspective).",
|
|
201
|
+
"default": None,
|
|
202
|
+
},
|
|
203
|
+
"scope": {
|
|
204
|
+
"type": "string",
|
|
205
|
+
"description": "Where to store this note: run | session | global (default run).",
|
|
206
|
+
"default": "run",
|
|
25
207
|
},
|
|
26
208
|
},
|
|
27
|
-
when_to_use=
|
|
209
|
+
when_to_use=(
|
|
210
|
+
"When you want to persist a key insight/decision/fact for later recall by time/topic/person, "
|
|
211
|
+
"especially before any compaction span exists."
|
|
212
|
+
),
|
|
28
213
|
)
|
|
29
214
|
|
|
215
|
+
COMPACT_MEMORY_TOOL = ToolDefinition(
|
|
216
|
+
name="compact_memory",
|
|
217
|
+
description="Compact older conversation context into an archived span and insert a summary handle.",
|
|
218
|
+
parameters={
|
|
219
|
+
"preserve_recent": {
|
|
220
|
+
"type": "integer",
|
|
221
|
+
"description": "Number of most recent non-system messages to keep verbatim (default 6).",
|
|
222
|
+
"default": 6,
|
|
223
|
+
},
|
|
224
|
+
"compression_mode": {
|
|
225
|
+
"type": "string",
|
|
226
|
+
"description": "Compression mode: light | standard | heavy (default standard).",
|
|
227
|
+
"default": "standard",
|
|
228
|
+
},
|
|
229
|
+
"focus": {
|
|
230
|
+
"type": "string",
|
|
231
|
+
"description": "Optional focus/topic to prioritize in the summary.",
|
|
232
|
+
"default": None,
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
when_to_use="Use when the active context is too large and you need to reduce it while keeping provenance.",
|
|
236
|
+
)
|
abstractagent/logic/codeact.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"""CodeAct logic (pure; no runtime imports).
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
This module implements a conventional CodeAct loop:
|
|
4
|
+
- the model primarily acts by producing Python code (or calling execute_python)
|
|
5
|
+
- tool results are appended to chat history
|
|
6
|
+
- the model iterates until it can answer directly
|
|
7
|
+
|
|
8
|
+
CodeAct is intentionally *not* a memory-enhanced agent.
|
|
5
9
|
"""
|
|
6
10
|
|
|
7
11
|
from __future__ import annotations
|
|
@@ -26,7 +30,6 @@ class CodeActLogic:
|
|
|
26
30
|
):
|
|
27
31
|
self._tools = list(tools)
|
|
28
32
|
self._max_history_messages = int(max_history_messages)
|
|
29
|
-
# -1 means unlimited (send all messages), otherwise must be >= 1
|
|
30
33
|
if self._max_history_messages != -1 and self._max_history_messages < 1:
|
|
31
34
|
self._max_history_messages = 1
|
|
32
35
|
self._max_tokens = max_tokens
|
|
@@ -35,6 +38,23 @@ class CodeActLogic:
|
|
|
35
38
|
def tools(self) -> List[ToolDefinition]:
|
|
36
39
|
return list(self._tools)
|
|
37
40
|
|
|
41
|
+
def add_tools(self, tools: List[ToolDefinition]) -> int:
|
|
42
|
+
if not isinstance(tools, list) or not tools:
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
existing = {str(t.name) for t in self._tools if getattr(t, "name", None)}
|
|
46
|
+
added = 0
|
|
47
|
+
for t in tools:
|
|
48
|
+
name = getattr(t, "name", None)
|
|
49
|
+
if not isinstance(name, str) or not name.strip():
|
|
50
|
+
continue
|
|
51
|
+
if name in existing:
|
|
52
|
+
continue
|
|
53
|
+
self._tools.append(t)
|
|
54
|
+
existing.add(name)
|
|
55
|
+
added += 1
|
|
56
|
+
return added
|
|
57
|
+
|
|
38
58
|
def build_request(
|
|
39
59
|
self,
|
|
40
60
|
*,
|
|
@@ -45,58 +65,70 @@ class CodeActLogic:
|
|
|
45
65
|
max_iterations: int = 20,
|
|
46
66
|
vars: Optional[Dict[str, Any]] = None,
|
|
47
67
|
) -> LLMRequest:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Args:
|
|
51
|
-
task: The task to perform
|
|
52
|
-
messages: Conversation history
|
|
53
|
-
guidance: Optional guidance text to inject
|
|
54
|
-
iteration: Current iteration number
|
|
55
|
-
max_iterations: Maximum allowed iterations
|
|
56
|
-
vars: Optional run.vars dict. If provided, limits are read from
|
|
57
|
-
vars["_limits"] (canonical) with fallback to instance defaults.
|
|
58
|
-
"""
|
|
68
|
+
_ = messages # history is carried out-of-band via chat messages
|
|
69
|
+
|
|
59
70
|
task = str(task or "")
|
|
60
71
|
guidance = str(guidance or "").strip()
|
|
61
72
|
|
|
62
|
-
# Get limits from vars if available, else use instance defaults
|
|
63
73
|
limits = (vars or {}).get("_limits", {})
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"
|
|
82
|
-
|
|
74
|
+
max_output_tokens = limits.get("max_output_tokens", None)
|
|
75
|
+
if max_output_tokens is not None:
|
|
76
|
+
try:
|
|
77
|
+
max_output_tokens = int(max_output_tokens)
|
|
78
|
+
except Exception:
|
|
79
|
+
max_output_tokens = None
|
|
80
|
+
|
|
81
|
+
runtime_ns = (vars or {}).get("_runtime", {})
|
|
82
|
+
scratchpad = (vars or {}).get("scratchpad", {})
|
|
83
|
+
plan_mode = bool(runtime_ns.get("plan_mode")) if isinstance(runtime_ns, dict) else False
|
|
84
|
+
plan_text = scratchpad.get("plan") if isinstance(scratchpad, dict) else None
|
|
85
|
+
plan = str(plan_text).strip() if isinstance(plan_text, str) and plan_text.strip() else ""
|
|
86
|
+
|
|
87
|
+
prompt = task.strip()
|
|
88
|
+
|
|
89
|
+
output_budget_line = ""
|
|
90
|
+
if isinstance(max_output_tokens, int) and max_output_tokens > 0:
|
|
91
|
+
output_budget_line = f"- Output token limit for this response: {max_output_tokens}.\n"
|
|
92
|
+
|
|
93
|
+
system_prompt = (
|
|
83
94
|
f"Iteration: {int(iteration)}/{int(max_iterations)}\n\n"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
95
|
+
"You are CodeAct: you solve tasks by writing and executing Python when needed.\n\n"
|
|
96
|
+
"Evidence & action (IMPORTANT):\n"
|
|
97
|
+
"- Be truthful: only claim actions supported by tool outputs.\n"
|
|
98
|
+
"- If the task requires code execution or file edits, do it now (call a tool or output a fenced ```python``` block).\n"
|
|
99
|
+
"- Do not “announce” actions without executing them.\n\n"
|
|
100
|
+
"Rules:\n"
|
|
101
|
+
"- Be truthful: only claim actions supported by tool outputs.\n"
|
|
102
|
+
"- Be autonomous: do not ask the user for confirmation to proceed; keep going until the task is done.\n"
|
|
103
|
+
"- If you need to run code, call `execute_python` (preferred) or output a fenced ```python code block.\n"
|
|
104
|
+
"- Never fabricate tool outputs.\n"
|
|
105
|
+
"- Only ask the user a question when required information is missing.\n"
|
|
106
|
+
f"{output_budget_line}"
|
|
107
|
+
).strip()
|
|
88
108
|
|
|
89
109
|
if guidance:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
110
|
+
system_prompt = (system_prompt + "\n\nGuidance:\n" + guidance).strip()
|
|
111
|
+
|
|
112
|
+
if plan_mode and plan:
|
|
113
|
+
system_prompt = (system_prompt + "\n\nCurrent plan:\n" + plan).strip()
|
|
114
|
+
|
|
115
|
+
if plan_mode:
|
|
116
|
+
system_prompt = (
|
|
117
|
+
system_prompt
|
|
118
|
+
+ "\n\nPlan mode:\n"
|
|
119
|
+
"- Maintain and update the plan as you work.\n"
|
|
120
|
+
"- If the plan changes, include a final section at the END of your message:\n"
|
|
121
|
+
" Plan Update:\n"
|
|
122
|
+
" <markdown checklist>\n"
|
|
123
|
+
).strip()
|
|
124
|
+
|
|
125
|
+
return LLMRequest(
|
|
126
|
+
prompt=prompt,
|
|
127
|
+
system_prompt=system_prompt,
|
|
128
|
+
tools=self.tools,
|
|
129
|
+
max_tokens=max_output_tokens,
|
|
96
130
|
)
|
|
97
131
|
|
|
98
|
-
return LLMRequest(prompt=prompt, tools=self.tools, max_tokens=max_tokens)
|
|
99
|
-
|
|
100
132
|
def parse_response(self, response: Any) -> Tuple[str, List[ToolCall]]:
|
|
101
133
|
if not isinstance(response, dict):
|
|
102
134
|
return "", []
|
|
@@ -104,6 +136,11 @@ class CodeActLogic:
|
|
|
104
136
|
content = response.get("content")
|
|
105
137
|
content = "" if content is None else str(content)
|
|
106
138
|
|
|
139
|
+
if not content.strip():
|
|
140
|
+
reasoning = response.get("reasoning")
|
|
141
|
+
if isinstance(reasoning, str) and reasoning.strip():
|
|
142
|
+
content = reasoning.strip()
|
|
143
|
+
|
|
107
144
|
tool_calls_raw = response.get("tool_calls") or []
|
|
108
145
|
tool_calls: List[ToolCall] = []
|
|
109
146
|
if isinstance(tool_calls_raw, list):
|
|
@@ -118,15 +155,6 @@ class CodeActLogic:
|
|
|
118
155
|
if isinstance(args, dict):
|
|
119
156
|
tool_calls.append(ToolCall(name=name, arguments=dict(args), call_id=call_id))
|
|
120
157
|
|
|
121
|
-
# FALLBACK: Parse from content if no native tool calls
|
|
122
|
-
# Handles <|tool_call|>, <function_call>, ```tool_code, etc.
|
|
123
|
-
if not tool_calls and content:
|
|
124
|
-
from abstractcore.tools.parser import parse_tool_calls, detect_tool_calls
|
|
125
|
-
if detect_tool_calls(content):
|
|
126
|
-
# Pass model name for architecture-specific parsing
|
|
127
|
-
model_name = response.get("model")
|
|
128
|
-
tool_calls = parse_tool_calls(content, model_name=model_name)
|
|
129
|
-
|
|
130
158
|
return content, tool_calls
|
|
131
159
|
|
|
132
160
|
def extract_code(self, text: str) -> str | None:
|
|
@@ -138,29 +166,8 @@ class CodeActLogic:
|
|
|
138
166
|
return code.strip() or None
|
|
139
167
|
|
|
140
168
|
def format_observation(self, *, name: str, output: Any, success: bool) -> str:
|
|
141
|
-
if
|
|
142
|
-
|
|
143
|
-
return f"[{name}]: {out}"
|
|
144
|
-
|
|
145
|
-
if not isinstance(output, dict):
|
|
146
|
-
out = "" if output is None else str(output)
|
|
147
|
-
return f"[execute_python]: {out}" if success else f"[execute_python]: Error: {out}"
|
|
148
|
-
|
|
149
|
-
stdout = str(output.get("stdout") or "")
|
|
150
|
-
stderr = str(output.get("stderr") or "")
|
|
151
|
-
exit_code = output.get("exit_code")
|
|
152
|
-
error = output.get("error")
|
|
153
|
-
|
|
154
|
-
parts: List[str] = []
|
|
155
|
-
if error:
|
|
156
|
-
parts.append(f"error={error}")
|
|
157
|
-
if exit_code is not None:
|
|
158
|
-
parts.append(f"exit_code={exit_code}")
|
|
159
|
-
if stdout:
|
|
160
|
-
parts.append("stdout:\n" + stdout)
|
|
161
|
-
if stderr:
|
|
162
|
-
parts.append("stderr:\n" + stderr)
|
|
163
|
-
|
|
164
|
-
rendered = "\n".join(parts).strip() or "(no output)"
|
|
165
|
-
return f"[execute_python]: {rendered}"
|
|
169
|
+
out = "" if output is None else str(output)
|
|
170
|
+
if success:
|
|
171
|
+
return f"[{name}]: {out}"
|
|
172
|
+
return f"[{name}]: Error: {out}"
|
|
166
173
|
|