abstractagent 0.2.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/__init__.py +49 -0
- abstractagent/adapters/__init__.py +6 -0
- abstractagent/adapters/codeact_runtime.py +397 -0
- abstractagent/adapters/react_runtime.py +390 -0
- abstractagent/agents/__init__.py +15 -0
- abstractagent/agents/base.py +421 -0
- abstractagent/agents/codeact.py +194 -0
- abstractagent/agents/react.py +210 -0
- abstractagent/logic/__init__.py +19 -0
- abstractagent/logic/builtins.py +29 -0
- abstractagent/logic/codeact.py +166 -0
- abstractagent/logic/react.py +126 -0
- abstractagent/logic/types.py +30 -0
- abstractagent/repl.py +457 -0
- abstractagent/sandbox/__init__.py +7 -0
- abstractagent/sandbox/interface.py +22 -0
- abstractagent/sandbox/local.py +68 -0
- abstractagent/tools/__init__.py +58 -0
- abstractagent/tools/code_execution.py +45 -0
- abstractagent/tools/self_improve.py +56 -0
- abstractagent/ui/__init__.py +5 -0
- abstractagent/ui/question.py +197 -0
- abstractagent-0.2.0.dist-info/METADATA +134 -0
- abstractagent-0.2.0.dist-info/RECORD +28 -0
- abstractagent-0.2.0.dist-info/WHEEL +5 -0
- abstractagent-0.2.0.dist-info/entry_points.txt +2 -0
- abstractagent-0.2.0.dist-info/licenses/LICENSE +25 -0
- abstractagent-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Base agent class with common functionality.
|
|
2
|
+
|
|
3
|
+
All agent types (ReAct, CodeAct, etc.) inherit from BaseAgent to get:
|
|
4
|
+
- Runtime access (ledger, run store, cancel)
|
|
5
|
+
- State management (save/load/attach)
|
|
6
|
+
- Async message injection
|
|
7
|
+
- Common lifecycle methods
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from abstractruntime import Runtime, RunState, RunStatus, WorkflowSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseAgent(ABC):
|
|
17
|
+
"""Abstract base class for all agent types.
|
|
18
|
+
|
|
19
|
+
Provides common functionality that all agents need:
|
|
20
|
+
- Runtime integration
|
|
21
|
+
- State persistence
|
|
22
|
+
- Cancellation
|
|
23
|
+
- Ledger access
|
|
24
|
+
- Async message injection
|
|
25
|
+
|
|
26
|
+
Subclasses must implement:
|
|
27
|
+
- _create_workflow(): Return the WorkflowSpec for this agent type
|
|
28
|
+
- start(): Initialize and start a run
|
|
29
|
+
- step(): Execute one step
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
runtime: Runtime,
|
|
35
|
+
tools: Optional[List[Callable]] = None,
|
|
36
|
+
on_step: Optional[Callable[[str, Dict[str, Any]], None]] = None,
|
|
37
|
+
*,
|
|
38
|
+
actor_id: Optional[str] = None,
|
|
39
|
+
session_id: Optional[str] = None,
|
|
40
|
+
):
|
|
41
|
+
"""Initialize the agent.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
runtime: AbstractRuntime instance for durable execution
|
|
45
|
+
tools: List of tool functions decorated with @tool
|
|
46
|
+
on_step: Optional callback for step visibility (step_name, data)
|
|
47
|
+
"""
|
|
48
|
+
self.runtime = runtime
|
|
49
|
+
self.tools = tools or []
|
|
50
|
+
self.on_step = on_step
|
|
51
|
+
self.workflow = self._create_workflow()
|
|
52
|
+
self._current_run_id: Optional[str] = None
|
|
53
|
+
self.actor_id: Optional[str] = actor_id
|
|
54
|
+
self.session_id: Optional[str] = session_id
|
|
55
|
+
self.session_messages: List[Dict[str, Any]] = []
|
|
56
|
+
|
|
57
|
+
def _ensure_actor_id(self) -> str:
|
|
58
|
+
if self.actor_id:
|
|
59
|
+
return self.actor_id
|
|
60
|
+
|
|
61
|
+
from abstractruntime.identity.fingerprint import ActorFingerprint
|
|
62
|
+
import uuid
|
|
63
|
+
|
|
64
|
+
fp = ActorFingerprint.from_metadata(
|
|
65
|
+
kind="agent",
|
|
66
|
+
display_name=self.__class__.__name__,
|
|
67
|
+
metadata={"nonce": uuid.uuid4().hex},
|
|
68
|
+
)
|
|
69
|
+
self.actor_id = fp.actor_id
|
|
70
|
+
return self.actor_id
|
|
71
|
+
|
|
72
|
+
def _ensure_session_id(self) -> str:
|
|
73
|
+
if self.session_id:
|
|
74
|
+
return self.session_id
|
|
75
|
+
import uuid
|
|
76
|
+
|
|
77
|
+
self.session_id = f"sess_{uuid.uuid4().hex}"
|
|
78
|
+
return self.session_id
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def _create_workflow(self) -> WorkflowSpec:
|
|
82
|
+
"""Create the workflow specification for this agent type.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
WorkflowSpec defining the agent's execution graph
|
|
86
|
+
"""
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def start(self, task: str) -> str:
|
|
91
|
+
"""Start a new agent run with a task.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
task: The task description for the agent
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The run_id for this execution
|
|
98
|
+
"""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def step(self) -> RunState:
|
|
103
|
+
"""Execute one step of the agent.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Current RunState after the step
|
|
107
|
+
"""
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
# -------------------------------------------------------------------------
|
|
111
|
+
# Common methods (inherited by all agent types)
|
|
112
|
+
# -------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
def run_to_completion(self) -> RunState:
|
|
115
|
+
"""Run the agent until completion or waiting.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Final RunState (COMPLETED, FAILED, or WAITING)
|
|
119
|
+
"""
|
|
120
|
+
if not self._current_run_id:
|
|
121
|
+
raise RuntimeError("No active run. Call start() first.")
|
|
122
|
+
|
|
123
|
+
state = self.step()
|
|
124
|
+
while state.status == RunStatus.RUNNING:
|
|
125
|
+
state = self.step()
|
|
126
|
+
|
|
127
|
+
return state
|
|
128
|
+
|
|
129
|
+
def get_state(self) -> Optional[RunState]:
|
|
130
|
+
"""Get current agent state.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Current RunState or None if no active run
|
|
134
|
+
"""
|
|
135
|
+
if not self._current_run_id:
|
|
136
|
+
return None
|
|
137
|
+
return self.runtime.get_state(self._current_run_id)
|
|
138
|
+
|
|
139
|
+
def is_waiting(self) -> bool:
|
|
140
|
+
"""Check if agent is waiting for input.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
True if agent is in WAITING status
|
|
144
|
+
"""
|
|
145
|
+
state = self.get_state()
|
|
146
|
+
return state is not None and state.status == RunStatus.WAITING
|
|
147
|
+
|
|
148
|
+
def is_running(self) -> bool:
|
|
149
|
+
"""Check if agent is actively running.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
True if agent is in RUNNING status
|
|
153
|
+
"""
|
|
154
|
+
state = self.get_state()
|
|
155
|
+
return state is not None and state.status == RunStatus.RUNNING
|
|
156
|
+
|
|
157
|
+
def is_complete(self) -> bool:
|
|
158
|
+
"""Check if agent has completed.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
True if agent is in COMPLETED status
|
|
162
|
+
"""
|
|
163
|
+
state = self.get_state()
|
|
164
|
+
return state is not None and state.status == RunStatus.COMPLETED
|
|
165
|
+
|
|
166
|
+
def get_pending_question(self) -> Optional[Dict[str, Any]]:
|
|
167
|
+
"""Get pending question if agent is waiting for user input.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Dict with prompt, choices, allow_free_text, wait_key
|
|
171
|
+
or None if not waiting
|
|
172
|
+
"""
|
|
173
|
+
state = self.get_state()
|
|
174
|
+
if not state or state.status != RunStatus.WAITING or not state.waiting:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
"prompt": state.waiting.prompt,
|
|
179
|
+
"choices": state.waiting.choices,
|
|
180
|
+
"allow_free_text": state.waiting.allow_free_text,
|
|
181
|
+
"wait_key": state.waiting.wait_key,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
def resume(self, response: str) -> RunState:
|
|
185
|
+
"""Resume agent with user response.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
response: User's answer to the pending question
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Updated RunState after resuming
|
|
192
|
+
"""
|
|
193
|
+
if not self._current_run_id:
|
|
194
|
+
raise RuntimeError("No active run.")
|
|
195
|
+
|
|
196
|
+
state = self.get_state()
|
|
197
|
+
if not state or state.status != RunStatus.WAITING:
|
|
198
|
+
raise RuntimeError("Agent is not waiting for input.")
|
|
199
|
+
|
|
200
|
+
wait_key = state.waiting.wait_key if state.waiting else None
|
|
201
|
+
|
|
202
|
+
return self.runtime.resume(
|
|
203
|
+
workflow=self.workflow,
|
|
204
|
+
run_id=self._current_run_id,
|
|
205
|
+
wait_key=wait_key,
|
|
206
|
+
payload={"response": response},
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def attach(self, run_id: str) -> RunState:
|
|
210
|
+
"""Attach to an existing run for resume.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
run_id: ID of the run to attach to
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Current RunState
|
|
217
|
+
"""
|
|
218
|
+
state = self.runtime.get_state(run_id)
|
|
219
|
+
if state.workflow_id != self.workflow.workflow_id:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
f"Run workflow_id mismatch: run has '{state.workflow_id}', "
|
|
222
|
+
f"agent expects '{self.workflow.workflow_id}'."
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if self.actor_id and state.actor_id and self.actor_id != state.actor_id:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"Run actor_id mismatch: run has '{state.actor_id}', agent expects '{self.actor_id}'."
|
|
228
|
+
)
|
|
229
|
+
if self.actor_id is None and state.actor_id:
|
|
230
|
+
self.actor_id = state.actor_id
|
|
231
|
+
|
|
232
|
+
state_session_id = getattr(state, "session_id", None)
|
|
233
|
+
if self.session_id and state_session_id and self.session_id != state_session_id:
|
|
234
|
+
raise ValueError(
|
|
235
|
+
f"Run session_id mismatch: run has '{state_session_id}', agent expects '{self.session_id}'."
|
|
236
|
+
)
|
|
237
|
+
if self.session_id is None and state_session_id:
|
|
238
|
+
self.session_id = state_session_id
|
|
239
|
+
|
|
240
|
+
self._current_run_id = run_id
|
|
241
|
+
return state
|
|
242
|
+
|
|
243
|
+
def save_state(self, filepath: str) -> None:
|
|
244
|
+
"""Save a run reference to file for later resume.
|
|
245
|
+
|
|
246
|
+
The full durable state is owned and persisted by AbstractRuntime's RunStore.
|
|
247
|
+
This method only stores the identifiers needed to re-attach on restart.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
filepath: Path to save state file
|
|
251
|
+
"""
|
|
252
|
+
import json
|
|
253
|
+
from pathlib import Path
|
|
254
|
+
from abstractruntime.storage.in_memory import InMemoryRunStore
|
|
255
|
+
|
|
256
|
+
if not self._current_run_id:
|
|
257
|
+
raise RuntimeError("No active run to save.")
|
|
258
|
+
|
|
259
|
+
if isinstance(self.runtime.run_store, InMemoryRunStore):
|
|
260
|
+
raise RuntimeError(
|
|
261
|
+
"save_state requires a persistent RunStore (e.g. JsonFileRunStore); "
|
|
262
|
+
"the current runtime uses InMemoryRunStore which cannot resume across restarts."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
data = {
|
|
266
|
+
"run_id": self._current_run_id,
|
|
267
|
+
"workflow_id": self.workflow.workflow_id,
|
|
268
|
+
"actor_id": self.actor_id,
|
|
269
|
+
"session_id": self._ensure_session_id(),
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
Path(filepath).write_text(json.dumps(data, indent=2))
|
|
273
|
+
|
|
274
|
+
def load_state(self, filepath: str) -> Optional[RunState]:
|
|
275
|
+
"""Load run reference from file and attach to it.
|
|
276
|
+
|
|
277
|
+
The full durable state is loaded from AbstractRuntime's RunStore.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
filepath: Path to state file
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
RunState if found and valid, None otherwise
|
|
284
|
+
"""
|
|
285
|
+
import json
|
|
286
|
+
from pathlib import Path
|
|
287
|
+
|
|
288
|
+
path = Path(filepath)
|
|
289
|
+
if not path.exists():
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
data = json.loads(path.read_text())
|
|
293
|
+
run_id = data.get("run_id")
|
|
294
|
+
if not run_id:
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
workflow_id = data.get("workflow_id")
|
|
298
|
+
if workflow_id and workflow_id != self.workflow.workflow_id:
|
|
299
|
+
raise ValueError(
|
|
300
|
+
f"Saved workflow_id mismatch: file has '{workflow_id}', "
|
|
301
|
+
f"agent expects '{self.workflow.workflow_id}'."
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
actor_id = data.get("actor_id")
|
|
305
|
+
if actor_id and self.actor_id and actor_id != self.actor_id:
|
|
306
|
+
raise ValueError(
|
|
307
|
+
f"Saved actor_id mismatch: file has '{actor_id}', agent expects '{self.actor_id}'."
|
|
308
|
+
)
|
|
309
|
+
if actor_id and self.actor_id is None:
|
|
310
|
+
self.actor_id = actor_id
|
|
311
|
+
|
|
312
|
+
session_id = data.get("session_id")
|
|
313
|
+
if session_id and self.session_id and session_id != self.session_id:
|
|
314
|
+
raise ValueError(
|
|
315
|
+
f"Saved session_id mismatch: file has '{session_id}', agent expects '{self.session_id}'."
|
|
316
|
+
)
|
|
317
|
+
if session_id and self.session_id is None:
|
|
318
|
+
self.session_id = session_id
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
return self.attach(str(run_id))
|
|
322
|
+
except KeyError as e:
|
|
323
|
+
raise RuntimeError(
|
|
324
|
+
f"Saved run_id '{run_id}' was not found in the configured RunStore. "
|
|
325
|
+
"If you deleted/moved the store directory, this run cannot be resumed."
|
|
326
|
+
) from e
|
|
327
|
+
|
|
328
|
+
def clear_state(self, filepath: str) -> None:
|
|
329
|
+
"""Remove state file after completion.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
filepath: Path to state file
|
|
333
|
+
"""
|
|
334
|
+
from pathlib import Path
|
|
335
|
+
Path(filepath).unlink(missing_ok=True)
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def run_id(self) -> Optional[str]:
|
|
339
|
+
"""Get the current run ID."""
|
|
340
|
+
return self._current_run_id
|
|
341
|
+
|
|
342
|
+
def cancel(self, reason: Optional[str] = None) -> RunState:
|
|
343
|
+
"""Cancel the current run.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
reason: Optional cancellation reason
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Updated RunState with CANCELLED status
|
|
350
|
+
"""
|
|
351
|
+
if not self._current_run_id:
|
|
352
|
+
raise RuntimeError("No active run to cancel.")
|
|
353
|
+
|
|
354
|
+
return self.runtime.cancel_run(self._current_run_id, reason=reason)
|
|
355
|
+
|
|
356
|
+
def get_ledger(self) -> list:
|
|
357
|
+
"""Get ledger entries for the current run.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
List of ledger entries (dicts with effect details)
|
|
361
|
+
"""
|
|
362
|
+
if not self._current_run_id:
|
|
363
|
+
return []
|
|
364
|
+
|
|
365
|
+
return self.runtime.get_ledger(self._current_run_id)
|
|
366
|
+
|
|
367
|
+
def inject_message(self, message: str) -> None:
|
|
368
|
+
"""Inject a message into the agent's inbox for next iteration.
|
|
369
|
+
|
|
370
|
+
The agent will see this message on its next reasoning step.
|
|
371
|
+
Useful for providing guidance or additional context while running.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
message: Message to inject
|
|
375
|
+
"""
|
|
376
|
+
if not self._current_run_id:
|
|
377
|
+
raise RuntimeError("No active run.")
|
|
378
|
+
|
|
379
|
+
state = self.runtime.get_state(self._current_run_id)
|
|
380
|
+
runtime_ns = state.vars.get("_runtime")
|
|
381
|
+
if not isinstance(runtime_ns, dict):
|
|
382
|
+
runtime_ns = {}
|
|
383
|
+
state.vars["_runtime"] = runtime_ns
|
|
384
|
+
|
|
385
|
+
inbox = runtime_ns.get("inbox")
|
|
386
|
+
if not isinstance(inbox, list):
|
|
387
|
+
legacy = state.vars.get("_inbox")
|
|
388
|
+
inbox = legacy if isinstance(legacy, list) else []
|
|
389
|
+
runtime_ns["inbox"] = inbox
|
|
390
|
+
|
|
391
|
+
inbox.append(
|
|
392
|
+
{
|
|
393
|
+
"type": "user_guidance",
|
|
394
|
+
"content": message,
|
|
395
|
+
"timestamp": self.runtime._ctx.now_iso() if hasattr(self.runtime._ctx, "now_iso") else None,
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
state.vars.pop("_inbox", None)
|
|
399
|
+
self.runtime._run_store.save(state)
|
|
400
|
+
|
|
401
|
+
def get_output(self) -> Optional[Dict[str, Any]]:
|
|
402
|
+
"""Get the output from a completed run.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Output dict if completed, None otherwise
|
|
406
|
+
"""
|
|
407
|
+
state = self.get_state()
|
|
408
|
+
if state and state.status == RunStatus.COMPLETED:
|
|
409
|
+
return state.output
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
def get_error(self) -> Optional[str]:
|
|
413
|
+
"""Get the error from a failed run.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
Error string if failed, None otherwise
|
|
417
|
+
"""
|
|
418
|
+
state = self.get_state()
|
|
419
|
+
if state and state.status == RunStatus.FAILED:
|
|
420
|
+
return state.error
|
|
421
|
+
return None
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""CodeAct agent implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from abstractcore.tools import ToolDefinition
|
|
8
|
+
from abstractruntime import RunState, Runtime, WorkflowSpec
|
|
9
|
+
|
|
10
|
+
from .base import BaseAgent
|
|
11
|
+
from ..adapters.codeact_runtime import create_codeact_workflow
|
|
12
|
+
from ..logic.builtins import ASK_USER_TOOL
|
|
13
|
+
from ..logic.codeact import CodeActLogic
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _tool_definitions_from_callables(tools: List[Callable[..., Any]]) -> List[ToolDefinition]:
|
|
17
|
+
tool_defs: List[ToolDefinition] = []
|
|
18
|
+
for t in tools:
|
|
19
|
+
tool_def = getattr(t, "_tool_definition", None)
|
|
20
|
+
if tool_def is None:
|
|
21
|
+
tool_def = ToolDefinition.from_function(t)
|
|
22
|
+
tool_defs.append(tool_def)
|
|
23
|
+
return tool_defs
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _copy_messages(messages: Any) -> List[Dict[str, Any]]:
|
|
27
|
+
if not isinstance(messages, list):
|
|
28
|
+
return []
|
|
29
|
+
out: List[Dict[str, Any]] = []
|
|
30
|
+
for m in messages:
|
|
31
|
+
if isinstance(m, dict):
|
|
32
|
+
out.append(dict(m))
|
|
33
|
+
return out
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CodeActAgent(BaseAgent):
|
|
37
|
+
"""Agent that primarily acts by executing Python code snippets."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
runtime: Runtime,
|
|
43
|
+
tools: Optional[List[Callable[..., Any]]] = None,
|
|
44
|
+
on_step: Optional[Callable[[str, Dict[str, Any]], None]] = None,
|
|
45
|
+
max_iterations: int = 25,
|
|
46
|
+
max_history_messages: int = -1,
|
|
47
|
+
max_tokens: Optional[int] = 32768,
|
|
48
|
+
actor_id: Optional[str] = None,
|
|
49
|
+
session_id: Optional[str] = None,
|
|
50
|
+
):
|
|
51
|
+
self._max_iterations = int(max_iterations)
|
|
52
|
+
if self._max_iterations < 1:
|
|
53
|
+
self._max_iterations = 1
|
|
54
|
+
self._max_history_messages = int(max_history_messages)
|
|
55
|
+
# -1 means unlimited (send all messages), otherwise must be >= 1
|
|
56
|
+
if self._max_history_messages != -1 and self._max_history_messages < 1:
|
|
57
|
+
self._max_history_messages = 1
|
|
58
|
+
self._max_tokens = max_tokens
|
|
59
|
+
|
|
60
|
+
self.logic: Optional[CodeActLogic] = None
|
|
61
|
+
super().__init__(
|
|
62
|
+
runtime=runtime,
|
|
63
|
+
tools=tools,
|
|
64
|
+
on_step=on_step,
|
|
65
|
+
actor_id=actor_id,
|
|
66
|
+
session_id=session_id,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _create_workflow(self) -> WorkflowSpec:
|
|
70
|
+
tool_defs = _tool_definitions_from_callables(self.tools)
|
|
71
|
+
tool_defs = [ASK_USER_TOOL, *tool_defs]
|
|
72
|
+
logic = CodeActLogic(
|
|
73
|
+
tools=tool_defs,
|
|
74
|
+
max_history_messages=self._max_history_messages,
|
|
75
|
+
max_tokens=self._max_tokens,
|
|
76
|
+
)
|
|
77
|
+
self.logic = logic
|
|
78
|
+
return create_codeact_workflow(logic=logic, on_step=self.on_step)
|
|
79
|
+
|
|
80
|
+
def start(self, task: str) -> str:
|
|
81
|
+
task = str(task or "").strip()
|
|
82
|
+
if not task:
|
|
83
|
+
raise ValueError("task must be a non-empty string")
|
|
84
|
+
|
|
85
|
+
vars: Dict[str, Any] = {
|
|
86
|
+
"context": {"task": task, "messages": _copy_messages(self.session_messages)},
|
|
87
|
+
"scratchpad": {"iteration": 0, "max_iterations": int(self._max_iterations)},
|
|
88
|
+
"_runtime": {"inbox": []},
|
|
89
|
+
"_temp": {},
|
|
90
|
+
# 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
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
run_id = self.runtime.start(
|
|
103
|
+
workflow=self.workflow,
|
|
104
|
+
vars=vars,
|
|
105
|
+
actor_id=self._ensure_actor_id(),
|
|
106
|
+
session_id=self._ensure_session_id(),
|
|
107
|
+
)
|
|
108
|
+
self._current_run_id = run_id
|
|
109
|
+
return run_id
|
|
110
|
+
|
|
111
|
+
def get_limit_status(self) -> Dict[str, Any]:
|
|
112
|
+
"""Get current limit status for the active run.
|
|
113
|
+
|
|
114
|
+
Returns a structured dict with information about iterations, tokens,
|
|
115
|
+
and history limits, including whether warning thresholds are reached.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Dict with "iterations", "tokens", and "history" status info,
|
|
119
|
+
or empty dict if no active run.
|
|
120
|
+
"""
|
|
121
|
+
if self._current_run_id is None:
|
|
122
|
+
return {}
|
|
123
|
+
return self.runtime.get_limit_status(self._current_run_id)
|
|
124
|
+
|
|
125
|
+
def update_limits(self, **updates: Any) -> None:
|
|
126
|
+
"""Update limits mid-session.
|
|
127
|
+
|
|
128
|
+
Only allowed limit keys are updated; unknown keys are ignored.
|
|
129
|
+
Allowed keys: max_iterations, max_tokens, max_output_tokens,
|
|
130
|
+
max_history_messages, warn_iterations_pct, warn_tokens_pct.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
**updates: Limit key-value pairs to update
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
RuntimeError: If no active run
|
|
137
|
+
"""
|
|
138
|
+
if self._current_run_id is None:
|
|
139
|
+
raise RuntimeError("No active run. Call start() first.")
|
|
140
|
+
self.runtime.update_limits(self._current_run_id, updates)
|
|
141
|
+
|
|
142
|
+
def step(self) -> RunState:
|
|
143
|
+
if not self._current_run_id:
|
|
144
|
+
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)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def create_codeact_agent(
|
|
149
|
+
*,
|
|
150
|
+
provider: str = "ollama",
|
|
151
|
+
model: str = "qwen3:1.7b-q4_K_M",
|
|
152
|
+
tools: Optional[List[Callable[..., Any]]] = None,
|
|
153
|
+
on_step: Optional[Callable[[str, Dict[str, Any]], None]] = None,
|
|
154
|
+
max_iterations: int = 25,
|
|
155
|
+
max_history_messages: int = -1,
|
|
156
|
+
max_tokens: Optional[int] = 32768,
|
|
157
|
+
llm_kwargs: Optional[Dict[str, Any]] = None,
|
|
158
|
+
run_store: Optional[Any] = None,
|
|
159
|
+
ledger_store: Optional[Any] = None,
|
|
160
|
+
actor_id: Optional[str] = None,
|
|
161
|
+
session_id: Optional[str] = None,
|
|
162
|
+
) -> CodeActAgent:
|
|
163
|
+
"""Factory: create a CodeActAgent with a local AbstractCore-backed runtime."""
|
|
164
|
+
|
|
165
|
+
from abstractruntime.integrations.abstractcore import MappingToolExecutor, create_local_runtime
|
|
166
|
+
|
|
167
|
+
if tools is None:
|
|
168
|
+
from ..tools.code_execution import execute_python
|
|
169
|
+
|
|
170
|
+
tools = [execute_python]
|
|
171
|
+
|
|
172
|
+
runtime = create_local_runtime(
|
|
173
|
+
provider=provider,
|
|
174
|
+
model=model,
|
|
175
|
+
llm_kwargs=llm_kwargs,
|
|
176
|
+
run_store=run_store,
|
|
177
|
+
ledger_store=ledger_store,
|
|
178
|
+
tool_executor=MappingToolExecutor.from_tools(list(tools)),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return CodeActAgent(
|
|
182
|
+
runtime=runtime,
|
|
183
|
+
tools=list(tools),
|
|
184
|
+
on_step=on_step,
|
|
185
|
+
max_iterations=max_iterations,
|
|
186
|
+
max_history_messages=max_history_messages,
|
|
187
|
+
max_tokens=max_tokens,
|
|
188
|
+
actor_id=actor_id,
|
|
189
|
+
session_id=session_id,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
__all__ = ["CodeActAgent", "create_codeact_workflow", "create_codeact_agent"]
|
|
194
|
+
|