aethergraph 0.1.0a1__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 (182) hide show
  1. aethergraph/__init__.py +49 -0
  2. aethergraph/config/__init__.py +0 -0
  3. aethergraph/config/config.py +121 -0
  4. aethergraph/config/context.py +16 -0
  5. aethergraph/config/llm.py +26 -0
  6. aethergraph/config/loader.py +60 -0
  7. aethergraph/config/runtime.py +9 -0
  8. aethergraph/contracts/errors/errors.py +44 -0
  9. aethergraph/contracts/services/artifacts.py +142 -0
  10. aethergraph/contracts/services/channel.py +72 -0
  11. aethergraph/contracts/services/continuations.py +23 -0
  12. aethergraph/contracts/services/eventbus.py +12 -0
  13. aethergraph/contracts/services/kv.py +24 -0
  14. aethergraph/contracts/services/llm.py +17 -0
  15. aethergraph/contracts/services/mcp.py +22 -0
  16. aethergraph/contracts/services/memory.py +108 -0
  17. aethergraph/contracts/services/resume.py +28 -0
  18. aethergraph/contracts/services/state_stores.py +33 -0
  19. aethergraph/contracts/services/wakeup.py +28 -0
  20. aethergraph/core/execution/base_scheduler.py +77 -0
  21. aethergraph/core/execution/forward_scheduler.py +777 -0
  22. aethergraph/core/execution/global_scheduler.py +634 -0
  23. aethergraph/core/execution/retry_policy.py +22 -0
  24. aethergraph/core/execution/step_forward.py +411 -0
  25. aethergraph/core/execution/step_result.py +18 -0
  26. aethergraph/core/execution/wait_types.py +72 -0
  27. aethergraph/core/graph/graph_builder.py +192 -0
  28. aethergraph/core/graph/graph_fn.py +219 -0
  29. aethergraph/core/graph/graph_io.py +67 -0
  30. aethergraph/core/graph/graph_refs.py +154 -0
  31. aethergraph/core/graph/graph_spec.py +115 -0
  32. aethergraph/core/graph/graph_state.py +59 -0
  33. aethergraph/core/graph/graphify.py +128 -0
  34. aethergraph/core/graph/interpreter.py +145 -0
  35. aethergraph/core/graph/node_handle.py +33 -0
  36. aethergraph/core/graph/node_spec.py +46 -0
  37. aethergraph/core/graph/node_state.py +63 -0
  38. aethergraph/core/graph/task_graph.py +747 -0
  39. aethergraph/core/graph/task_node.py +82 -0
  40. aethergraph/core/graph/utils.py +37 -0
  41. aethergraph/core/graph/visualize.py +239 -0
  42. aethergraph/core/runtime/ad_hoc_context.py +61 -0
  43. aethergraph/core/runtime/base_service.py +153 -0
  44. aethergraph/core/runtime/bind_adapter.py +42 -0
  45. aethergraph/core/runtime/bound_memory.py +69 -0
  46. aethergraph/core/runtime/execution_context.py +220 -0
  47. aethergraph/core/runtime/graph_runner.py +349 -0
  48. aethergraph/core/runtime/lifecycle.py +26 -0
  49. aethergraph/core/runtime/node_context.py +203 -0
  50. aethergraph/core/runtime/node_services.py +30 -0
  51. aethergraph/core/runtime/recovery.py +159 -0
  52. aethergraph/core/runtime/run_registration.py +33 -0
  53. aethergraph/core/runtime/runtime_env.py +157 -0
  54. aethergraph/core/runtime/runtime_registry.py +32 -0
  55. aethergraph/core/runtime/runtime_services.py +224 -0
  56. aethergraph/core/runtime/wakeup_watcher.py +40 -0
  57. aethergraph/core/tools/__init__.py +10 -0
  58. aethergraph/core/tools/builtins/channel_tools.py +194 -0
  59. aethergraph/core/tools/builtins/toolset.py +134 -0
  60. aethergraph/core/tools/toolkit.py +510 -0
  61. aethergraph/core/tools/waitable.py +109 -0
  62. aethergraph/plugins/channel/__init__.py +0 -0
  63. aethergraph/plugins/channel/adapters/__init__.py +0 -0
  64. aethergraph/plugins/channel/adapters/console.py +106 -0
  65. aethergraph/plugins/channel/adapters/file.py +102 -0
  66. aethergraph/plugins/channel/adapters/slack.py +285 -0
  67. aethergraph/plugins/channel/adapters/telegram.py +302 -0
  68. aethergraph/plugins/channel/adapters/webhook.py +104 -0
  69. aethergraph/plugins/channel/adapters/webui.py +134 -0
  70. aethergraph/plugins/channel/routes/__init__.py +0 -0
  71. aethergraph/plugins/channel/routes/console_routes.py +86 -0
  72. aethergraph/plugins/channel/routes/slack_routes.py +49 -0
  73. aethergraph/plugins/channel/routes/telegram_routes.py +26 -0
  74. aethergraph/plugins/channel/routes/webui_routes.py +136 -0
  75. aethergraph/plugins/channel/utils/__init__.py +0 -0
  76. aethergraph/plugins/channel/utils/slack_utils.py +278 -0
  77. aethergraph/plugins/channel/utils/telegram_utils.py +324 -0
  78. aethergraph/plugins/channel/websockets/slack_ws.py +68 -0
  79. aethergraph/plugins/channel/websockets/telegram_polling.py +151 -0
  80. aethergraph/plugins/mcp/fs_server.py +128 -0
  81. aethergraph/plugins/mcp/http_server.py +101 -0
  82. aethergraph/plugins/mcp/ws_server.py +180 -0
  83. aethergraph/plugins/net/http.py +10 -0
  84. aethergraph/plugins/utils/data_io.py +359 -0
  85. aethergraph/runner/__init__.py +5 -0
  86. aethergraph/runtime/__init__.py +62 -0
  87. aethergraph/server/__init__.py +3 -0
  88. aethergraph/server/app_factory.py +84 -0
  89. aethergraph/server/start.py +122 -0
  90. aethergraph/services/__init__.py +10 -0
  91. aethergraph/services/artifacts/facade.py +284 -0
  92. aethergraph/services/artifacts/factory.py +35 -0
  93. aethergraph/services/artifacts/fs_store.py +656 -0
  94. aethergraph/services/artifacts/jsonl_index.py +123 -0
  95. aethergraph/services/artifacts/paths.py +23 -0
  96. aethergraph/services/artifacts/sqlite_index.py +209 -0
  97. aethergraph/services/artifacts/utils.py +124 -0
  98. aethergraph/services/auth/dev.py +16 -0
  99. aethergraph/services/channel/channel_bus.py +293 -0
  100. aethergraph/services/channel/factory.py +44 -0
  101. aethergraph/services/channel/session.py +511 -0
  102. aethergraph/services/channel/wait_helpers.py +57 -0
  103. aethergraph/services/clock/clock.py +9 -0
  104. aethergraph/services/container/default_container.py +320 -0
  105. aethergraph/services/continuations/continuation.py +56 -0
  106. aethergraph/services/continuations/factory.py +34 -0
  107. aethergraph/services/continuations/stores/fs_store.py +264 -0
  108. aethergraph/services/continuations/stores/inmem_store.py +95 -0
  109. aethergraph/services/eventbus/inmem.py +21 -0
  110. aethergraph/services/features/static.py +10 -0
  111. aethergraph/services/kv/ephemeral.py +90 -0
  112. aethergraph/services/kv/factory.py +27 -0
  113. aethergraph/services/kv/layered.py +41 -0
  114. aethergraph/services/kv/sqlite_kv.py +128 -0
  115. aethergraph/services/llm/factory.py +157 -0
  116. aethergraph/services/llm/generic_client.py +542 -0
  117. aethergraph/services/llm/providers.py +3 -0
  118. aethergraph/services/llm/service.py +105 -0
  119. aethergraph/services/logger/base.py +36 -0
  120. aethergraph/services/logger/compat.py +50 -0
  121. aethergraph/services/logger/formatters.py +106 -0
  122. aethergraph/services/logger/std.py +203 -0
  123. aethergraph/services/mcp/helpers.py +23 -0
  124. aethergraph/services/mcp/http_client.py +70 -0
  125. aethergraph/services/mcp/mcp_tools.py +21 -0
  126. aethergraph/services/mcp/registry.py +14 -0
  127. aethergraph/services/mcp/service.py +100 -0
  128. aethergraph/services/mcp/stdio_client.py +70 -0
  129. aethergraph/services/mcp/ws_client.py +115 -0
  130. aethergraph/services/memory/bound.py +106 -0
  131. aethergraph/services/memory/distillers/episode.py +116 -0
  132. aethergraph/services/memory/distillers/rolling.py +74 -0
  133. aethergraph/services/memory/facade.py +633 -0
  134. aethergraph/services/memory/factory.py +78 -0
  135. aethergraph/services/memory/hotlog_kv.py +27 -0
  136. aethergraph/services/memory/indices.py +74 -0
  137. aethergraph/services/memory/io_helpers.py +72 -0
  138. aethergraph/services/memory/persist_fs.py +40 -0
  139. aethergraph/services/memory/resolver.py +152 -0
  140. aethergraph/services/metering/noop.py +4 -0
  141. aethergraph/services/prompts/file_store.py +41 -0
  142. aethergraph/services/rag/chunker.py +29 -0
  143. aethergraph/services/rag/facade.py +593 -0
  144. aethergraph/services/rag/index/base.py +27 -0
  145. aethergraph/services/rag/index/faiss_index.py +121 -0
  146. aethergraph/services/rag/index/sqlite_index.py +134 -0
  147. aethergraph/services/rag/index_factory.py +52 -0
  148. aethergraph/services/rag/parsers/md.py +7 -0
  149. aethergraph/services/rag/parsers/pdf.py +14 -0
  150. aethergraph/services/rag/parsers/txt.py +7 -0
  151. aethergraph/services/rag/utils/hybrid.py +39 -0
  152. aethergraph/services/rag/utils/make_fs_key.py +62 -0
  153. aethergraph/services/redactor/simple.py +16 -0
  154. aethergraph/services/registry/key_parsing.py +44 -0
  155. aethergraph/services/registry/registry_key.py +19 -0
  156. aethergraph/services/registry/unified_registry.py +185 -0
  157. aethergraph/services/resume/multi_scheduler_resume_bus.py +65 -0
  158. aethergraph/services/resume/router.py +73 -0
  159. aethergraph/services/schedulers/registry.py +41 -0
  160. aethergraph/services/secrets/base.py +7 -0
  161. aethergraph/services/secrets/env.py +8 -0
  162. aethergraph/services/state_stores/externalize.py +135 -0
  163. aethergraph/services/state_stores/graph_observer.py +131 -0
  164. aethergraph/services/state_stores/json_store.py +67 -0
  165. aethergraph/services/state_stores/resume_policy.py +119 -0
  166. aethergraph/services/state_stores/serialize.py +249 -0
  167. aethergraph/services/state_stores/utils.py +91 -0
  168. aethergraph/services/state_stores/validate.py +78 -0
  169. aethergraph/services/tracing/noop.py +18 -0
  170. aethergraph/services/waits/wait_registry.py +91 -0
  171. aethergraph/services/wakeup/memory_queue.py +57 -0
  172. aethergraph/services/wakeup/scanner_producer.py +56 -0
  173. aethergraph/services/wakeup/worker.py +31 -0
  174. aethergraph/tools/__init__.py +25 -0
  175. aethergraph/utils/optdeps.py +8 -0
  176. aethergraph-0.1.0a1.dist-info/METADATA +410 -0
  177. aethergraph-0.1.0a1.dist-info/RECORD +182 -0
  178. aethergraph-0.1.0a1.dist-info/WHEEL +5 -0
  179. aethergraph-0.1.0a1.dist-info/entry_points.txt +2 -0
  180. aethergraph-0.1.0a1.dist-info/licenses/LICENSE +176 -0
  181. aethergraph-0.1.0a1.dist-info/licenses/NOTICE +31 -0
  182. aethergraph-0.1.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from aethergraph.contracts.services.artifacts import AsyncArtifactStore # generic protocol
7
+ from aethergraph.contracts.services.memory import HotLog, Indices, Persistence
8
+ from aethergraph.services.memory.facade import MemoryFacade
9
+
10
+ """
11
+ # --- Artifacts (async FS store)
12
+ artifacts = FSArtifactStore(artifacts_dir)
13
+
14
+ # --- KV for hotlog/indices (choose EphemeralKV or SQLiteKV)
15
+ kv = SQLiteKV(f"{artifacts_dir}/kv.sqlite") if durable else EphemeralKV()
16
+
17
+ # --- HotLog + Indices
18
+ hotlog = KVHotLog(kv, default_ttl_s=7*24*3600, default_limit=1000)
19
+ indices = KVIndices(kv, ttl_s=7*24*3600)
20
+
21
+ # --- Persistence (JSONL under artifacts_dir/mem/<session>/events/...)
22
+ persistence = FSPersistence(base_dir=artifacts_dir)
23
+
24
+ # --- Factory
25
+ factory = MemoryFactory(
26
+ hotlog=hotlog,
27
+ persistence=persistence,
28
+ indices=indices,
29
+ artifacts=artifacts,
30
+ hot_limit=1000,
31
+ hot_ttl_s=7*24*3600,
32
+ default_signal_threshold=0.25,
33
+ )
34
+
35
+ # --- Global session handle (optional convenience)
36
+ global_mem = factory.for_session("global", run_id="global")
37
+ """
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class MemoryFactory:
42
+ """Factory for creating MemoryFacade instances with shared components."""
43
+
44
+ hotlog: HotLog
45
+ persistence: Persistence
46
+ indices: Indices # key-value backed indices for fast lookups, not artifact storage index
47
+ artifacts: AsyncArtifactStore
48
+ hot_limit: int = 1000
49
+ hot_ttl_s: int = 7 * 24 * 3600
50
+ default_signal_threshold: float = 0.25
51
+ logger: Any | None = None
52
+ llm_service: Any | None = None # LLMService
53
+ rag_facade: Any | None = None # RAGFacade
54
+
55
+ def for_session(
56
+ self,
57
+ run_id: str,
58
+ *,
59
+ graph_id: str | None = None,
60
+ node_id: str | None = None,
61
+ agent_id: str | None = None,
62
+ ) -> MemoryFacade:
63
+ return MemoryFacade(
64
+ run_id=run_id,
65
+ graph_id=graph_id,
66
+ node_id=node_id,
67
+ agent_id=agent_id,
68
+ hotlog=self.hotlog,
69
+ persistence=self.persistence,
70
+ indices=self.indices,
71
+ artifact_store=self.artifacts,
72
+ hot_limit=self.hot_limit,
73
+ hot_ttl_s=self.hot_ttl_s,
74
+ default_signal_threshold=self.default_signal_threshold,
75
+ logger=self.logger,
76
+ rag=self.rag_facade,
77
+ llm=self.llm_service,
78
+ )
@@ -0,0 +1,27 @@
1
+ from aethergraph.contracts.services.kv import AsyncKV
2
+ from aethergraph.contracts.services.memory import Event, HotLog
3
+
4
+
5
+ def kv_hot_key(run_id: str) -> str:
6
+ return f"mem:{run_id}:hot"
7
+
8
+
9
+ class KVHotLog(HotLog):
10
+ def __init__(self, kv: AsyncKV):
11
+ self.kv = kv
12
+
13
+ async def append(self, run_id: str, evt: Event, *, ttl_s: int, limit: int) -> None:
14
+ key = kv_hot_key(run_id)
15
+ buf = list((await self.kv.get(key, default=[])) or [])
16
+ buf.append(evt.__dict__) # store as dict for JSON serializability
17
+ if len(buf) > limit:
18
+ buf = buf[-limit:]
19
+ await self.kv.set(key, buf, ttl_s=ttl_s)
20
+
21
+ async def recent(
22
+ self, run_id: str, *, kinds: list[str] | None = None, limit: int = 50
23
+ ) -> list[Event]:
24
+ buf = (await self.kv.get(kv_hot_key(run_id), default=[])) or []
25
+ if kinds:
26
+ buf = [e for e in buf if e.get("kind") in kinds]
27
+ return [Event(**e) for e in buf[-limit:]]
@@ -0,0 +1,74 @@
1
+ from typing import Any
2
+
3
+ from aethergraph.contracts.services.kv import AsyncKV
4
+ from aethergraph.contracts.services.memory import Event, Indices
5
+
6
+
7
+ def idx_by_ref_kind(run_id: str) -> str:
8
+ return f"mem:{run_id}:idx2:ref_kind"
9
+
10
+
11
+ def idx_by_name(run_id: str) -> str:
12
+ return f"mem:{run_id}:idx2:name"
13
+
14
+
15
+ def idx_by_topic(run_id: str) -> str:
16
+ return f"mem:{run_id}:idx2:topic"
17
+
18
+
19
+ class KVIndices(Indices):
20
+ def __init__(self, kv: AsyncKV, hot_ttl_s: int):
21
+ self.kv = kv
22
+ self.ttl = hot_ttl_s
23
+
24
+ async def update(self, run_id: str, evt: Event) -> None:
25
+ ts, eid, tool = evt.ts, evt.event_id, (evt.tool or "")
26
+ outs = evt.outputs or []
27
+
28
+ by_kind = (await self.kv.get(idx_by_ref_kind(run_id), {})) or {}
29
+ by_name = (await self.kv.get(idx_by_name(run_id), {})) or {}
30
+ by_topic = (await self.kv.get(idx_by_topic(run_id), {})) or {}
31
+
32
+ for v in outs:
33
+ nm = v.get("name")
34
+ if not nm:
35
+ continue
36
+ by_name[nm] = {
37
+ "ts": ts,
38
+ "event_id": eid,
39
+ "vtype": v.get("vtype"),
40
+ "value": v.get("value"),
41
+ }
42
+ if v.get("vtype") == "ref" and isinstance(v.get("value"), dict):
43
+ kind = v["value"].get("kind")
44
+ uri = v["value"].get("uri")
45
+ if kind and uri:
46
+ lst = by_kind.setdefault(kind, [])
47
+ lst.append({"ts": ts, "event_id": eid, "name": nm, "uri": uri, "topic": tool})
48
+ if len(lst) > 200:
49
+ del lst[:-200]
50
+
51
+ if tool:
52
+ last = by_topic.get(tool, {}) or {}
53
+ last["ts"] = ts
54
+ last["event_id"] = eid
55
+ last["last_outputs"] = {v["name"]: v.get("value") for v in outs if v.get("name")}
56
+ by_topic[tool] = last
57
+
58
+ await self.kv.set(idx_by_ref_kind(run_id), by_kind, ttl_s=self.ttl)
59
+ await self.kv.set(idx_by_name(run_id), by_name, ttl_s=self.ttl)
60
+ await self.kv.set(idx_by_topic(run_id), by_topic, ttl_s=self.ttl)
61
+
62
+ async def last_by_name(self, run_id: str, name: str) -> dict[str, Any] | None:
63
+ by_name = await self.kv.get(idx_by_name(run_id), {}) or {}
64
+ return by_name.get(name)
65
+
66
+ async def latest_refs_by_kind(
67
+ self, run_id: str, kind: str, *, limit: int = 50
68
+ ) -> list[dict[str, Any]]:
69
+ by_kind = await self.kv.get(idx_by_ref_kind(run_id), {}) or {}
70
+ return list(reversed((by_kind.get(kind) or [])[-limit:]))
71
+
72
+ async def last_outputs_by_topic(self, run_id: str, topic: str) -> dict[str, Any] | None:
73
+ by_topic = await self.kv.get(idx_by_topic(run_id), {}) or {}
74
+ return by_topic.get(topic)
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ from aethergraph.contracts.services.memory import Value
4
+
5
+ """Create a Value of vtype 'ref' pointing to the given kind and uri.
6
+ Args:
7
+ name: name of the Value slot
8
+ kind: kind of the referenced artifact, e.g. "spec", "design", "output", "tool_result"
9
+ uri: URI of the referenced artifact, e.g. "file://...", "mem://...", "db://..."
10
+ meta: optional additional metadata for the Ref
11
+ Returns:
12
+ Value dict with vtype 'ref'
13
+
14
+ Example:
15
+ v = ref(
16
+ name="my_ref",
17
+ kind="spec",
18
+ uri="file://path/to/spec",
19
+ title="My Spec",
20
+ mime="application/json"
21
+ )
22
+ print(v)
23
+ # Output: {
24
+ # "name": "my_ref",
25
+ # "vtype": "ref",
26
+ # "value": {
27
+ # "kind": "spec",
28
+ # "uri": "file://path/to/spec",
29
+ # "title": "My Spec",
30
+ # "mime": "application/json"
31
+ # }
32
+ # }
33
+ """
34
+
35
+
36
+ def ref(name: str, kind: str, uri: str, **meta) -> Value:
37
+ v: Value = {
38
+ "name": name,
39
+ "vtype": "ref",
40
+ "value": {"kind": kind, "uri": uri, **meta},
41
+ }
42
+ return v
43
+
44
+
45
+ def num(name: str, x: float) -> Value:
46
+ """Create a Value of vtype 'number'."""
47
+ return {"name": name, "vtype": "number", "value": float(x)}
48
+
49
+
50
+ def text(name: str, s: str) -> Value:
51
+ """Create a Value of vtype 'string'."""
52
+ return {"name": name, "vtype": "string", "value": str(s)}
53
+
54
+
55
+ def flag(name: str, b: bool) -> Value:
56
+ """Create a Value of vtype 'boolean'."""
57
+ return {"name": name, "vtype": "boolean", "value": bool(b)}
58
+
59
+
60
+ def obj(name: str, d: dict) -> Value:
61
+ """Create a Value of vtype 'object'."""
62
+ return {"name": name, "vtype": "object", "value": dict(d)}
63
+
64
+
65
+ def arr(name: str, lst: list) -> Value:
66
+ """Create a Value of vtype 'array'."""
67
+ return {"name": name, "vtype": "array", "value": list(lst)}
68
+
69
+
70
+ def null(name: str) -> Value:
71
+ """Create a Value of vtype 'null'."""
72
+ return {"name": name, "vtype": "null", "value": None}
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from dataclasses import asdict
5
+ import json
6
+ import os
7
+ import time
8
+
9
+ from aethergraph.contracts.services.memory import Event, Persistence
10
+
11
+
12
+ class FSPersistence(Persistence):
13
+ def __init__(self, *, base_dir: str):
14
+ self.base_dir = os.path.abspath(base_dir)
15
+
16
+ async def append_event(self, run_id: str, evt: Event) -> None:
17
+ day = time.strftime("%Y-%m-%d", time.gmtime())
18
+ rel = os.path.join("mem", run_id, "events", f"{day}.jsonl")
19
+ path = os.path.join(self.base_dir, rel)
20
+
21
+ def _write():
22
+ os.makedirs(os.path.dirname(path), exist_ok=True)
23
+ with open(path, "a", encoding="utf-8") as f:
24
+ f.write(json.dumps(asdict(evt), ensure_ascii=False) + "\n")
25
+
26
+ await asyncio.to_thread(_write)
27
+
28
+ async def save_json(self, uri: str, obj: dict[str, any]) -> None:
29
+ assert uri.startswith("file://"), f"FSPersistence only supports file://, got {uri!r}"
30
+ rel = uri[len("file://") :].lstrip("/\\")
31
+ path = os.path.join(self.base_dir, rel)
32
+
33
+ def _write():
34
+ os.makedirs(os.path.dirname(path), exist_ok=True)
35
+ tmp = path + ".tmp"
36
+ with open(tmp, "w", encoding="utf-8") as f:
37
+ json.dump(obj, f, ensure_ascii=False, indent=2)
38
+ os.replace(tmp, path)
39
+
40
+ await asyncio.to_thread(_write)
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+ from aethergraph.services.memory.facade import MemoryFacade
7
+
8
+ # -------- Regexes (unchanged) --------
9
+ STEP_RE = re.compile(r"^\s*\$step\[(?P<idx>-?\d+)\]\.refs\.(?P<key>\w+)\s*$")
10
+ FROM_RE = re.compile(r"^\s*\$from:(\w+)\s*$")
11
+ VAR_RE = re.compile(r"^\s*\$var:(\w+)\s*$")
12
+
13
+ REF_KIND_RE = re.compile(r"^\s*\$resolve\s*:\s*ref\.kind\s*=\s*(\w+)\s*\|\s*last\s*$", re.I)
14
+ NAME_RE = re.compile(r"^\s*\$resolve\s*:\s*name\s*=\s*(\w+)\s*\|\s*last\s*$", re.I)
15
+ TOPIC_NAME_RE = re.compile(
16
+ r"^\s*\$resolve\s*:\s*topic\s*=\s*([\w\.\-\/]+)\s*\|\s*name\s*=\s*(\w+)\s*$", re.I
17
+ )
18
+ LEGACY_KIND_RE = re.compile(r"^\s*\$resolve\s*:\s*kind\s*=\s*(\w+)\s*\|\s*last\s*$", re.I)
19
+
20
+
21
+ class ResolverContext:
22
+ def __init__(
23
+ self, mem: MemoryFacade, seq_ctx: dict | None = None, vars: dict[str, Any] | None = None
24
+ ):
25
+ self.mem = mem
26
+ self.seq_ctx = seq_ctx or {}
27
+ self.vars = vars or {}
28
+
29
+
30
+ def _get_step_outputs(seq_ctx: dict, j: int) -> dict[str, Any] | None:
31
+ steps = seq_ctx.get("steps") or []
32
+ if 0 <= j < len(steps):
33
+ return steps[j].get("outputs") or {}
34
+ return None
35
+
36
+
37
+ async def _latest_ref_by_kind(mem: MemoryFacade, kind: str) -> str | None:
38
+ arr = await mem.latest_refs_by_kind(kind, limit=1)
39
+ if arr:
40
+ return arr[0].get("uri")
41
+ # Fallback scan
42
+ events = await mem.recent(kinds=["tool_result", "checkpoint"], limit=400)
43
+ for e in reversed(events):
44
+ outs = e.outputs or []
45
+ for v in outs:
46
+ if (
47
+ v.get("vtype") == "ref"
48
+ and isinstance(v.get("value"), dict)
49
+ and v["value"].get("kind") == kind
50
+ ):
51
+ return v["value"].get("uri")
52
+ # legacy
53
+ if e.outputs_ref and f"{kind}_ref" in e.outputs_ref:
54
+ return e.outputs_ref.get(f"{kind}_ref")
55
+ return None
56
+
57
+
58
+ async def _latest_value_by_name(mem: MemoryFacade, name: str) -> Any | None:
59
+ ent = await mem.last_by_name(name)
60
+ if ent:
61
+ return ent.get("value")
62
+ # Fallback scan
63
+ events = await mem.recent(kinds=["tool_result", "checkpoint"], limit=400)
64
+ for e in reversed(events):
65
+ outs = e.outputs or []
66
+ for v in outs:
67
+ if v.get("name") == name:
68
+ return v.get("value")
69
+ if e.outputs_ref and name in e.outputs_ref:
70
+ return e.outputs_ref.get(name)
71
+ return None
72
+
73
+
74
+ async def _latest_value_by_topic_name(mem: MemoryFacade, topic: str, name: str) -> Any | None:
75
+ ent = await mem.last_outputs_by_topic(topic)
76
+ if ent:
77
+ last = ent.get("last_outputs") or {}
78
+ if name in last:
79
+ return last[name]
80
+ # Fallback scan
81
+ events = await mem.recent(kinds=["tool_result"], limit=400)
82
+ for e in reversed(events):
83
+ if (e.tool or "") != topic:
84
+ continue
85
+ outs = e.outputs or []
86
+ for v in outs:
87
+ if v.get("name") == name:
88
+ return v.get("value")
89
+ if e.outputs_ref and name in e.outputs_ref:
90
+ return e.outputs_ref.get(name)
91
+ return None
92
+
93
+
94
+ async def resolve_params(raw: dict[str, Any], ctx: ResolverContext) -> dict[str, Any]:
95
+ out = dict(raw)
96
+
97
+ # 1) $step[i].refs.key
98
+ for k, v in list(out.items()):
99
+ if not isinstance(v, str):
100
+ continue
101
+ m = STEP_RE.match(v)
102
+ if not m:
103
+ continue
104
+ idx = int(m.group("idx"))
105
+ key = m.group("key")
106
+ steps = ctx.seq_ctx.get("steps") or []
107
+ j = idx if idx >= 0 else len(steps) + idx
108
+ refs = _get_step_outputs(ctx.seq_ctx, j)
109
+ if refs and key in refs:
110
+ out[k] = refs[key]
111
+ else:
112
+ if key.endswith("_ref"):
113
+ kind = key[:-4]
114
+ out[k] = await _latest_ref_by_kind(ctx.mem, kind)
115
+ else:
116
+ out[k] = None
117
+
118
+ # 2) $from:TAG (example strategy slot)
119
+ for k, v in list(out.items()):
120
+ if not isinstance(v, str):
121
+ continue
122
+ m = FROM_RE.match(v)
123
+ if m:
124
+ tag = m.group(1)
125
+ resolved = None
126
+ if tag == "last_opt_top1":
127
+ resolved = await _latest_value_by_topic_name(ctx.mem, "optimize.flow", "top1_ref")
128
+ out[k] = resolved
129
+
130
+ # 3) New selectors + legacy
131
+ for k, v in list(out.items()):
132
+ if not isinstance(v, str):
133
+ continue
134
+ if m := REF_KIND_RE.match(v):
135
+ out[k] = await _latest_ref_by_kind(ctx.mem, m.group(1).lower())
136
+ continue
137
+ if m := NAME_RE.match(v):
138
+ out[k] = await _latest_value_by_name(ctx.mem, m.group(1))
139
+ continue
140
+ if m := TOPIC_NAME_RE.match(v):
141
+ out[k] = await _latest_value_by_topic_name(ctx.mem, m.group(1), m.group(2))
142
+ continue
143
+ if m := LEGACY_KIND_RE.match(v):
144
+ out[k] = await _latest_ref_by_kind(ctx.mem, m.group(1).lower())
145
+ continue
146
+
147
+ # 4) $var:NAME
148
+ for k, v in list(out.items()):
149
+ if isinstance(v, str) and (m := VAR_RE.match(v)):
150
+ out[k] = ctx.vars.get(m.group(1))
151
+
152
+ return out
@@ -0,0 +1,4 @@
1
+ # services/metering/noop.py
2
+ class NoopMetering:
3
+ async def incr(self, metric: str, value: float = 1.0, **tags):
4
+ return None
@@ -0,0 +1,41 @@
1
+ # services/prompts/file_store.py
2
+ # Simple file-based prompt store
3
+ from pathlib import Path
4
+
5
+
6
+ class FilePromptStore:
7
+ def __init__(self, root: str = "./prompts"):
8
+ self.root = Path(root)
9
+
10
+ async def get(self, name: str, version: str | None = None) -> str:
11
+ """Get prompt by name and optional version.
12
+ If version is None, get the latest (unversioned) prompt.
13
+
14
+ Args:
15
+ name: Prompt name (filename without extension)
16
+ version: Optional version string
17
+ Returns:
18
+ Prompt content as string
19
+
20
+ Example:
21
+ prompt = await store.get("welcome_message", version="v1")
22
+ print(prompt)
23
+ # ./prompts/welcome_message@v1.md
24
+ """
25
+ p = self.root / (f"{name}@{version}.md" if version else f"{name}.md")
26
+ return p.read_text(encoding="utf-8")
27
+
28
+ async def render(self, name: str, **vars) -> str:
29
+ """Get and render prompt with variable substitution.
30
+
31
+ Args:
32
+ name: Prompt name (filename without extension)
33
+ **vars: Variables to substitute in the prompt
34
+ Returns:
35
+ Rendered prompt content as string
36
+ """
37
+ # Tiny {{var}} replacement; swap later for jinja/mustache
38
+ txt = await self.get(name)
39
+ for k, v in vars.items():
40
+ txt = txt.replace(f"{{{{{k}}}}}", str(v))
41
+ return txt
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class TextSplitter:
5
+ """A simple text splitter that splits text into chunks of approximately target_tokens,
6
+ with a specified overlap in tokens.
7
+
8
+ Example:
9
+ splitter = TextSplitter(target_tokens=400, overlap_tokens=60)
10
+ chunks = splitter.split(long_text)
11
+ for chunk in chunks:
12
+ print(chunk)
13
+ """
14
+
15
+ def __init__(self, target_tokens: int = 400, overlap_tokens: int = 60):
16
+ self.n = max(50, target_tokens)
17
+ self.o = max(0, min(self.n - 1, overlap_tokens))
18
+
19
+ def split(self, text: str) -> list[str]:
20
+ words = text.split()
21
+ if not words:
22
+ return []
23
+ step = self.n - self.o
24
+ chunks = []
25
+ for i in range(0, len(words), step):
26
+ chunk = " ".join(words[i : i + self.n])
27
+ if chunk.strip():
28
+ chunks.append(chunk)
29
+ return chunks