aethergraph 0.1.0a3__py3-none-any.whl → 0.1.0a4__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.
Files changed (113) hide show
  1. aethergraph/api/v1/artifacts.py +23 -4
  2. aethergraph/api/v1/schemas.py +7 -0
  3. aethergraph/api/v1/session.py +123 -4
  4. aethergraph/config/config.py +2 -0
  5. aethergraph/config/search.py +49 -0
  6. aethergraph/contracts/services/channel.py +18 -1
  7. aethergraph/contracts/services/execution.py +58 -0
  8. aethergraph/contracts/services/llm.py +26 -0
  9. aethergraph/contracts/services/memory.py +10 -4
  10. aethergraph/contracts/services/planning.py +53 -0
  11. aethergraph/contracts/storage/event_log.py +8 -0
  12. aethergraph/contracts/storage/search_backend.py +47 -0
  13. aethergraph/contracts/storage/vector_index.py +73 -0
  14. aethergraph/core/graph/action_spec.py +76 -0
  15. aethergraph/core/graph/graph_fn.py +75 -2
  16. aethergraph/core/graph/graphify.py +74 -2
  17. aethergraph/core/runtime/graph_runner.py +2 -1
  18. aethergraph/core/runtime/node_context.py +66 -3
  19. aethergraph/core/runtime/node_services.py +8 -0
  20. aethergraph/core/runtime/run_manager.py +263 -271
  21. aethergraph/core/runtime/run_types.py +54 -1
  22. aethergraph/core/runtime/runtime_env.py +35 -14
  23. aethergraph/core/runtime/runtime_services.py +308 -18
  24. aethergraph/plugins/agents/default_chat_agent.py +266 -74
  25. aethergraph/plugins/agents/default_chat_agent_v2.py +487 -0
  26. aethergraph/plugins/channel/adapters/webui.py +69 -21
  27. aethergraph/plugins/channel/routes/webui_routes.py +8 -48
  28. aethergraph/runtime/__init__.py +12 -0
  29. aethergraph/server/app_factory.py +3 -0
  30. aethergraph/server/ui_static/assets/index-CFktGdbW.js +4913 -0
  31. aethergraph/server/ui_static/assets/index-DcfkFlTA.css +1 -0
  32. aethergraph/server/ui_static/index.html +2 -2
  33. aethergraph/services/artifacts/facade.py +157 -21
  34. aethergraph/services/artifacts/types.py +35 -0
  35. aethergraph/services/artifacts/utils.py +42 -0
  36. aethergraph/services/channel/channel_bus.py +3 -1
  37. aethergraph/services/channel/event_hub copy.py +55 -0
  38. aethergraph/services/channel/event_hub.py +81 -0
  39. aethergraph/services/channel/factory.py +3 -2
  40. aethergraph/services/channel/session.py +709 -74
  41. aethergraph/services/container/default_container.py +69 -7
  42. aethergraph/services/execution/__init__.py +0 -0
  43. aethergraph/services/execution/local_python.py +118 -0
  44. aethergraph/services/indices/__init__.py +0 -0
  45. aethergraph/services/indices/global_indices.py +21 -0
  46. aethergraph/services/indices/scoped_indices.py +292 -0
  47. aethergraph/services/llm/generic_client.py +342 -46
  48. aethergraph/services/llm/generic_embed_client.py +359 -0
  49. aethergraph/services/llm/types.py +3 -1
  50. aethergraph/services/memory/distillers/llm_long_term.py +60 -109
  51. aethergraph/services/memory/distillers/llm_long_term_v1.py +180 -0
  52. aethergraph/services/memory/distillers/llm_meta_summary.py +57 -266
  53. aethergraph/services/memory/distillers/llm_meta_summary_v1.py +342 -0
  54. aethergraph/services/memory/distillers/long_term.py +48 -131
  55. aethergraph/services/memory/distillers/long_term_v1.py +170 -0
  56. aethergraph/services/memory/facade/chat.py +18 -8
  57. aethergraph/services/memory/facade/core.py +159 -19
  58. aethergraph/services/memory/facade/distillation.py +86 -31
  59. aethergraph/services/memory/facade/retrieval.py +100 -1
  60. aethergraph/services/memory/factory.py +4 -1
  61. aethergraph/services/planning/__init__.py +0 -0
  62. aethergraph/services/planning/action_catalog.py +271 -0
  63. aethergraph/services/planning/bindings.py +56 -0
  64. aethergraph/services/planning/dependency_index.py +65 -0
  65. aethergraph/services/planning/flow_validator.py +263 -0
  66. aethergraph/services/planning/graph_io_adapter.py +150 -0
  67. aethergraph/services/planning/input_parser.py +312 -0
  68. aethergraph/services/planning/missing_inputs.py +28 -0
  69. aethergraph/services/planning/node_planner.py +613 -0
  70. aethergraph/services/planning/orchestrator.py +112 -0
  71. aethergraph/services/planning/plan_executor.py +506 -0
  72. aethergraph/services/planning/plan_types.py +321 -0
  73. aethergraph/services/planning/planner.py +617 -0
  74. aethergraph/services/planning/planner_service.py +369 -0
  75. aethergraph/services/planning/planning_context_builder.py +43 -0
  76. aethergraph/services/planning/quick_actions.py +29 -0
  77. aethergraph/services/planning/routers/__init__.py +0 -0
  78. aethergraph/services/planning/routers/simple_router.py +26 -0
  79. aethergraph/services/rag/facade.py +0 -3
  80. aethergraph/services/scope/scope.py +30 -30
  81. aethergraph/services/scope/scope_factory.py +15 -7
  82. aethergraph/services/skills/__init__.py +0 -0
  83. aethergraph/services/skills/skill_registry.py +465 -0
  84. aethergraph/services/skills/skills.py +220 -0
  85. aethergraph/services/skills/utils.py +194 -0
  86. aethergraph/storage/artifacts/artifact_index_jsonl.py +16 -10
  87. aethergraph/storage/artifacts/artifact_index_sqlite.py +12 -2
  88. aethergraph/storage/docstore/sqlite_doc_sync.py +1 -1
  89. aethergraph/storage/memory/event_persist.py +42 -2
  90. aethergraph/storage/memory/fs_persist.py +32 -2
  91. aethergraph/storage/search_backend/__init__.py +0 -0
  92. aethergraph/storage/search_backend/generic_vector_backend.py +230 -0
  93. aethergraph/storage/search_backend/null_backend.py +34 -0
  94. aethergraph/storage/search_backend/sqlite_lexical_backend.py +387 -0
  95. aethergraph/storage/search_backend/utils.py +31 -0
  96. aethergraph/storage/search_factory.py +75 -0
  97. aethergraph/storage/vector_index/faiss_index.py +72 -4
  98. aethergraph/storage/vector_index/sqlite_index.py +521 -52
  99. aethergraph/storage/vector_index/sqlite_index_vanila.py +311 -0
  100. aethergraph/storage/vector_index/utils.py +22 -0
  101. {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/METADATA +1 -1
  102. {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/RECORD +107 -63
  103. {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/WHEEL +1 -1
  104. aethergraph/plugins/agents/default_chat_agent copy.py +0 -90
  105. aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +0 -1
  106. aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +0 -400
  107. aethergraph/services/eventhub/event_hub.py +0 -76
  108. aethergraph/services/llm/generic_client copy.py +0 -691
  109. aethergraph/services/prompts/file_store.py +0 -41
  110. {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/entry_points.txt +0 -0
  111. {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/licenses/LICENSE +0 -0
  112. {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/licenses/NOTICE +0 -0
  113. {aethergraph-0.1.0a3.dist-info → aethergraph-0.1.0a4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Literal
5
+
6
+ JsonType = Literal["string", "number", "boolean", "object", "array"]
7
+
8
+
9
+ @dataclass
10
+ class IOSlot:
11
+ """
12
+ One input or output slot for a graph action.
13
+
14
+ - `type` is a coarse JSON-like type for LLM/tool matching.
15
+ """
16
+
17
+ name: str
18
+ type: JsonType | None = None
19
+ description: str | None = None
20
+ required: bool = True
21
+ default: Any | None = None
22
+
23
+
24
+ @dataclass
25
+ class ActionSpec:
26
+ """
27
+ Docstring for ActionSpec
28
+ """
29
+
30
+ name: str # hunman name: usually the graph/graphfn name
31
+ ref: str # canonical ref: e.g. "graph:mygraph:1.0.0"
32
+ kind: Literal["graph", "graphfn"] # kind of action
33
+ version: str # version string
34
+
35
+ inputs: list[IOSlot] # input slots
36
+ outputs: list[IOSlot] # output slots
37
+
38
+ description: str | None = None # human description for LLM/tool matching
39
+ tags: list[str] = None # optional tags
40
+ flow_id: str | None = None # optional flow ID. If None, treat it globally.
41
+
42
+
43
+ def _map_py_type_to_json_type(tp: Any) -> JsonType | None:
44
+ """
45
+ Docstring for _map_py_type_to_json_type
46
+
47
+ :param tp: Description
48
+ :type tp: Any
49
+ :return: Description
50
+ :rtype: JsonType | None
51
+ """
52
+ origin = getattr(tp, "__origin__", None)
53
+
54
+ if tp in (str, bytes):
55
+ return "string"
56
+ if tp in (int, float):
57
+ return "number"
58
+ if tp is bool:
59
+ return "boolean"
60
+
61
+ # collections
62
+ if origin in (list, tuple, set) or tp in (list, tuple, set):
63
+ return "array"
64
+
65
+ # dict / mapping -> treat as object
66
+ if origin in (dict,) or tp in (dict,):
67
+ return "object"
68
+
69
+ # Optional[T] / Union[T, None] etc: peel outer layer and re-map
70
+ if origin is __import__("typing").Union:
71
+ args = [a for a in tp.__args__ if a is not type(None)] # noqa: E721
72
+ if len(args) == 1:
73
+ return _map_py_type_to_json_type(args[0])
74
+
75
+ # Fallback = "object" or None; for now we return object
76
+ return "object"
@@ -2,8 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  from collections.abc import Callable
4
4
  import inspect
5
- from typing import Any
5
+ from typing import Any, get_type_hints
6
6
 
7
+ from aethergraph.core.graph.action_spec import IOSlot, _map_py_type_to_json_type
7
8
  from aethergraph.core.runtime.run_registration import RunRegistrationGuard
8
9
  from aethergraph.services.registry.agent_app_meta import (
9
10
  build_agent_meta,
@@ -113,6 +114,68 @@ class GraphFunction:
113
114
 
114
115
  return run(self, inputs)
115
116
 
117
+ def io_signature(self) -> dict[str, list[IOSlot]]:
118
+ """
119
+ Infer typed IO based on decorator inputs/outputs and Python annotations.
120
+
121
+ Rule of thumbs:
122
+ - Inputs: use decorator list; if missing, use all params except 'context'.
123
+ - Outputs: use decorator list; if missing, use return annotation if any.
124
+
125
+ """
126
+ sig = inspect.signature(self.fn)
127
+ hints = get_type_hints(self.fn)
128
+
129
+ # --- Inputs ---
130
+ if self.inputs is not None:
131
+ input_names = list(self.inputs)
132
+ else:
133
+ # fallback: all params except 'context'
134
+ input_names = [p for p in sig.parameters if p != "context"]
135
+
136
+ input_slots: list[IOSlot] = []
137
+ for name in input_names:
138
+ param = sig.parameters.get(name)
139
+ if param is None:
140
+ # decorator said it's a input but not in signature
141
+ # treat as unknown, required
142
+ input_slots.append(IOSlot(name=name, type=None, required=True))
143
+ continue
144
+
145
+ anno = hints.get(name)
146
+ j_type = _map_py_type_to_json_type(anno) if anno is not None else None
147
+ required = param.default is inspect._empty
148
+ default = None if required else param.default
149
+
150
+ input_slots.append(
151
+ IOSlot(
152
+ name=name,
153
+ type=j_type,
154
+ required=required,
155
+ default=default,
156
+ )
157
+ )
158
+
159
+ # --- Outputs ---
160
+ output_slots: list[IOSlot] = []
161
+ out_names = list(self.outputs or [])
162
+
163
+ ret_anno = hints.get("return")
164
+ if out_names:
165
+ if len(out_names) == 1 and ret_anno is not None:
166
+ j_type = _map_py_type_to_json_type(ret_anno)
167
+ output_slots.append(IOSlot(name=out_names[0], type=j_type, required=True))
168
+ else:
169
+ # Multi-output or no return type: names only
170
+ for name in out_names:
171
+ output_slots.append(IOSlot(name=name, type=None, required=True))
172
+ elif ret_anno is not None:
173
+ # No explicit output names but we *do* know the type: create a single output
174
+ j_type = _map_py_type_to_json_type(ret_anno)
175
+ output_slots.append(IOSlot(name="result", type=j_type, required=True))
176
+
177
+ return {"inputs": input_slots, "outputs": output_slots}
178
+
116
179
 
117
180
  def _is_ref(x: object) -> bool:
118
181
  return isinstance(x, dict) and x.get("_type") == "ref" and "from" in x and "key" in x
@@ -207,6 +270,7 @@ def graph_fn(
207
270
  tags: list[str] | None = None,
208
271
  as_agent: dict[str, Any] | None = None,
209
272
  as_app: dict[str, Any] | None = None,
273
+ description: str | None = None,
210
274
  ) -> Callable[[Callable], GraphFunction]:
211
275
  """
212
276
  Decorator to define a graph function and optionally register it as an agent or app.
@@ -275,6 +339,7 @@ def graph_fn(
275
339
  tags: List of string tags for discovery and categorization.
276
340
  as_agent: Optional dictionary defining agent metadata. Used when running through Aethergraph UI. See additional information below.
277
341
  as_app: Optional dictionary defining app metadata. Used when running through Aethergraph UI. See additional information below.
342
+ description: Optional human-readable description of the graph function.
278
343
 
279
344
  Returns:
280
345
  Callable: A decorator that wraps the function as a `GraphFunction` and registers it
@@ -307,11 +372,19 @@ def graph_fn(
307
372
  return gf
308
373
 
309
374
  base_tags = tags or []
375
+
376
+ # prefer explicit description; then docstring; else name
377
+ doc_desc = inspect.getdoc(fn) or None
378
+ eff_description = description or doc_desc or name
379
+
310
380
  graph_meta: dict[str, Any] = {
311
381
  "kind": "graphfn",
312
382
  "entrypoint": entrypoint,
313
- "flow_id": flow_id or name,
383
+ "flow_id": flow_id,
314
384
  "tags": base_tags,
385
+ "description": eff_description,
386
+ "inputs": inputs,
387
+ "outputs": outputs,
315
388
  }
316
389
 
317
390
  registry.register(
@@ -1,14 +1,45 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
- from typing import Any
4
+ from typing import Any, get_origin, get_type_hints
5
5
 
6
+ from aethergraph.core.graph.action_spec import _map_py_type_to_json_type
6
7
  from aethergraph.services.registry.agent_app_meta import build_agent_meta, build_app_meta
7
8
 
8
9
  from ..runtime.runtime_registry import current_registry
9
10
  from .task_graph import TaskGraph
10
11
 
11
12
 
13
+ def _normalize_type_hint(ann: Any) -> str:
14
+ """Convert a Python annotation into a simple string for IO types."""
15
+ if ann is inspect._empty:
16
+ return "any"
17
+
18
+ origin = get_origin(ann)
19
+ # args = get_args(ann)
20
+
21
+ # Builtins
22
+ if ann is str:
23
+ return "string"
24
+ if ann is int:
25
+ return "int"
26
+ if ann is float:
27
+ return "float"
28
+ if ann is bool:
29
+ return "bool"
30
+ if ann is dict or origin is dict:
31
+ # e.g. dict[str, float]
32
+ return "object"
33
+ if ann in (list, tuple) or origin in (list, tuple):
34
+ # e.g. list[int] / list[dict[str, float]]
35
+ return "array"
36
+ if ann is Any:
37
+ return "any"
38
+
39
+ # Fallback: stringified type name
40
+ return getattr(ann, "__name__", str(ann))
41
+
42
+
12
43
  def graphify(
13
44
  name="default_graph",
14
45
  inputs=(),
@@ -20,6 +51,7 @@ def graphify(
20
51
  tags: list[str] | None = None,
21
52
  as_agent: dict[str, Any] | None = None,
22
53
  as_app: dict[str, Any] | None = None,
54
+ description: str | None = None,
23
55
  ):
24
56
  """
25
57
  Decorator to define a `TaskGraph` and optionally register it as an agent or app.
@@ -88,6 +120,7 @@ def graphify(
88
120
  tags: List of string tags for discovery and categorization.
89
121
  as_agent: Optional dictionary defining agent metadata. Used when running through Aethergraph UI. See additional information below.
90
122
  as_app: Optional dictionary defining app metadata. Used when running through Aethergraph UI. See additional information below.
123
+ description: Optional human-readable description of the graph function.
91
124
 
92
125
  Returns:
93
126
  TaskGraph: A decorator that transforms a function into a TaskGraph with the specified configuration.
@@ -195,11 +228,50 @@ def graphify(
195
228
  return _build
196
229
 
197
230
  base_tags = tags or []
231
+
232
+ # Effective description
233
+ doc_desc = inspect.getdoc(fn) or None
234
+ eff_description = description or doc_desc or name
235
+
236
+ # Infer IO types from annotations if possible
237
+ try:
238
+ resolved_hints = get_type_hints(fn)
239
+ except Exception:
240
+ # Fallback: use raw __annotations__ if get_type_hints blows up
241
+ resolved_hints = getattr(fn, "__annotations__", {}) or {}
242
+
243
+ # Infer IO types from annotations in a JSON-ish schema
244
+ input_type_map: dict[str, str] = {}
245
+ for pname in required_inputs:
246
+ param = fn_sig.parameters.get(pname)
247
+ if param is None:
248
+ continue
249
+
250
+ # Prefer resolved type hint; fall back to the raw annotation
251
+ ann = resolved_hints.get(pname, param.annotation)
252
+ if ann is inspect._empty:
253
+ continue
254
+
255
+ j = _map_py_type_to_json_type(ann)
256
+ if j is not None:
257
+ input_type_map[pname] = j
258
+
259
+ # for outputs, we only have the return annotation as a whole
260
+ output_names = list(outputs or [])
261
+ output_type_map: dict[str, str] = {n: "any" for n in output_names}
262
+
198
263
  graph_meta: dict[str, Any] = {
199
264
  "kind": "graph",
200
265
  "entrypoint": entrypoint,
201
- "flow_id": flow_id or name,
266
+ "flow_id": flow_id,
202
267
  "tags": base_tags,
268
+ "description": eff_description,
269
+ "inputs": inputs,
270
+ "outputs": outputs,
271
+ "io_types": {
272
+ "inputs": input_type_map,
273
+ "outputs": output_type_map,
274
+ },
203
275
  }
204
276
 
205
277
  registry.register(
@@ -10,7 +10,6 @@ from aethergraph.contracts.errors.errors import GraphHasPendingWaits
10
10
  from aethergraph.contracts.services.state_stores import GraphSnapshot
11
11
  from aethergraph.core.graph.task_graph import TaskGraph
12
12
  from aethergraph.core.runtime.recovery import hash_spec, recover_graph_run
13
- from aethergraph.services.container.default_container import build_default_container # adjust path
14
13
  from aethergraph.services.state_stores.graph_observer import PersistenceObserver
15
14
  from aethergraph.services.state_stores.resume_policy import (
16
15
  assert_snapshot_json_only,
@@ -29,6 +28,8 @@ from .run_registration import RunRegistrationGuard
29
28
 
30
29
  # ---------- env helpers ----------
31
30
  def _get_container():
31
+ from aethergraph.services.container.default_container import build_default_container
32
+
32
33
  # install once if not installed by sidecar/server
33
34
  return ensure_services_installed(build_default_container)
34
35
 
@@ -2,7 +2,15 @@ from dataclasses import dataclass
2
2
  from datetime import timedelta
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
+ from aethergraph.contracts.services.execution import (
6
+ CodeExecutionRequest,
7
+ CodeExecutionResult,
8
+ ExecutionService,
9
+ )
5
10
  from aethergraph.services.artifacts.facade import ArtifactFacade
11
+ from aethergraph.services.indices.scoped_indices import ScopedIndices
12
+ from aethergraph.services.planning.node_planner import NodePlanner
13
+ from aethergraph.services.skills.skill_registry import SkillRegistry
6
14
 
7
15
  if TYPE_CHECKING:
8
16
  from aethergraph.core.runtime.run_manager import RunManager
@@ -41,10 +49,37 @@ class NodeContext:
41
49
  app_id: str | None = None # for app-invoked runs
42
50
  bound_memory: BoundMemoryAdapter | None = None # back-compat
43
51
 
52
+ _planner_facade: NodePlanner | None = None # lazy init
53
+
44
54
  # --- accessors (compatible names) ---
45
55
  def runtime(self) -> NodeServices:
46
56
  return self.services
47
57
 
58
+ async def execute(
59
+ self,
60
+ code: str,
61
+ *,
62
+ language: str = "python",
63
+ timeout_s: float = 30.0,
64
+ args: list[str] | None = None,
65
+ workdir: str | None = None,
66
+ env: dict[str, str] | None = None,
67
+ ) -> CodeExecutionResult:
68
+ """ """
69
+ exe_svs: ExecutionService | None = getattr(self.services, "execution", None)
70
+ if exe_svs is None:
71
+ raise RuntimeError("NodeContext.services.execution is not configured")
72
+
73
+ req = CodeExecutionRequest(
74
+ language=language,
75
+ code=code,
76
+ args=args or [],
77
+ timeout_s=timeout_s,
78
+ workdir=workdir,
79
+ env=env,
80
+ )
81
+ return await exe_svs.execute(req)
82
+
48
83
  async def spawn_run(
49
84
  self,
50
85
  graph_id: str,
@@ -83,7 +118,7 @@ class NodeContext:
83
118
  inputs={"foo": "bar"},
84
119
  tags=["experiment", "priority"],
85
120
  agent_id="agent-123", # associate with an agent if applicable
86
- visibility=RunVisibility.ineline, # not shown in UI
121
+ visibility=RunVisibility.inline, # not shown in UI
87
122
  )
88
123
  ```
89
124
 
@@ -232,6 +267,7 @@ class NodeContext:
232
267
  run_id: str,
233
268
  *,
234
269
  timeout_s: float | None = None,
270
+ return_outputs: bool = False,
235
271
  ) -> RunRecord:
236
272
  """
237
273
  Wait for a run to complete and retrieve its final record.
@@ -249,16 +285,18 @@ class NodeContext:
249
285
 
250
286
  Waiting with a timeout:
251
287
  ```python
252
- record = await context.wait_run(run_id, timeout_s=30)
288
+ record, outputs = await context.wait_run(run_id, timeout_s=30, return_outputs=True)
253
289
  ```
254
290
 
255
291
  Args:
256
292
  run_id: The unique identifier of the run to wait for.
257
293
  timeout_s: Optional timeout in seconds. If set, the method will raise
258
294
  a TimeoutError if the run does not complete in time.
295
+ return_outputs: If True, also return the run's outputs along with the record.
259
296
 
260
297
  Returns:
261
298
  RunRecord: The final record of the completed run.
299
+ Output: If `return_outputs` is True, returns a tuple of (RunRecord, outputs dict).
262
300
 
263
301
  Raises:
264
302
  RuntimeError: If the RunManager service is not configured in the context.
@@ -273,7 +311,7 @@ class NodeContext:
273
311
  rm: RunManager | None = getattr(self.services, "run_manager", None)
274
312
  if rm is None:
275
313
  raise RuntimeError("NodeContext.services.run_manager is not configured")
276
- return await rm.wait_run(run_id, timeout_s=timeout_s)
314
+ return await rm.wait_run(run_id, timeout_s=timeout_s, return_outputs=return_outputs)
277
315
 
278
316
  async def cancel_run(self, run_id: str) -> None:
279
317
  """
@@ -315,6 +353,16 @@ class NodeContext:
315
353
  raise RuntimeError("NodeContext.services.run_manager is not configured")
316
354
  await rm.cancel_run(run_id)
317
355
 
356
+ def planner(self) -> "NodePlanner":
357
+ if self._planner_facade is None:
358
+ if self.services.planner_service is None:
359
+ raise RuntimeError("NodeContext.services.planner_service is not configured")
360
+ self._planner_facade = NodePlanner(
361
+ service=self.services.planner_service,
362
+ node_ctx=self,
363
+ )
364
+ return self._planner_facade
365
+
318
366
  def logger(self):
319
367
  return self.services.logger.for_node_ctx(
320
368
  run_id=self.run_id, node_id=self.node_id, graph_id=self.graph_id
@@ -346,6 +394,11 @@ class NodeContext:
346
394
  """
347
395
  return ChannelSession(self, f"ui:run/{self.run_id}")
348
396
 
397
+ def skills(self) -> SkillRegistry:
398
+ if not self.services.skills:
399
+ raise RuntimeError("NodeContext.services.skills is not configured")
400
+ return self.services.skills
401
+
349
402
  def channel(self, channel_key: str | None = None):
350
403
  """
351
404
  Set up a new ChannelSession for the current node context.
@@ -521,6 +574,16 @@ class NodeContext:
521
574
  raise RuntimeError("MCPService not available")
522
575
  return self.services.mcp.get(name)
523
576
 
577
+ def indices(self) -> ScopedIndices:
578
+ if not self.services.indices:
579
+ raise RuntimeError("ScopedIndices not available")
580
+ return self.services.indices
581
+
582
+ # def run_manager(self) -> RunManager:
583
+ # if not self.services.run_manager:
584
+ # raise RuntimeError("RunManager not available")
585
+ # return self.services.run_manager
586
+
524
587
  def continuations(self):
525
588
  return self.services.continuation_store
526
589
 
@@ -3,6 +3,10 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import TYPE_CHECKING, Any
5
5
 
6
+ from aethergraph.services.indices.scoped_indices import ScopedIndices
7
+ from aethergraph.services.planning.planner_service import PlannerService
8
+ from aethergraph.services.skills.skill_registry import SkillRegistry
9
+
6
10
  if TYPE_CHECKING:
7
11
  from aethergraph.core.runtime.run_manager import RunManager
8
12
  from aethergraph.services.channel.channel_bus import ChannelBus
@@ -35,3 +39,7 @@ class NodeServices:
35
39
  rag: NodeRAG | None = None # RAGService
36
40
  mcp: MCPService | None = None # MCPService
37
41
  run_manager: RunManager | None = None # RunManager
42
+ indices: ScopedIndices | None = None # ScopedIndices for this node
43
+ execution: Any | None = None # ExecutionService
44
+ planner_service: PlannerService | None = None # PlannerService
45
+ skills: SkillRegistry | None = None # SkillRegistry