abstractflow 0.3.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.
@@ -62,27 +62,35 @@ def get_interface_specs() -> Dict[str, VisualFlowInterfaceSpec]:
62
62
  return {
63
63
  ABSTRACTCODE_AGENT_V1: VisualFlowInterfaceSpec(
64
64
  interface_id=ABSTRACTCODE_AGENT_V1,
65
- label="AbstractCode Agent (v1)",
65
+ label="RunnableFlow (v1)",
66
66
  description=(
67
- "Host-configurable request → response contract for running a workflow as an AbstractCode agent."
67
+ "Host-configurable prompt → response contract for running a workflow in chat-like clients (AbstractCode, AbstractObserver, etc)."
68
68
  ),
69
- # NOTE: We require host routing/policy pins (provider/model/tools) so workflows
70
- # can be driven by AbstractCode without hardcoding node configs.
71
69
  required_start_outputs={
72
- "request": "string",
73
70
  "provider": "provider",
74
71
  "model": "model",
75
- "tools": "tools",
72
+ "prompt": "string",
73
+ },
74
+ required_end_inputs={
75
+ "response": "string",
76
+ "success": "boolean",
77
+ "meta": "object",
76
78
  },
77
- required_end_inputs={"response": "string"},
78
79
  recommended_start_outputs={
80
+ "use_context": "boolean",
81
+ "memory": "memory",
79
82
  "context": "object",
83
+ "system": "string",
84
+ "tools": "tools",
80
85
  "max_iterations": "number",
86
+ "max_in_tokens": "number",
87
+ "temperature": "number",
88
+ "seed": "number",
89
+ "resp_schema": "object",
81
90
  },
82
91
  recommended_end_inputs={
83
- "meta": "object",
92
+ # Optional but commonly wired for host UX:
84
93
  "scratchpad": "object",
85
- "raw_result": "object",
86
94
  },
87
95
  ),
88
96
  }
@@ -211,6 +219,39 @@ def apply_visual_flow_interface_scaffold(
211
219
  pins.insert(0, {"id": pin_id, "label": "", "type": "execution"})
212
220
  return True
213
221
 
222
+ def _reorder_pins(pins: list[Any], *, desired_ids: list[str]) -> bool:
223
+ """Reorder pins in-place so interface pins appear in a stable, readable order."""
224
+ if not isinstance(pins, list) or not desired_ids:
225
+ return False
226
+ ordered: list[Any] = []
227
+ seen: set[str] = set()
228
+
229
+ def _first_pin(pid: str) -> Any | None:
230
+ for p in pins:
231
+ if isinstance(p, dict) and p.get("id") == pid:
232
+ return p
233
+ return None
234
+
235
+ for pid in desired_ids:
236
+ if pid in seen:
237
+ continue
238
+ p = _first_pin(pid)
239
+ if p is None:
240
+ continue
241
+ ordered.append(p)
242
+ seen.add(pid)
243
+
244
+ for p in pins:
245
+ pid = p.get("id") if isinstance(p, dict) else None
246
+ if isinstance(pid, str) and pid in seen:
247
+ continue
248
+ ordered.append(p)
249
+
250
+ if ordered == pins:
251
+ return False
252
+ pins[:] = ordered
253
+ return True
254
+
214
255
  # Desired pins (required + optional recommended).
215
256
  start_pins = dict(spec.required_start_outputs)
216
257
  end_pins = dict(spec.required_end_inputs)
@@ -312,6 +353,24 @@ def apply_visual_flow_interface_scaffold(
312
353
  for pid, t in start_pins.items():
313
354
  changed = _ensure_pin(outputs, pin_id=str(pid), type_str=str(t), label=str(pid)) or changed
314
355
 
356
+ desired_start_order = [
357
+ "exec-out",
358
+ "use_context",
359
+ "memory",
360
+ "context",
361
+ "provider",
362
+ "model",
363
+ "system",
364
+ "prompt",
365
+ "tools",
366
+ "max_iterations",
367
+ "max_in_tokens",
368
+ "temperature",
369
+ "seed",
370
+ "resp_schema",
371
+ ]
372
+ changed = _reorder_pins(outputs, desired_ids=desired_start_order) or changed
373
+
315
374
  # Ensure pins on all end nodes.
316
375
  for end in end_nodes:
317
376
  end_data = getattr(end, "data", None)
@@ -324,10 +383,45 @@ def apply_visual_flow_interface_scaffold(
324
383
  inputs = []
325
384
  end_data["inputs"] = inputs
326
385
  changed = True
386
+
387
+ # Backward-compat cleanup: remove deprecated interface pins (`result` / `raw_result`)
388
+ # when they are not part of the current desired contract.
389
+ deprecated_end_pins = {"result", "raw_result"}
390
+ if not any(pid in end_pins for pid in deprecated_end_pins):
391
+ removed: set[str] = set()
392
+ kept: list[Any] = []
393
+ for p in inputs:
394
+ pid = p.get("id") if isinstance(p, dict) else None
395
+ if isinstance(pid, str) and pid in deprecated_end_pins:
396
+ removed.add(pid)
397
+ changed = True
398
+ continue
399
+ kept.append(p)
400
+ if removed:
401
+ inputs[:] = kept
402
+ # Remove edges that targeted the deprecated pins (best-effort).
403
+ try:
404
+ flow_edges = getattr(flow, "edges", None)
405
+ if isinstance(flow_edges, list):
406
+ flow.edges = [
407
+ e
408
+ for e in flow_edges
409
+ if not (
410
+ getattr(e, "target", None) == getattr(end, "id", None)
411
+ and getattr(e, "targetHandle", None) in removed
412
+ )
413
+ ]
414
+ except Exception:
415
+ pass
416
+
327
417
  changed = _ensure_exec_pin(inputs, pin_id="exec-in", direction="in") or changed
328
418
  for pid, t in end_pins.items():
329
419
  changed = _ensure_pin(inputs, pin_id=str(pid), type_str=str(t), label=str(pid)) or changed
330
420
 
421
+ # Keep interface pins in a predictable order for UX.
422
+ desired_end_order = ["exec-in", "response", "success", "meta", "scratchpad"]
423
+ changed = _reorder_pins(inputs, desired_ids=desired_end_order) or changed
424
+
331
425
  # Write back nodes list if it was reconstructed.
332
426
  try:
333
427
  flow.nodes = nodes # type: ignore[assignment]
@@ -344,4 +438,3 @@ def apply_visual_flow_interface_scaffold(
344
438
  pass
345
439
 
346
440
  return bool(changed)
347
-
@@ -23,6 +23,9 @@ class PinType(str, Enum):
23
23
  NUMBER = "number" # Green #00FF00 - Integer/Float
24
24
  BOOLEAN = "boolean" # Red #FF0000 - True/False
25
25
  OBJECT = "object" # Cyan #00FFFF - JSON objects
26
+ MEMORY = "memory" # Mint - Memory configuration object (KG/span/session controls)
27
+ ASSERTION = "assertion" # Teal - KG assertion object (subject/predicate/object + metadata)
28
+ ASSERTIONS = "assertions" # Teal - List of KG assertion objects (assertion[])
26
29
  ARRAY = "array" # Orange #FF8800 - Collections
27
30
  TOOLS = "tools" # Orange - Tool allowlist (string[])
28
31
  PROVIDER = "provider" # Cyan-blue - LLM provider id/name (string-like)
@@ -56,6 +59,8 @@ class NodeType(str, Enum):
56
59
  POWER = "power"
57
60
  ABS = "abs"
58
61
  ROUND = "round"
62
+ RANDOM_INT = "random_int"
63
+ RANDOM_FLOAT = "random_float"
59
64
  # String
60
65
  CONCAT = "concat"
61
66
  SPLIT = "split"
@@ -67,6 +72,8 @@ class NodeType(str, Enum):
67
72
  TRIM = "trim"
68
73
  SUBSTRING = "substring"
69
74
  LENGTH = "length"
75
+ CONTAINS = "contains"
76
+ REPLACE = "replace"
70
77
  # Control
71
78
  IF = "if"
72
79
  SWITCH = "switch"
@@ -85,18 +92,28 @@ class NodeType(str, Enum):
85
92
  SET = "set"
86
93
  MERGE = "merge"
87
94
  MAKE_ARRAY = "make_array"
95
+ MAKE_OBJECT = "make_object"
96
+ MAKE_CONTEXT = "make_context"
97
+ MAKE_META = "make_meta"
98
+ MAKE_SCRATCHPAD = "make_scratchpad"
99
+ GET_ELEMENT = "get_element"
100
+ GET_RANDOM_ELEMENT = "get_random_element"
88
101
  ARRAY_MAP = "array_map"
89
102
  ARRAY_FILTER = "array_filter"
90
103
  ARRAY_CONCAT = "array_concat"
91
104
  ARRAY_LENGTH = "array_length"
92
105
  ARRAY_APPEND = "array_append"
93
106
  ARRAY_DEDUP = "array_dedup"
107
+ HAS_TOOLS = "has_tools"
108
+ ADD_MESSAGE = "add_message"
109
+ GET_CONTEXT = "get_context"
94
110
  GET_VAR = "get_var"
95
111
  SET_VAR = "set_var"
96
112
  SET_VARS = "set_vars"
97
113
  SET_VAR_PROPERTY = "set_var_property"
98
114
  PARSE_JSON = "parse_json"
99
115
  STRINGIFY_JSON = "stringify_json"
116
+ FORMAT_TOOL_RESULTS = "format_tool_results"
100
117
  AGENT_TRACE_REPORT = "agent_trace_report"
101
118
  BREAK_OBJECT = "break_object"
102
119
  SYSTEM_DATETIME = "system_datetime"
@@ -110,6 +127,7 @@ class NodeType(str, Enum):
110
127
  LITERAL_JSON = "literal_json"
111
128
  JSON_SCHEMA = "json_schema"
112
129
  LITERAL_ARRAY = "literal_array"
130
+ TOOL_PARAMETERS = "tool_parameters"
113
131
  # Effects
114
132
  ASK_USER = "ask_user"
115
133
  ANSWER_USER = "answer_user"
@@ -121,8 +139,15 @@ class NodeType(str, Enum):
121
139
  WRITE_FILE = "write_file"
122
140
  MEMORY_NOTE = "memory_note"
123
141
  MEMORY_QUERY = "memory_query"
142
+ MEMORY_TAG = "memory_tag"
143
+ MEMORY_COMPACT = "memory_compact"
124
144
  MEMORY_REHYDRATE = "memory_rehydrate"
145
+ MEMORY_KG_ASSERT = "memory_kg_assert"
146
+ MEMORY_KG_QUERY = "memory_kg_query"
147
+ MEMORY_KG_RESOLVE = "memory_kg_resolve"
148
+ MEMACT_COMPOSE = "memact_compose"
125
149
  TOOL_CALLS = "tool_calls"
150
+ CALL_TOOL = "call_tool"
126
151
  TOOLS_ALLOWLIST = "tools_allowlist"
127
152
  BOOL_VAR = "bool_var"
128
153
  VAR_DECL = "var_decl"
@@ -176,7 +201,7 @@ class VisualFlow(BaseModel):
176
201
  name: str
177
202
  description: str = ""
178
203
  # Optional interface markers (host contracts).
179
- # Example: ["abstractcode.agent.v1"] to indicate this workflow can be run as an AbstractCode agent.
204
+ # Example: ["abstractcode.agent.v1"] to indicate this workflow can be run as a RunnableFlow in chat-like clients.
180
205
  interfaces: List[str] = Field(default_factory=list)
181
206
  nodes: List[VisualNode] = Field(default_factory=list)
182
207
  edges: List[VisualEdge] = Field(default_factory=list)
@@ -37,18 +37,27 @@ class VisualSessionRunner(FlowRunner):
37
37
  def event_listener_run_ids(self) -> List[str]:
38
38
  return list(self._event_listener_run_ids)
39
39
 
40
- def start(self, input_data: Optional[Dict[str, Any]] = None) -> str:
41
- run_id = super().start(input_data)
40
+ def start(
41
+ self,
42
+ input_data: Optional[Dict[str, Any]] = None,
43
+ *,
44
+ actor_id: Optional[str] = None,
45
+ session_id: Optional[str] = None,
46
+ ) -> str:
47
+ run_id = super().start(input_data, actor_id=actor_id, session_id=session_id)
42
48
 
43
49
  # Default session_id to the root run_id for session-scoped events.
50
+ # If a session_id is explicitly provided, preserve it.
51
+ effective_session_id = session_id
44
52
  try:
45
53
  state = self.runtime.get_state(run_id)
46
54
  if not getattr(state, "session_id", None):
47
55
  state.session_id = run_id # type: ignore[attr-defined]
48
56
  self.runtime.run_store.save(state)
57
+ effective_session_id = str(getattr(state, "session_id", None) or run_id).strip() or run_id
49
58
  except Exception:
50
59
  # Best-effort; session-scoped keys will fall back to run_id if missing.
51
- pass
60
+ effective_session_id = str(session_id).strip() if isinstance(session_id, str) and session_id.strip() else run_id
52
61
 
53
62
  if not self._event_listener_specs:
54
63
  return run_id
@@ -59,7 +68,8 @@ class VisualSessionRunner(FlowRunner):
59
68
  child_run_id = self.runtime.start(
60
69
  workflow=spec,
61
70
  vars={},
62
- session_id=run_id,
71
+ session_id=effective_session_id,
72
+ actor_id=actor_id,
63
73
  parent_run_id=run_id,
64
74
  )
65
75
  # Advance the listener to its first WAIT_EVENT (On Event node).
@@ -72,7 +82,13 @@ class VisualSessionRunner(FlowRunner):
72
82
 
73
83
  return run_id
74
84
 
75
- def run(self, input_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
85
+ def run(
86
+ self,
87
+ input_data: Optional[Dict[str, Any]] = None,
88
+ *,
89
+ actor_id: Optional[str] = None,
90
+ session_id: Optional[str] = None,
91
+ ) -> Dict[str, Any]:
76
92
  """Execute the root run and drive session-level listener runs.
77
93
 
78
94
  Rationale:
@@ -81,7 +97,7 @@ class VisualSessionRunner(FlowRunner):
81
97
  """
82
98
  from abstractruntime.core.models import RunStatus, WaitReason
83
99
 
84
- run_id = self.start(input_data)
100
+ run_id = self.start(input_data, actor_id=actor_id, session_id=session_id)
85
101
  runtime = self.runtime
86
102
 
87
103
  def _list_session_runs() -> List[str]:
@@ -150,7 +166,7 @@ class VisualSessionRunner(FlowRunner):
150
166
  runtime.cancel_run(cid, reason="Session completed")
151
167
  except Exception:
152
168
  pass
153
- return state.output or {}
169
+ return self._normalize_completed_output(state.output)
154
170
  # Otherwise, keep driving until children settle into waits/terminal.
155
171
  continue
156
172
 
@@ -164,5 +180,3 @@ class VisualSessionRunner(FlowRunner):
164
180
  "state": state,
165
181
  "wait_key": state.waiting.wait_key if state.waiting else None,
166
182
  }
167
-
168
-
@@ -1,251 +1,20 @@
1
- """Workspace-scoped tool execution helpers.
1
+ """Workspace-scoped tool execution helpers (AbstractFlow re-export).
2
2
 
3
- These are host-friendly utilities to scope filesystem-ish tool calls (files + shell)
4
- to a single "workspace root" folder:
5
-
6
- - Relative paths resolve under `workspace_root`.
7
- - Absolute paths are only allowed if they remain under `workspace_root`.
8
- - `execute_command` defaults to `working_directory=workspace_root` when not specified.
9
-
10
- This is implemented as a thin wrapper around an AbstractRuntime ToolExecutor that
11
- rewrites/validates tool call arguments before delegating.
3
+ The core implementation lives in AbstractRuntime's AbstractCore integration so it can
4
+ be shared across hosts/clients. AbstractFlow re-exports the types to preserve existing
5
+ imports (`abstractflow.visual.workspace_scoped_tools`).
12
6
  """
13
7
 
14
8
  from __future__ import annotations
15
9
 
16
- from dataclasses import dataclass
17
- import os
18
- from pathlib import Path
19
- from typing import Any, Dict, List, Optional, Tuple
20
-
21
-
22
- def _resolve_no_strict(path: Path) -> Path:
23
- """Resolve without requiring the path to exist (best-effort across py versions)."""
24
- try:
25
- return path.resolve(strict=False)
26
- except TypeError: # pragma: no cover (older python)
27
- return path.resolve()
28
-
29
-
30
- def _find_repo_root_from_here(*, start: Path, max_hops: int = 10) -> Optional[Path]:
31
- """Best-effort monorepo root detection for local/dev runs."""
32
- cur = _resolve_no_strict(start)
33
- for _ in range(max_hops):
34
- docs = cur / "docs" / "KnowledgeBase.md"
35
- if docs.exists():
36
- return cur
37
- if (cur / "abstractflow").exists() and (cur / "abstractcore").exists() and (cur / "abstractruntime").exists():
38
- return cur
39
- nxt = cur.parent
40
- if nxt == cur:
41
- break
42
- cur = nxt
43
- return None
44
-
45
-
46
- def resolve_workspace_base_dir() -> Path:
47
- """Base directory against which relative workspace roots are resolved.
48
-
49
- Priority:
50
- - `ABSTRACTFLOW_WORKSPACE_BASE_DIR` env var, if set.
51
- - Best-effort monorepo root detection from this file location.
52
- - Current working directory.
53
- """
54
- env = os.getenv("ABSTRACTFLOW_WORKSPACE_BASE_DIR")
55
- if isinstance(env, str) and env.strip():
56
- return _resolve_no_strict(Path(env.strip()).expanduser())
57
-
58
- here_dir = Path(__file__).resolve().parent
59
- guessed = _find_repo_root_from_here(start=here_dir)
60
- if guessed is not None:
61
- return guessed
62
-
63
- return _resolve_no_strict(Path.cwd())
64
-
65
-
66
- def _resolve_under_root(*, root: Path, user_path: str) -> Path:
67
- """Resolve a user-provided path under a workspace root and ensure it doesn't escape."""
68
- p = Path(str(user_path or "").strip()).expanduser()
69
- if not p.is_absolute():
70
- p = root / p
71
- resolved = _resolve_no_strict(p)
72
- root_resolved = _resolve_no_strict(root)
73
- try:
74
- resolved.relative_to(root_resolved)
75
- except Exception as e:
76
- raise ValueError(f"Path escapes workspace_root: '{user_path}'") from e
77
- return resolved
78
-
79
-
80
- def _normalize_arguments(raw: Any) -> Dict[str, Any]:
81
- if raw is None:
82
- return {}
83
- if isinstance(raw, dict):
84
- return dict(raw)
85
- # Some models emit JSON strings for args.
86
- if isinstance(raw, str) and raw.strip():
87
- import json
88
-
89
- try:
90
- parsed = json.loads(raw)
91
- except Exception:
92
- return {}
93
- return dict(parsed) if isinstance(parsed, dict) else {}
94
- return {}
95
-
96
-
97
- @dataclass(frozen=True)
98
- class WorkspaceScope:
99
- root: Path
100
-
101
- @classmethod
102
- def from_input_data(
103
- cls, input_data: Dict[str, Any], *, key: str = "workspace_root", base_dir: Optional[Path] = None
104
- ) -> Optional["WorkspaceScope"]:
105
- raw = input_data.get(key)
106
- if not isinstance(raw, str) or not raw.strip():
107
- return None
108
- base = base_dir or resolve_workspace_base_dir()
109
- root = Path(raw.strip()).expanduser()
110
- if not root.is_absolute():
111
- root = base / root
112
- root = _resolve_no_strict(root)
113
- if root.exists() and not root.is_dir():
114
- raise ValueError(f"workspace_root must be a directory (got file): {raw}")
115
- root.mkdir(parents=True, exist_ok=True)
116
- return cls(root=root)
10
+ from typing import Any
117
11
 
118
-
119
- class WorkspaceScopedToolExecutor:
120
- """Wrap another ToolExecutor and scope filesystem-ish tool calls to a workspace root."""
121
-
122
- def __init__(self, *, scope: WorkspaceScope, delegate: Any):
123
- self._scope = scope
124
- self._delegate = delegate
125
-
126
- def set_timeout_s(self, timeout_s: Optional[float]) -> None: # pragma: no cover (depends on delegate)
127
- setter = getattr(self._delegate, "set_timeout_s", None)
128
- if callable(setter):
129
- setter(timeout_s)
130
-
131
- def execute(self, *, tool_calls: List[Dict[str, Any]]) -> Dict[str, Any]:
132
- # Preprocess: rewrite and pre-block invalid calls so we don't crash the whole run.
133
- blocked: Dict[Tuple[int, str], Dict[str, Any]] = {}
134
- to_execute: List[Dict[str, Any]] = []
135
-
136
- for i, tc in enumerate(tool_calls or []):
137
- name = str(tc.get("name", "") or "")
138
- call_id = str(tc.get("call_id") or tc.get("id") or f"call_{i}")
139
- args = _normalize_arguments(tc.get("arguments"))
140
-
141
- try:
142
- rewritten_args = self._rewrite_args(tool_name=name, args=args)
143
- except Exception as e:
144
- blocked[(i, call_id)] = {
145
- "call_id": call_id,
146
- "name": name,
147
- "success": False,
148
- "output": None,
149
- "error": str(e),
150
- }
151
- continue
152
-
153
- rewritten = dict(tc)
154
- rewritten["name"] = name
155
- rewritten["call_id"] = call_id
156
- rewritten["arguments"] = rewritten_args
157
- to_execute.append(rewritten)
158
-
159
- delegate_result = self._delegate.execute(tool_calls=to_execute)
160
-
161
- # If the delegate didn't execute tools, we can't merge blocked results meaningfully.
162
- if not isinstance(delegate_result, dict) or delegate_result.get("mode") != "executed":
163
- return delegate_result
164
-
165
- results = delegate_result.get("results")
166
- if not isinstance(results, list):
167
- results = []
168
-
169
- by_id: Dict[str, Dict[str, Any]] = {}
170
- for r in results:
171
- if not isinstance(r, dict):
172
- continue
173
- rid = str(r.get("call_id") or "")
174
- if rid:
175
- by_id[rid] = r
176
-
177
- merged: List[Dict[str, Any]] = []
178
- for i, tc in enumerate(tool_calls or []):
179
- call_id = str(tc.get("call_id") or tc.get("id") or f"call_{i}")
180
- key = (i, call_id)
181
- if key in blocked:
182
- merged.append(blocked[key])
183
- continue
184
- r = by_id.get(call_id)
185
- if r is None:
186
- merged.append(
187
- {
188
- "call_id": call_id,
189
- "name": str(tc.get("name", "") or ""),
190
- "success": False,
191
- "output": None,
192
- "error": "Tool result missing (internal error)",
193
- }
194
- )
195
- continue
196
- merged.append(r)
197
-
198
- return {"mode": "executed", "results": merged}
199
-
200
- def _rewrite_args(self, *, tool_name: str, args: Dict[str, Any]) -> Dict[str, Any]:
201
- """Rewrite tool args so file operations are scoped under workspace_root."""
202
- root = self._scope.root
203
- out = dict(args or {})
204
-
205
- def _rewrite_path_field(field: str, *, default_to_root: bool = False) -> None:
206
- raw = out.get(field)
207
- if (raw is None or (isinstance(raw, str) and not raw.strip())) and default_to_root:
208
- out[field] = str(_resolve_no_strict(root))
209
- return
210
- if raw is None:
211
- return
212
- if not isinstance(raw, str):
213
- raw = str(raw)
214
- resolved = _resolve_under_root(root=root, user_path=raw)
215
- out[field] = str(resolved)
216
-
217
- # Filesystem-ish tools (AbstractCore common tools)
218
- if tool_name == "list_files":
219
- _rewrite_path_field("directory_path", default_to_root=True)
220
- return out
221
- if tool_name == "search_files":
222
- _rewrite_path_field("path", default_to_root=True)
223
- return out
224
- if tool_name == "analyze_code":
225
- _rewrite_path_field("file_path")
226
- if "file_path" not in out:
227
- raise ValueError("analyze_code requires file_path")
228
- return out
229
- if tool_name == "read_file":
230
- _rewrite_path_field("file_path")
231
- if "file_path" not in out:
232
- raise ValueError("read_file requires file_path")
233
- return out
234
- if tool_name == "write_file":
235
- _rewrite_path_field("file_path")
236
- if "file_path" not in out:
237
- raise ValueError("write_file requires file_path")
238
- return out
239
- if tool_name == "edit_file":
240
- _rewrite_path_field("file_path")
241
- if "file_path" not in out:
242
- raise ValueError("edit_file requires file_path")
243
- return out
244
- if tool_name == "execute_command":
245
- _rewrite_path_field("working_directory", default_to_root=True)
246
- return out
247
-
248
- return out
12
+ from abstractruntime.integrations.abstractcore.workspace_scoped_tools import ( # noqa: F401
13
+ WorkspaceScope,
14
+ WorkspaceScopedToolExecutor,
15
+ resolve_workspace_base_dir,
16
+ resolve_user_path,
17
+ )
249
18
 
250
19
 
251
20
  def build_scoped_tool_executor(*, scope: WorkspaceScope) -> Any:
@@ -258,4 +27,3 @@ def build_scoped_tool_executor(*, scope: WorkspaceScope) -> Any:
258
27
 
259
28
 
260
29
 
261
-