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
@@ -16,6 +16,7 @@ from aethergraph.core.runtime.run_types import (
16
16
  RunRecord,
17
17
  RunStatus,
18
18
  RunVisibility,
19
+ _make_preview,
19
20
  )
20
21
  from aethergraph.core.runtime.runtime_metering import current_metering
21
22
  from aethergraph.core.runtime.runtime_registry import current_registry
@@ -40,68 +41,6 @@ def _is_graphfn(obj: Any) -> bool:
40
41
 
41
42
  class RunManager:
42
43
  """
43
- High-level coordinator for running graphs.
44
-
45
- Responsibilities
46
- ----------------
47
- - Resolve graph targets (TaskGraph / GraphFunction) from the UnifiedRegistry.
48
- - Create and persist RunRecord metadata in the RunStore.
49
- - Enforce a soft concurrency limit via an in-process run slot counter.
50
- - Drive execution via run_or_resume_async and record status / errors.
51
- - Emit metering events (duration, status, user/org, graph_id).
52
- - Best-effort cancellation by talking to the scheduler registry.
53
-
54
- Key entrypoints
55
- ---------------
56
- submit_run(...)
57
- Non-blocking API entrypoint (used by HTTP routes).
58
- - Acquires a run slot (respecting max_concurrent_runs).
59
- - Creates a RunRecord (status=running) and saves it.
60
- - Schedules a background coroutine (_bg) that:
61
- * Calls _run_and_finalize(...)
62
- * Always releases the run slot in a finally block.
63
- - Returns immediately with the RunRecord so the caller can poll status.
64
-
65
- start_run(...)
66
- Blocking helper (tests / CLI).
67
- - Same setup as submit_run, but runs _run_and_finalize(...) inline.
68
- - Returns (RunRecord, outputs, has_waits, continuations).
69
-
70
- _run_and_finalize(...)
71
- Shared core logic used by both submit_run and start_run.
72
- - Calls run_or_resume_async(target, inputs, run_id, session_id).
73
- - Maps successful results into a dict of outputs.
74
- - Handles:
75
- * Normal completion -> status = succeeded.
76
- * GraphHasPendingWaits -> status = failed (for now), has_waits=True.
77
- * asyncio.CancelledError -> status = canceled.
78
- * Other exceptions -> status = failed, error message recorded.
79
- - Updates RunStore status fields (finished_at, error).
80
- - Sends a metering event with status / duration.
81
-
82
- Concurrency model
83
- -----------------
84
- - _acquire_run_slot / _release_run_slot protect a _running counter with an
85
- asyncio.Lock to enforce max_concurrent_runs within this process.
86
- - submit_run takes ownership of a slot until responsibility is handed to
87
- the background runner (_bg). Once _bg is scheduled, it is responsible
88
- for releasing the slot in its finally block.
89
- - If submit_run fails before the handoff, it releases the slot itself to
90
- avoid leaks.
91
-
92
- Cancellation
93
- ------------
94
- cancel_run(run_id)
95
- - Looks up the RunRecord (if available) and, if not terminal, marks it
96
- as cancellation_requested in the RunStore.
97
- - Uses the scheduler registry to find the scheduler for this run:
98
- * GlobalForwardScheduler: terminate_run(run_id)
99
- * ForwardScheduler: terminate()
100
- - The actual transition to RunStatus.canceled happens when the
101
- scheduler cancels the task and run_or_resume_async raises
102
- asyncio.CancelledError, which _run_and_finalize() translates into
103
- a canceled run.
104
-
105
44
  TODO: for global schedulers, we may want to have a dedicated run manager -- current
106
45
  implementation utilize the async_run which create a local ForwardScheduler instance
107
46
  each graph run. This is fine for concurrent graphs under thousands but may
@@ -167,6 +106,84 @@ class RunManager:
167
106
  raise KeyError(f"Graph '{graph_id}' not found")
168
107
 
169
108
  # -------- core execution helper --------
109
+ async def _build_run_record(
110
+ self,
111
+ *,
112
+ graph_id: str,
113
+ run_id: str | None,
114
+ session_id: str | None,
115
+ tags: list[str] | None,
116
+ identity: RequestIdentity,
117
+ origin: RunOrigin | None,
118
+ visibility: RunVisibility | None,
119
+ importance: RunImportance | None,
120
+ agent_id: str | None,
121
+ app_id: str | None,
122
+ ) -> tuple[RunRecord, Any]:
123
+ """
124
+ Shared helper for submit_run and run_and_wait:
125
+ - Resolves target
126
+ - Determines kind
127
+ - Attaches flow_id and session tags
128
+
129
+ Return:
130
+ - RunRecord (not yet persisted)
131
+ - target object: graph or graphfn
132
+ """
133
+ rid = run_id or f"run-{uuid4().hex[:8]}"
134
+ started_at = _utcnow()
135
+ tags = list(tags or [])
136
+
137
+ target = await self._resolve_target(graph_id)
138
+ if _is_task_graph(target):
139
+ kind = "taskgraph"
140
+ elif _is_graphfn(target):
141
+ kind = "graphfn"
142
+ else:
143
+ kind = "other"
144
+
145
+ flow_id: str | None = None
146
+ reg = self.registry()
147
+ if reg is not None:
148
+ if kind == "taskgraph":
149
+ meta = reg.get_meta(nspace="graph", name=graph_id, version=None) or {}
150
+ elif kind == "graphfn":
151
+ meta = reg.get_meta(nspace="graphfn", name=graph_id, version=None) or {}
152
+ else:
153
+ meta = {}
154
+ flow_id = meta.get("flow_id") or graph_id
155
+
156
+ if session_id is None:
157
+ session_id = rid
158
+
159
+ record = RunRecord(
160
+ run_id=rid,
161
+ graph_id=graph_id,
162
+ kind=kind,
163
+ status=RunStatus.running,
164
+ started_at=started_at,
165
+ tags=tags,
166
+ user_id=identity.user_id,
167
+ org_id=identity.org_id,
168
+ meta={},
169
+ session_id=session_id,
170
+ origin=origin or RunOrigin.app,
171
+ visibility=visibility or RunVisibility.normal,
172
+ importance=importance or RunImportance.normal,
173
+ agent_id=agent_id,
174
+ app_id=app_id,
175
+ )
176
+
177
+ if flow_id:
178
+ record.meta["flow_id"] = flow_id
179
+ if f"flow:{flow_id}" not in record.tags:
180
+ record.tags.append(f"flow:{flow_id}")
181
+ if session_id:
182
+ record.meta["session_id"] = session_id
183
+ if f"session:{session_id}" not in record.tags:
184
+ record.tags.append(f"session:{session_id}")
185
+
186
+ return record, target
170
187
 
171
188
  async def _run_and_finalize(
172
189
  self,
@@ -214,6 +231,18 @@ class RunManager:
214
231
  record.status = RunStatus.succeeded
215
232
  record.finished_at = _utcnow()
216
233
 
234
+ # Optional: store a UI-only output preview
235
+ try:
236
+ preview, truncated = _make_preview(outputs)
237
+ record.meta["output_preview"] = preview
238
+ record.meta["output_truncated"] = truncated
239
+ except Exception:
240
+ import logging
241
+
242
+ logging.getLogger("aethergraph.runtime.run_manager").exception(
243
+ "Error creating output preview for run_id=%s", record.run_id
244
+ )
245
+
217
246
  except asyncio.CancelledError:
218
247
  # Cancellation path: scheduler.terminate() or external cancel.
219
248
  import logging
@@ -281,7 +310,8 @@ class RunManager:
281
310
 
282
311
  try:
283
312
  if record.status in {RunStatus.succeeded, RunStatus.failed, RunStatus.canceled}:
284
- await self._resolve_run_future(record.run_id, record)
313
+ # IMPORTANT: now resolve with (record, outputs)
314
+ await self._resolve_run_future(record.run_id, (record, outputs))
285
315
  except Exception: # noqa: BLE001
286
316
  import logging
287
317
 
@@ -319,9 +349,6 @@ class RunManager:
319
349
  if identity is None:
320
350
  identity = RequestIdentity(user_id="local", org_id="local", mode="local")
321
351
 
322
- user_id = identity.user_id
323
- org_id = identity.org_id
324
-
325
352
  # Acquire run slot (rate limiting)
326
353
  await self._acquire_run_slot()
327
354
  # Tracks whether responsibility for releasing the slot has been handed
@@ -331,59 +358,31 @@ class RunManager:
331
358
 
332
359
  try:
333
360
  tags = tags or []
334
- target = await self._resolve_target(graph_id)
335
- rid = run_id or f"run-{uuid4().hex[:8]}"
336
- started_at = _utcnow()
337
-
338
- if _is_task_graph(target):
339
- kind = "taskgraph"
340
- elif _is_graphfn(target):
341
- kind = "graphfn"
342
- else:
343
- kind = "other"
344
-
345
- # pull flow_id and entrypoint from registry if possible
346
- flow_id: str | None = None
347
- reg = self.registry()
348
- if reg is not None:
349
- if kind == "taskgraph":
350
- meta = reg.get_meta(nspace="graph", name=graph_id, version=None) or {}
351
- elif kind == "graphfn":
352
- meta = reg.get_meta(nspace="graphfn", name=graph_id, version=None) or {}
353
- else:
354
- meta = {}
355
- flow_id = meta.get("flow_id") or graph_id
356
-
357
- # use run_id as session_id if not provided
358
- if session_id is None:
359
- session_id = rid
360
-
361
- record = RunRecord(
362
- run_id=rid,
361
+
362
+ record, target = await self._build_run_record(
363
363
  graph_id=graph_id,
364
- kind=kind,
365
- status=RunStatus.running, # we go straight to running as before
366
- started_at=started_at,
367
- tags=list(tags),
368
- user_id=user_id,
369
- org_id=org_id,
370
- meta={},
364
+ run_id=run_id,
371
365
  session_id=session_id,
372
- origin=origin or RunOrigin.app, # app is a typical default for graph runs
373
- visibility=visibility or RunVisibility.normal,
374
- importance=importance or RunImportance.normal,
366
+ tags=tags,
367
+ identity=identity,
368
+ origin=origin,
369
+ visibility=visibility,
370
+ importance=importance,
375
371
  agent_id=agent_id,
376
372
  app_id=app_id,
377
373
  )
378
374
 
379
- if flow_id:
380
- record.meta["flow_id"] = flow_id
381
- if f"flow:{flow_id}" not in record.tags:
382
- record.tags.append(f"flow:{flow_id}") # add flow tag if missing
383
- if session_id:
384
- record.meta["session_id"] = session_id
385
- if f"session:{session_id}" not in record.tags:
386
- record.tags.append(f"session:{session_id}") # add session tag if missing
375
+ # Optional: store a UI-only input preview
376
+ try:
377
+ preview, truncated = _make_preview(inputs)
378
+ record.meta["input_preview"] = preview
379
+ record.meta["input_truncated"] = truncated
380
+ except Exception:
381
+ import logging
382
+
383
+ logging.getLogger("aethergraph.runtime.run_manager").exception(
384
+ "Error creating input preview for run_id=%s", record.run_id
385
+ )
387
386
 
388
387
  if self._store is not None:
389
388
  await self._store.create(record)
@@ -395,28 +394,22 @@ class RunManager:
395
394
  target=target,
396
395
  graph_id=graph_id,
397
396
  inputs=inputs,
398
- # user_id=user_id,
399
- # org_id=org_id,
400
397
  identity=identity,
401
398
  )
402
399
  finally:
403
400
  await self._release_run_slot()
404
401
 
405
- # If we're in an event loop (server), schedule in the background.
406
- # If not (CLI), just run inline so behaviour is still sane.
407
402
  try:
408
403
  loop = asyncio.get_running_loop()
409
404
  except RuntimeError:
410
- # Not inside a running loop – e.g., CLI usage.
411
405
  slot_handed_to_bg = True
412
- # _bg() is responsible for releasing the slot in its finally.
413
406
  await _bg()
414
407
  else:
415
408
  slot_handed_to_bg = True
416
- # Background tasks; _bg() will release the slot in its finally.
417
409
  loop.create_task(_bg())
418
410
 
419
411
  return record
412
+
420
413
  except Exception:
421
414
  # If submit_run itself fails *before* handing off to _bg, we must release the slot here.
422
415
  # Once slot_handed_to_bg is True, _bg is responsible for releasing the slot.
@@ -459,65 +452,35 @@ class RunManager:
459
452
 
460
453
  try:
461
454
  tags = tags or []
462
- target = await self._resolve_target(
463
- graph_id
464
- ) # same resolver as submit_run :contentReference[oaicite:1]{index=1}
465
- rid = run_id or f"run-{uuid4().hex[:8]}"
466
- started_at = _utcnow()
467
-
468
- if _is_task_graph(target):
469
- kind = "taskgraph"
470
- elif _is_graphfn(target):
471
- kind = "graphfn"
472
- else:
473
- kind = "other"
474
-
475
- # flow_id extraction same pattern as submit_run :contentReference[oaicite:2]{index=2}
476
- flow_id: str | None = None
477
- reg = self.registry()
478
- if reg is not None:
479
- if kind == "taskgraph":
480
- meta = reg.get_meta(nspace="graph", name=graph_id, version=None) or {}
481
- elif kind == "graphfn":
482
- meta = reg.get_meta(nspace="graphfn", name=graph_id, version=None) or {}
483
- else:
484
- meta = {}
485
- flow_id = meta.get("flow_id") or graph_id
486
-
487
- if session_id is None:
488
- session_id = rid
489
-
490
- record = RunRecord(
491
- run_id=rid,
455
+
456
+ record, target = await self._build_run_record(
492
457
  graph_id=graph_id,
493
- kind=kind,
494
- status=RunStatus.running,
495
- started_at=started_at,
496
- tags=list(tags),
497
- user_id=identity.user_id,
498
- org_id=identity.org_id,
499
- meta={},
458
+ run_id=run_id,
500
459
  session_id=session_id,
501
- origin=origin or RunOrigin.app,
502
- visibility=visibility or RunVisibility.normal,
503
- importance=importance or RunImportance.normal,
460
+ tags=tags,
461
+ identity=identity,
462
+ origin=origin,
463
+ visibility=visibility,
464
+ importance=importance,
504
465
  agent_id=agent_id,
505
466
  app_id=app_id,
506
467
  )
507
468
 
508
- if flow_id:
509
- record.meta["flow_id"] = flow_id
510
- if f"flow:{flow_id}" not in record.tags:
511
- record.tags.append(f"flow:{flow_id}")
512
- if session_id:
513
- record.meta["session_id"] = session_id
514
- if f"session:{session_id}" not in record.tags:
515
- record.tags.append(f"session:{session_id}")
469
+ # Optional: UI-only input preview
470
+ try:
471
+ preview, truncated = _make_preview(inputs)
472
+ record.meta["input_preview"] = preview
473
+ record.meta["input_truncated"] = truncated
474
+ except Exception:
475
+ import logging
476
+
477
+ logging.getLogger("aethergraph.runtime.run_manager").exception(
478
+ "Error creating input preview for run_id=%s", record.run_id
479
+ )
516
480
 
517
481
  if self._store is not None:
518
482
  await self._store.create(record)
519
483
 
520
- # Inline execution; still uses run_or_resume_async under the hood :contentReference[oaicite:3]{index=3}
521
484
  return await self._run_and_finalize(
522
485
  record=record,
523
486
  target=target,
@@ -529,105 +492,6 @@ class RunManager:
529
492
  if count_slot:
530
493
  await self._release_run_slot()
531
494
 
532
- # -------- old: blocking start_run (CLI/tests) --------
533
- async def start_run(
534
- self,
535
- graph_id: str,
536
- *,
537
- inputs: dict[str, Any],
538
- run_id: str | None = None,
539
- session_id: str | None = None,
540
- tags: list[str] | None = None,
541
- identity: RequestIdentity | None = None,
542
- agent_id: str | None = None,
543
- app_id: str | None = None,
544
- ) -> tuple[RunRecord, dict[str, Any] | None, bool, list[dict[str, Any]]]:
545
- """
546
- Blocking helper (original behaviour).
547
-
548
- - Resolves target.
549
- - Creates RunRecord with status=running.
550
- - Runs once via run_or_resume_async.
551
- - Updates store + metering.
552
- - Returns (record, outputs, has_waits, continuations).
553
-
554
- Still useful for tests/CLI, but the HTTP route should prefer submit_run().
555
-
556
- NOTE:
557
- agent_id and app_id will override any value pulled from original graphs. Use it
558
- only when you want to explicitly set these fields for tracking purpose.
559
- """
560
- if identity is None:
561
- identity = RequestIdentity(user_id="local", org_id="local", mode="local")
562
-
563
- tags = tags or []
564
- target = await self._resolve_target(graph_id)
565
- rid = run_id or f"run-{uuid4().hex[:8]}"
566
- started_at = _utcnow()
567
-
568
- if _is_task_graph(target):
569
- kind = "taskgraph"
570
- elif _is_graphfn(target):
571
- kind = "graphfn"
572
- else:
573
- kind = "other"
574
-
575
- # pull flow_id and entrypoint from registry if possible
576
- flow_id: str | None = None
577
- reg = self.registry()
578
- if reg is not None:
579
- if kind == "taskgraph":
580
- meta = reg.get_meta(nspace="graph", name=graph_id, version=None) or {}
581
- elif kind == "graphfn":
582
- meta = reg.get_meta(nspace="graphfn", name=graph_id, version=None) or {}
583
- else:
584
- meta = {}
585
- flow_id = meta.get("flow_id") or graph_id
586
-
587
- # use run_id as session_id if not provided
588
- if session_id is None:
589
- session_id = rid
590
-
591
- record = RunRecord(
592
- run_id=rid,
593
- graph_id=graph_id,
594
- kind=kind,
595
- status=RunStatus.running, # we go straight to running as before
596
- started_at=started_at,
597
- tags=list(tags),
598
- user_id=identity.user_id,
599
- org_id=identity.org_id,
600
- meta={},
601
- session_id=session_id,
602
- origin=RunOrigin.app, # app is a typical default for graph runs
603
- visibility=RunVisibility.normal,
604
- importance=RunImportance.normal,
605
- agent_id=agent_id,
606
- app_id=app_id,
607
- )
608
-
609
- if flow_id:
610
- record.meta["flow_id"] = flow_id
611
- if f"flow:{flow_id}" not in record.tags:
612
- record.tags.append(f"flow:{flow_id}") # add flow tag if missing
613
- if session_id:
614
- record.meta["session_id"] = session_id
615
- if f"session:{session_id}" not in record.tags:
616
- record.tags.append(f"session:{session_id}") # add session tag if missing
617
-
618
- if self._store is not None:
619
- await self._store.create(record)
620
-
621
- return await self._run_and_finalize(
622
- record=record,
623
- target=target,
624
- graph_id=graph_id,
625
- inputs=inputs,
626
- identity=identity,
627
- # agent_id=agent_id,
628
- # app_id=app_id,
629
- )
630
-
631
495
  async def get_record(self, run_id: str) -> RunRecord | None:
632
496
  if self._store is None:
633
497
  return None
@@ -748,16 +612,45 @@ class RunManager:
748
612
  run_id: str,
749
613
  *,
750
614
  timeout_s: float | None = None,
751
- ) -> RunRecord:
615
+ return_outputs: bool = False,
616
+ ) -> RunRecord | tuple[RunRecord, dict[str, Any] | None]:
617
+ """
618
+ Wait for a run to reach a terminal state.
619
+
620
+ - If return_outputs=False (default), returns RunRecord (backwards compatible).
621
+ - If return_outputs=True, returns (RunRecord, outputs_dict_or_none).
622
+
623
+ NOTE:
624
+ - outputs are only guaranteed when the run was executed in THIS process
625
+ via RunManager. If the run is already terminal in the store and no
626
+ in-process outputs exist, outputs will be None.
627
+ """
752
628
  # Fast path: already terminal in store
753
629
  rec = await self.get_record(run_id)
754
630
  if rec and rec.status in {RunStatus.succeeded, RunStatus.failed, RunStatus.canceled}:
631
+ if return_outputs:
632
+ return rec, None
755
633
  return rec
756
634
 
757
635
  fut = await self._get_or_create_run_future(run_id)
636
+
758
637
  if timeout_s is not None:
759
- return await asyncio.wait_for(fut, timeout=timeout_s)
760
- return await fut
638
+ result = await asyncio.wait_for(fut, timeout=timeout_s)
639
+ else:
640
+ result = await fut
641
+
642
+ # result is either:
643
+ # - RunRecord (old-style resolvers)
644
+ # - or (RunRecord, outputs) from _run_and_finalize
645
+ if isinstance(result, RunRecord):
646
+ if return_outputs:
647
+ return result, None
648
+ return result
649
+
650
+ rec2, outputs = result
651
+ if return_outputs:
652
+ return rec2, outputs
653
+ return rec2
761
654
 
762
655
  async def _get_or_create_run_future(self, run_id: str) -> asyncio.Future:
763
656
  async with self._run_waiters_lock:
@@ -781,3 +674,102 @@ class RunManager:
781
674
  if fut and not fut.done():
782
675
  fut.set_exception(err)
783
676
  self._run_waiters.pop(run_id, None)
677
+
678
+ # -------- old: blocking start_run (CLI/tests) --------
679
+ async def start_run(
680
+ self,
681
+ graph_id: str,
682
+ *,
683
+ inputs: dict[str, Any],
684
+ run_id: str | None = None,
685
+ session_id: str | None = None,
686
+ tags: list[str] | None = None,
687
+ identity: RequestIdentity | None = None,
688
+ agent_id: str | None = None,
689
+ app_id: str | None = None,
690
+ ) -> tuple[RunRecord, dict[str, Any] | None, bool, list[dict[str, Any]]]:
691
+ """
692
+ Blocking helper (original behaviour).
693
+
694
+ - Resolves target.
695
+ - Creates RunRecord with status=running.
696
+ - Runs once via run_or_resume_async.
697
+ - Updates store + metering.
698
+ - Returns (record, outputs, has_waits, continuations).
699
+
700
+ Still useful for tests/CLI, but the HTTP route should prefer submit_run().
701
+
702
+ NOTE:
703
+ agent_id and app_id will override any value pulled from original graphs. Use it
704
+ only when you want to explicitly set these fields for tracking purpose.
705
+ """
706
+ if identity is None:
707
+ identity = RequestIdentity(user_id="local", org_id="local", mode="local")
708
+
709
+ tags = tags or []
710
+ target = await self._resolve_target(graph_id)
711
+ rid = run_id or f"run-{uuid4().hex[:8]}"
712
+ started_at = _utcnow()
713
+
714
+ if _is_task_graph(target):
715
+ kind = "taskgraph"
716
+ elif _is_graphfn(target):
717
+ kind = "graphfn"
718
+ else:
719
+ kind = "other"
720
+
721
+ # pull flow_id and entrypoint from registry if possible
722
+ flow_id: str | None = None
723
+ reg = self.registry()
724
+ if reg is not None:
725
+ if kind == "taskgraph":
726
+ meta = reg.get_meta(nspace="graph", name=graph_id, version=None) or {}
727
+ elif kind == "graphfn":
728
+ meta = reg.get_meta(nspace="graphfn", name=graph_id, version=None) or {}
729
+ else:
730
+ meta = {}
731
+ flow_id = meta.get("flow_id") or graph_id
732
+
733
+ # use run_id as session_id if not provided
734
+ if session_id is None:
735
+ session_id = rid
736
+
737
+ record = RunRecord(
738
+ run_id=rid,
739
+ graph_id=graph_id,
740
+ kind=kind,
741
+ status=RunStatus.running, # we go straight to running as before
742
+ started_at=started_at,
743
+ tags=list(tags),
744
+ user_id=identity.user_id,
745
+ org_id=identity.org_id,
746
+ meta={},
747
+ session_id=session_id,
748
+ origin=RunOrigin.app, # app is a typical default for graph runs
749
+ visibility=RunVisibility.normal,
750
+ importance=RunImportance.normal,
751
+ agent_id=agent_id,
752
+ app_id=app_id,
753
+ )
754
+
755
+ if flow_id:
756
+ record.meta["flow_id"] = flow_id
757
+ if f"flow:{flow_id}" not in record.tags:
758
+ record.tags.append(f"flow:{flow_id}") # add flow tag if missing
759
+ if session_id:
760
+ record.meta["session_id"] = session_id
761
+ if f"session:{session_id}" not in record.tags:
762
+ record.tags.append(f"session:{session_id}") # add session tag if missing
763
+
764
+ if self._store is not None:
765
+ await self._store.create(record)
766
+
767
+ return await self._run_and_finalize(
768
+ record=record,
769
+ target=target,
770
+ graph_id=graph_id,
771
+ inputs=inputs,
772
+ identity=identity,
773
+ # agent_id=agent_id,
774
+ # app_id=app_id,
775
+ )