AbstractRuntime 0.2.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- abstractruntime/__init__.py +7 -2
- abstractruntime/core/config.py +14 -1
- abstractruntime/core/event_keys.py +62 -0
- abstractruntime/core/models.py +12 -1
- abstractruntime/core/runtime.py +2444 -14
- abstractruntime/core/vars.py +95 -0
- abstractruntime/evidence/__init__.py +10 -0
- abstractruntime/evidence/recorder.py +325 -0
- abstractruntime/integrations/abstractcore/__init__.py +3 -0
- abstractruntime/integrations/abstractcore/constants.py +19 -0
- abstractruntime/integrations/abstractcore/default_tools.py +134 -0
- abstractruntime/integrations/abstractcore/effect_handlers.py +255 -6
- abstractruntime/integrations/abstractcore/factory.py +95 -10
- abstractruntime/integrations/abstractcore/llm_client.py +456 -52
- abstractruntime/integrations/abstractcore/mcp_worker.py +586 -0
- abstractruntime/integrations/abstractcore/observability.py +80 -0
- abstractruntime/integrations/abstractcore/summarizer.py +154 -0
- abstractruntime/integrations/abstractcore/tool_executor.py +481 -24
- abstractruntime/memory/__init__.py +21 -0
- abstractruntime/memory/active_context.py +746 -0
- abstractruntime/memory/active_memory.py +452 -0
- abstractruntime/memory/compaction.py +105 -0
- abstractruntime/rendering/__init__.py +17 -0
- abstractruntime/rendering/agent_trace_report.py +256 -0
- abstractruntime/rendering/json_stringify.py +136 -0
- abstractruntime/scheduler/scheduler.py +93 -2
- abstractruntime/storage/__init__.py +3 -1
- abstractruntime/storage/artifacts.py +20 -5
- abstractruntime/storage/json_files.py +15 -2
- abstractruntime/storage/observable.py +99 -0
- {abstractruntime-0.2.0.dist-info → abstractruntime-0.4.0.dist-info}/METADATA +5 -1
- abstractruntime-0.4.0.dist-info/RECORD +49 -0
- abstractruntime-0.4.0.dist-info/entry_points.txt +2 -0
- abstractruntime-0.2.0.dist-info/RECORD +0 -32
- {abstractruntime-0.2.0.dist-info → abstractruntime-0.4.0.dist-info}/WHEEL +0 -0
- {abstractruntime-0.2.0.dist-info → abstractruntime-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -11,7 +11,8 @@ They are designed to keep `RunState.vars` JSON-safe.
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
import json
|
|
15
|
+
from typing import Any, Dict, Optional, Set, Tuple, Type
|
|
15
16
|
|
|
16
17
|
from ...core.models import Effect, EffectType, RunState, WaitReason, WaitState
|
|
17
18
|
from ...core.runtime import EffectOutcome, EffectHandler
|
|
@@ -22,8 +23,96 @@ from .logging import get_logger
|
|
|
22
23
|
logger = get_logger(__name__)
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
def _jsonable(value: Any) -> Any:
|
|
27
|
+
"""Best-effort conversion to JSON-safe objects.
|
|
28
|
+
|
|
29
|
+
Runtime traces and effect outcomes are persisted in RunState.vars and must remain JSON-safe.
|
|
30
|
+
"""
|
|
31
|
+
if value is None:
|
|
32
|
+
return None
|
|
33
|
+
if isinstance(value, (str, int, float, bool)):
|
|
34
|
+
return value
|
|
35
|
+
if isinstance(value, dict):
|
|
36
|
+
return {str(k): _jsonable(v) for k, v in value.items()}
|
|
37
|
+
if isinstance(value, list):
|
|
38
|
+
return [_jsonable(v) for v in value]
|
|
39
|
+
try:
|
|
40
|
+
json.dumps(value)
|
|
41
|
+
return value
|
|
42
|
+
except Exception:
|
|
43
|
+
return str(value)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _pydantic_model_from_json_schema(schema: Dict[str, Any], *, name: str) -> Type[Any]:
|
|
47
|
+
"""Best-effort conversion from a JSON schema dict to a Pydantic model.
|
|
48
|
+
|
|
49
|
+
This exists so structured output requests can remain JSON-safe in durable
|
|
50
|
+
effect payloads (we persist the schema, not the Python class).
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
from pydantic import BaseModel, create_model
|
|
54
|
+
except Exception as e: # pragma: no cover
|
|
55
|
+
raise RuntimeError(f"Pydantic is required for structured outputs: {e}")
|
|
56
|
+
|
|
57
|
+
def _python_type(sub_schema: Any, *, nested_name: str) -> Any:
|
|
58
|
+
if not isinstance(sub_schema, dict):
|
|
59
|
+
return Any
|
|
60
|
+
t = sub_schema.get("type")
|
|
61
|
+
if t == "string":
|
|
62
|
+
return str
|
|
63
|
+
if t == "integer":
|
|
64
|
+
return int
|
|
65
|
+
if t == "number":
|
|
66
|
+
return float
|
|
67
|
+
if t == "boolean":
|
|
68
|
+
return bool
|
|
69
|
+
if t == "array":
|
|
70
|
+
items = sub_schema.get("items")
|
|
71
|
+
return list[_python_type(items, nested_name=f"{nested_name}Item")] # type: ignore[index]
|
|
72
|
+
if t == "object":
|
|
73
|
+
props = sub_schema.get("properties")
|
|
74
|
+
if isinstance(props, dict) and props:
|
|
75
|
+
return _model(sub_schema, name=nested_name)
|
|
76
|
+
return Dict[str, Any]
|
|
77
|
+
return Any
|
|
78
|
+
|
|
79
|
+
def _model(obj_schema: Dict[str, Any], *, name: str) -> Type[BaseModel]:
|
|
80
|
+
if obj_schema.get("type") != "object":
|
|
81
|
+
raise ValueError("response_schema must be a JSON schema object")
|
|
82
|
+
props = obj_schema.get("properties")
|
|
83
|
+
if not isinstance(props, dict) or not props:
|
|
84
|
+
raise ValueError("response_schema must define properties")
|
|
85
|
+
required_raw = obj_schema.get("required")
|
|
86
|
+
required: Set[str] = set()
|
|
87
|
+
if isinstance(required_raw, list):
|
|
88
|
+
required = {str(x) for x in required_raw if isinstance(x, str)}
|
|
89
|
+
|
|
90
|
+
fields: Dict[str, Tuple[Any, Any]] = {}
|
|
91
|
+
for prop_name, prop_schema in props.items():
|
|
92
|
+
if not isinstance(prop_name, str) or not prop_name.strip():
|
|
93
|
+
continue
|
|
94
|
+
# Keep things simple: only support identifier-like names to avoid aliasing issues.
|
|
95
|
+
if not prop_name.isidentifier():
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Invalid property name '{prop_name}'. Use identifier-style names (letters, digits, underscore)."
|
|
98
|
+
)
|
|
99
|
+
t = _python_type(prop_schema, nested_name=f"{name}_{prop_name}")
|
|
100
|
+
if prop_name in required:
|
|
101
|
+
fields[prop_name] = (t, ...)
|
|
102
|
+
else:
|
|
103
|
+
fields[prop_name] = (Optional[t], None)
|
|
104
|
+
|
|
105
|
+
return create_model(name, **fields) # type: ignore[call-arg]
|
|
106
|
+
|
|
107
|
+
return _model(schema, name=name)
|
|
108
|
+
|
|
109
|
+
|
|
25
110
|
def _trace_context(run: RunState) -> Dict[str, str]:
|
|
26
|
-
ctx: Dict[str, str] = {
|
|
111
|
+
ctx: Dict[str, str] = {
|
|
112
|
+
"run_id": run.run_id,
|
|
113
|
+
"workflow_id": str(run.workflow_id),
|
|
114
|
+
"node_id": str(run.current_node),
|
|
115
|
+
}
|
|
27
116
|
if run.actor_id:
|
|
28
117
|
ctx["actor_id"] = str(run.actor_id)
|
|
29
118
|
session_id = getattr(run, "session_id", None)
|
|
@@ -40,7 +129,11 @@ def make_llm_call_handler(*, llm: AbstractCoreLLMClient) -> EffectHandler:
|
|
|
40
129
|
prompt = payload.get("prompt")
|
|
41
130
|
messages = payload.get("messages")
|
|
42
131
|
system_prompt = payload.get("system_prompt")
|
|
132
|
+
provider = payload.get("provider")
|
|
133
|
+
model = payload.get("model")
|
|
43
134
|
tools = payload.get("tools")
|
|
135
|
+
response_schema = payload.get("response_schema")
|
|
136
|
+
response_schema_name = payload.get("response_schema_name")
|
|
44
137
|
raw_params = payload.get("params")
|
|
45
138
|
params = dict(raw_params) if isinstance(raw_params, dict) else {}
|
|
46
139
|
|
|
@@ -51,10 +144,38 @@ def make_llm_call_handler(*, llm: AbstractCoreLLMClient) -> EffectHandler:
|
|
|
51
144
|
trace_metadata.update(_trace_context(run))
|
|
52
145
|
params["trace_metadata"] = trace_metadata
|
|
53
146
|
|
|
147
|
+
# Support per-effect routing: allow the payload to override provider/model.
|
|
148
|
+
# These reserved keys are consumed by MultiLocalAbstractCoreLLMClient and
|
|
149
|
+
# ignored by LocalAbstractCoreLLMClient.
|
|
150
|
+
if isinstance(provider, str) and provider.strip():
|
|
151
|
+
params["_provider"] = provider.strip()
|
|
152
|
+
if isinstance(model, str) and model.strip():
|
|
153
|
+
params["_model"] = model.strip()
|
|
154
|
+
|
|
54
155
|
if not prompt and not messages:
|
|
55
156
|
return EffectOutcome.failed("llm_call requires payload.prompt or payload.messages")
|
|
56
157
|
|
|
57
158
|
try:
|
|
159
|
+
if isinstance(response_schema, dict) and response_schema:
|
|
160
|
+
model_name = (
|
|
161
|
+
str(response_schema_name).strip()
|
|
162
|
+
if isinstance(response_schema_name, str) and response_schema_name.strip()
|
|
163
|
+
else "StructuredOutput"
|
|
164
|
+
)
|
|
165
|
+
params["response_model"] = _pydantic_model_from_json_schema(response_schema, name=model_name)
|
|
166
|
+
|
|
167
|
+
runtime_observability = {
|
|
168
|
+
"llm_generate_kwargs": _jsonable(
|
|
169
|
+
{
|
|
170
|
+
"prompt": str(prompt or ""),
|
|
171
|
+
"messages": messages,
|
|
172
|
+
"system_prompt": system_prompt,
|
|
173
|
+
"tools": tools,
|
|
174
|
+
"params": params,
|
|
175
|
+
}
|
|
176
|
+
),
|
|
177
|
+
}
|
|
178
|
+
|
|
58
179
|
result = llm.generate(
|
|
59
180
|
prompt=str(prompt or ""),
|
|
60
181
|
messages=messages,
|
|
@@ -62,6 +183,16 @@ def make_llm_call_handler(*, llm: AbstractCoreLLMClient) -> EffectHandler:
|
|
|
62
183
|
tools=tools,
|
|
63
184
|
params=params,
|
|
64
185
|
)
|
|
186
|
+
if isinstance(result, dict):
|
|
187
|
+
meta = result.get("metadata")
|
|
188
|
+
if not isinstance(meta, dict):
|
|
189
|
+
meta = {}
|
|
190
|
+
result["metadata"] = meta
|
|
191
|
+
existing = meta.get("_runtime_observability")
|
|
192
|
+
if not isinstance(existing, dict):
|
|
193
|
+
existing = {}
|
|
194
|
+
meta["_runtime_observability"] = existing
|
|
195
|
+
existing.update(runtime_observability)
|
|
65
196
|
return EffectOutcome.completed(result=result)
|
|
66
197
|
except Exception as e:
|
|
67
198
|
logger.error("LLM_CALL failed", error=str(e))
|
|
@@ -81,6 +212,11 @@ def make_tool_calls_handler(*, tools: Optional[ToolExecutor] = None) -> EffectHa
|
|
|
81
212
|
tool_calls = payload.get("tool_calls")
|
|
82
213
|
if not isinstance(tool_calls, list):
|
|
83
214
|
return EffectOutcome.failed("tool_calls requires payload.tool_calls (list)")
|
|
215
|
+
allowed_tools_raw = payload.get("allowed_tools")
|
|
216
|
+
allowlist_enabled = isinstance(allowed_tools_raw, list)
|
|
217
|
+
allowed_tools: Set[str] = set()
|
|
218
|
+
if allowlist_enabled:
|
|
219
|
+
allowed_tools = {str(t) for t in allowed_tools_raw if isinstance(t, str) and t.strip()}
|
|
84
220
|
|
|
85
221
|
if tools is None:
|
|
86
222
|
return EffectOutcome.failed(
|
|
@@ -88,8 +224,70 @@ def make_tool_calls_handler(*, tools: Optional[ToolExecutor] = None) -> EffectHa
|
|
|
88
224
|
"MappingToolExecutor/AbstractCoreToolExecutor/PassthroughToolExecutor."
|
|
89
225
|
)
|
|
90
226
|
|
|
227
|
+
original_call_count = len(tool_calls)
|
|
228
|
+
|
|
229
|
+
# Always block non-dict tool call entries: passthrough hosts expect dicts and may crash otherwise.
|
|
230
|
+
blocked_by_index: Dict[int, Dict[str, Any]] = {}
|
|
231
|
+
filtered_tool_calls: list[Dict[str, Any]] = []
|
|
232
|
+
|
|
233
|
+
# For evidence and deterministic resume merging, keep a positional tool call list aligned to the
|
|
234
|
+
# *original* tool call order. Blocked entries are represented as empty-args stubs.
|
|
235
|
+
tool_calls_for_evidence: list[Dict[str, Any]] = []
|
|
236
|
+
|
|
237
|
+
for idx, tc in enumerate(tool_calls):
|
|
238
|
+
if not isinstance(tc, dict):
|
|
239
|
+
blocked_by_index[idx] = {
|
|
240
|
+
"call_id": "",
|
|
241
|
+
"name": "",
|
|
242
|
+
"success": False,
|
|
243
|
+
"output": None,
|
|
244
|
+
"error": "Invalid tool call (expected an object)",
|
|
245
|
+
}
|
|
246
|
+
tool_calls_for_evidence.append({})
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
name_raw = tc.get("name")
|
|
250
|
+
name = name_raw.strip() if isinstance(name_raw, str) else ""
|
|
251
|
+
call_id = str(tc.get("call_id") or "")
|
|
252
|
+
|
|
253
|
+
if allowlist_enabled:
|
|
254
|
+
if not name:
|
|
255
|
+
blocked_by_index[idx] = {
|
|
256
|
+
"call_id": call_id,
|
|
257
|
+
"name": "",
|
|
258
|
+
"success": False,
|
|
259
|
+
"output": None,
|
|
260
|
+
"error": "Tool call missing a valid name",
|
|
261
|
+
}
|
|
262
|
+
tool_calls_for_evidence.append({"call_id": call_id, "name": "", "arguments": {}})
|
|
263
|
+
continue
|
|
264
|
+
if name not in allowed_tools:
|
|
265
|
+
blocked_by_index[idx] = {
|
|
266
|
+
"call_id": call_id,
|
|
267
|
+
"name": name,
|
|
268
|
+
"success": False,
|
|
269
|
+
"output": None,
|
|
270
|
+
"error": f"Tool '{name}' is not allowed for this node",
|
|
271
|
+
}
|
|
272
|
+
# Do not leak arguments for disallowed tools into the durable wait payload.
|
|
273
|
+
tool_calls_for_evidence.append({"call_id": call_id, "name": name, "arguments": {}})
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
# Allowed (or allowlist disabled): include for execution and keep full args for evidence.
|
|
277
|
+
filtered_tool_calls.append(tc)
|
|
278
|
+
tool_calls_for_evidence.append(tc)
|
|
279
|
+
|
|
280
|
+
# If everything was blocked, complete immediately with blocked results (no waiting/execution).
|
|
281
|
+
if not filtered_tool_calls and blocked_by_index:
|
|
282
|
+
return EffectOutcome.completed(
|
|
283
|
+
result={
|
|
284
|
+
"mode": "executed",
|
|
285
|
+
"results": [blocked_by_index[i] for i in sorted(blocked_by_index.keys())],
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
|
|
91
289
|
try:
|
|
92
|
-
result = tools.execute(tool_calls=
|
|
290
|
+
result = tools.execute(tool_calls=filtered_tool_calls)
|
|
93
291
|
except Exception as e:
|
|
94
292
|
logger.error("TOOL_CALLS execution failed", error=str(e))
|
|
95
293
|
return EffectOutcome.failed(str(e))
|
|
@@ -97,16 +295,67 @@ def make_tool_calls_handler(*, tools: Optional[ToolExecutor] = None) -> EffectHa
|
|
|
97
295
|
mode = result.get("mode")
|
|
98
296
|
if mode and mode != "executed":
|
|
99
297
|
# Passthrough/untrusted mode: pause until an external host resumes with tool results.
|
|
100
|
-
|
|
298
|
+
#
|
|
299
|
+
# Correctness/security: persist only allowlist-safe tool calls in the wait payload.
|
|
300
|
+
wait_key = payload.get("wait_key") or result.get("wait_key") or f"tool_calls:{run.run_id}:{run.current_node}"
|
|
301
|
+
raw_wait_reason = result.get("wait_reason")
|
|
302
|
+
wait_reason = WaitReason.EVENT
|
|
303
|
+
if isinstance(raw_wait_reason, str) and raw_wait_reason.strip():
|
|
304
|
+
try:
|
|
305
|
+
wait_reason = WaitReason(raw_wait_reason.strip())
|
|
306
|
+
except ValueError:
|
|
307
|
+
wait_reason = WaitReason.EVENT
|
|
308
|
+
elif str(mode).strip().lower() == "delegated":
|
|
309
|
+
wait_reason = WaitReason.JOB
|
|
310
|
+
|
|
311
|
+
tool_calls_for_wait = result.get("tool_calls")
|
|
312
|
+
if not isinstance(tool_calls_for_wait, list):
|
|
313
|
+
tool_calls_for_wait = filtered_tool_calls
|
|
314
|
+
|
|
315
|
+
details: Dict[str, Any] = {"mode": mode, "tool_calls": _jsonable(tool_calls_for_wait)}
|
|
316
|
+
executor_details = result.get("details")
|
|
317
|
+
if isinstance(executor_details, dict) and executor_details:
|
|
318
|
+
# Avoid collisions with our reserved keys.
|
|
319
|
+
details["executor"] = _jsonable(executor_details)
|
|
320
|
+
if blocked_by_index:
|
|
321
|
+
details["original_call_count"] = original_call_count
|
|
322
|
+
details["blocked_by_index"] = {str(k): _jsonable(v) for k, v in blocked_by_index.items()}
|
|
323
|
+
details["tool_calls_for_evidence"] = _jsonable(tool_calls_for_evidence)
|
|
324
|
+
|
|
101
325
|
wait = WaitState(
|
|
102
|
-
reason=
|
|
326
|
+
reason=wait_reason,
|
|
103
327
|
wait_key=str(wait_key),
|
|
104
328
|
resume_to_node=payload.get("resume_to_node") or default_next_node,
|
|
105
329
|
result_key=effect.result_key,
|
|
106
|
-
details=
|
|
330
|
+
details=details,
|
|
107
331
|
)
|
|
108
332
|
return EffectOutcome.waiting(wait)
|
|
109
333
|
|
|
334
|
+
if blocked_by_index:
|
|
335
|
+
existing_results = result.get("results")
|
|
336
|
+
if isinstance(existing_results, list):
|
|
337
|
+
merged_results: list[Any] = []
|
|
338
|
+
executed_iter = iter(existing_results)
|
|
339
|
+
for idx in range(len(tool_calls)):
|
|
340
|
+
blocked = blocked_by_index.get(idx)
|
|
341
|
+
if blocked is not None:
|
|
342
|
+
merged_results.append(blocked)
|
|
343
|
+
continue
|
|
344
|
+
try:
|
|
345
|
+
merged_results.append(next(executed_iter))
|
|
346
|
+
except StopIteration:
|
|
347
|
+
merged_results.append(
|
|
348
|
+
{
|
|
349
|
+
"call_id": "",
|
|
350
|
+
"name": "",
|
|
351
|
+
"success": False,
|
|
352
|
+
"output": None,
|
|
353
|
+
"error": "Missing tool result",
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
result = dict(result)
|
|
357
|
+
result["results"] = merged_results
|
|
358
|
+
|
|
110
359
|
return EffectOutcome.completed(result=result)
|
|
111
360
|
|
|
112
361
|
return _handler
|
|
@@ -20,10 +20,14 @@ from ...core.runtime import Runtime
|
|
|
20
20
|
from ...storage.in_memory import InMemoryLedgerStore, InMemoryRunStore
|
|
21
21
|
from ...storage.json_files import JsonFileRunStore, JsonlLedgerStore
|
|
22
22
|
from ...storage.base import LedgerStore, RunStore
|
|
23
|
+
from ...storage.artifacts import FileArtifactStore, InMemoryArtifactStore, ArtifactStore
|
|
24
|
+
from ...storage.observable import ObservableLedgerStore, ObservableLedgerStoreProtocol
|
|
23
25
|
|
|
24
26
|
from .effect_handlers import build_effect_handlers
|
|
25
|
-
from .llm_client import
|
|
27
|
+
from .llm_client import MultiLocalAbstractCoreLLMClient, RemoteAbstractCoreLLMClient
|
|
26
28
|
from .tool_executor import AbstractCoreToolExecutor, PassthroughToolExecutor, ToolExecutor
|
|
29
|
+
from .summarizer import AbstractCoreChatSummarizer
|
|
30
|
+
from .constants import DEFAULT_LLM_TIMEOUT_S, DEFAULT_TOOL_TIMEOUT_S
|
|
27
31
|
|
|
28
32
|
|
|
29
33
|
def _default_in_memory_stores() -> tuple[RunStore, LedgerStore]:
|
|
@@ -35,6 +39,18 @@ def _default_file_stores(*, base_dir: str | Path) -> tuple[RunStore, LedgerStore
|
|
|
35
39
|
base.mkdir(parents=True, exist_ok=True)
|
|
36
40
|
return JsonFileRunStore(base), JsonlLedgerStore(base)
|
|
37
41
|
|
|
42
|
+
def _ensure_observable_ledger(ledger_store: LedgerStore) -> LedgerStore:
|
|
43
|
+
"""Wrap a LedgerStore so Runtime.subscribe_ledger() is available (in-process).
|
|
44
|
+
|
|
45
|
+
Why:
|
|
46
|
+
- Real-time UI/UX often needs "step started" signals *before* a blocking effect
|
|
47
|
+
(LLM/tool HTTP) returns.
|
|
48
|
+
- The runtime kernel stays transport-agnostic; this is an optional decorator.
|
|
49
|
+
"""
|
|
50
|
+
if isinstance(ledger_store, ObservableLedgerStoreProtocol):
|
|
51
|
+
return ledger_store
|
|
52
|
+
return ObservableLedgerStore(ledger_store)
|
|
53
|
+
|
|
38
54
|
|
|
39
55
|
def create_local_runtime(
|
|
40
56
|
*,
|
|
@@ -44,9 +60,11 @@ def create_local_runtime(
|
|
|
44
60
|
run_store: Optional[RunStore] = None,
|
|
45
61
|
ledger_store: Optional[LedgerStore] = None,
|
|
46
62
|
tool_executor: Optional[ToolExecutor] = None,
|
|
63
|
+
tool_timeout_s: float = DEFAULT_TOOL_TIMEOUT_S,
|
|
47
64
|
context: Optional[Any] = None,
|
|
48
65
|
effect_policy: Optional[Any] = None,
|
|
49
66
|
config: Optional[RuntimeConfig] = None,
|
|
67
|
+
artifact_store: Optional[ArtifactStore] = None,
|
|
50
68
|
) -> Runtime:
|
|
51
69
|
"""Create a runtime with local LLM execution via AbstractCore.
|
|
52
70
|
|
|
@@ -69,19 +87,49 @@ def create_local_runtime(
|
|
|
69
87
|
"""
|
|
70
88
|
if run_store is None or ledger_store is None:
|
|
71
89
|
run_store, ledger_store = _default_in_memory_stores()
|
|
90
|
+
ledger_store = _ensure_observable_ledger(ledger_store)
|
|
72
91
|
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
if artifact_store is None:
|
|
93
|
+
artifact_store = InMemoryArtifactStore()
|
|
94
|
+
|
|
95
|
+
# Runtime authority: default LLM timeout for orchestrated workflows.
|
|
96
|
+
#
|
|
97
|
+
# We set this here (in the runtime layer) rather than relying on AbstractCore global config,
|
|
98
|
+
# so workflow behavior is consistent and controlled by the orchestrator.
|
|
99
|
+
effective_llm_kwargs: Dict[str, Any] = dict(llm_kwargs or {})
|
|
100
|
+
effective_llm_kwargs.setdefault("timeout", DEFAULT_LLM_TIMEOUT_S)
|
|
101
|
+
|
|
102
|
+
llm_client = MultiLocalAbstractCoreLLMClient(provider=provider, model=model, llm_kwargs=effective_llm_kwargs)
|
|
103
|
+
tools = tool_executor or AbstractCoreToolExecutor(timeout_s=tool_timeout_s)
|
|
104
|
+
# Orchestrator policy: enforce tool execution timeout at the runtime layer.
|
|
105
|
+
try:
|
|
106
|
+
setter = getattr(tools, "set_timeout_s", None)
|
|
107
|
+
if callable(setter):
|
|
108
|
+
setter(tool_timeout_s)
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
75
111
|
handlers = build_effect_handlers(llm=llm_client, tools=tools)
|
|
76
112
|
|
|
77
113
|
# Query model capabilities and merge into config
|
|
78
114
|
capabilities = llm_client.get_model_capabilities()
|
|
79
115
|
if config is None:
|
|
80
|
-
config = RuntimeConfig(
|
|
116
|
+
config = RuntimeConfig(
|
|
117
|
+
provider=str(provider).strip() if isinstance(provider, str) and str(provider).strip() else None,
|
|
118
|
+
model=str(model).strip() if isinstance(model, str) and str(model).strip() else None,
|
|
119
|
+
model_capabilities=capabilities,
|
|
120
|
+
)
|
|
81
121
|
else:
|
|
82
122
|
# Merge capabilities into provided config
|
|
83
123
|
config = config.with_capabilities(capabilities)
|
|
84
124
|
|
|
125
|
+
# Create chat summarizer with token limits from config
|
|
126
|
+
# This enables adaptive chunking during MEMORY_COMPACT
|
|
127
|
+
summarizer = AbstractCoreChatSummarizer(
|
|
128
|
+
llm=llm_client._llm, # Use the underlying AbstractCore LLM instance
|
|
129
|
+
max_tokens=config.max_tokens if config.max_tokens is not None else -1,
|
|
130
|
+
max_output_tokens=config.max_output_tokens if config.max_output_tokens is not None else -1,
|
|
131
|
+
)
|
|
132
|
+
|
|
85
133
|
return Runtime(
|
|
86
134
|
run_store=run_store,
|
|
87
135
|
ledger_store=ledger_store,
|
|
@@ -89,6 +137,8 @@ def create_local_runtime(
|
|
|
89
137
|
context=context,
|
|
90
138
|
effect_policy=effect_policy,
|
|
91
139
|
config=config,
|
|
140
|
+
artifact_store=artifact_store,
|
|
141
|
+
chat_summarizer=summarizer,
|
|
92
142
|
)
|
|
93
143
|
|
|
94
144
|
|
|
@@ -97,14 +147,19 @@ def create_remote_runtime(
|
|
|
97
147
|
server_base_url: str,
|
|
98
148
|
model: str,
|
|
99
149
|
headers: Optional[Dict[str, str]] = None,
|
|
100
|
-
timeout_s: float =
|
|
150
|
+
timeout_s: float = DEFAULT_LLM_TIMEOUT_S,
|
|
101
151
|
run_store: Optional[RunStore] = None,
|
|
102
152
|
ledger_store: Optional[LedgerStore] = None,
|
|
103
153
|
tool_executor: Optional[ToolExecutor] = None,
|
|
104
154
|
context: Optional[Any] = None,
|
|
155
|
+
artifact_store: Optional[ArtifactStore] = None,
|
|
105
156
|
) -> Runtime:
|
|
106
157
|
if run_store is None or ledger_store is None:
|
|
107
158
|
run_store, ledger_store = _default_in_memory_stores()
|
|
159
|
+
ledger_store = _ensure_observable_ledger(ledger_store)
|
|
160
|
+
|
|
161
|
+
if artifact_store is None:
|
|
162
|
+
artifact_store = InMemoryArtifactStore()
|
|
108
163
|
|
|
109
164
|
llm_client = RemoteAbstractCoreLLMClient(
|
|
110
165
|
server_base_url=server_base_url,
|
|
@@ -115,7 +170,13 @@ def create_remote_runtime(
|
|
|
115
170
|
tools = tool_executor or PassthroughToolExecutor()
|
|
116
171
|
handlers = build_effect_handlers(llm=llm_client, tools=tools)
|
|
117
172
|
|
|
118
|
-
return Runtime(
|
|
173
|
+
return Runtime(
|
|
174
|
+
run_store=run_store,
|
|
175
|
+
ledger_store=ledger_store,
|
|
176
|
+
effect_handlers=handlers,
|
|
177
|
+
context=context,
|
|
178
|
+
artifact_store=artifact_store,
|
|
179
|
+
)
|
|
119
180
|
|
|
120
181
|
|
|
121
182
|
def create_hybrid_runtime(
|
|
@@ -123,15 +184,21 @@ def create_hybrid_runtime(
|
|
|
123
184
|
server_base_url: str,
|
|
124
185
|
model: str,
|
|
125
186
|
headers: Optional[Dict[str, str]] = None,
|
|
126
|
-
timeout_s: float =
|
|
187
|
+
timeout_s: float = DEFAULT_LLM_TIMEOUT_S,
|
|
188
|
+
tool_timeout_s: float = DEFAULT_TOOL_TIMEOUT_S,
|
|
127
189
|
run_store: Optional[RunStore] = None,
|
|
128
190
|
ledger_store: Optional[LedgerStore] = None,
|
|
129
191
|
context: Optional[Any] = None,
|
|
192
|
+
artifact_store: Optional[ArtifactStore] = None,
|
|
130
193
|
) -> Runtime:
|
|
131
194
|
"""Remote LLM via AbstractCore server, local tool execution."""
|
|
132
195
|
|
|
133
196
|
if run_store is None or ledger_store is None:
|
|
134
197
|
run_store, ledger_store = _default_in_memory_stores()
|
|
198
|
+
ledger_store = _ensure_observable_ledger(ledger_store)
|
|
199
|
+
|
|
200
|
+
if artifact_store is None:
|
|
201
|
+
artifact_store = InMemoryArtifactStore()
|
|
135
202
|
|
|
136
203
|
llm_client = RemoteAbstractCoreLLMClient(
|
|
137
204
|
server_base_url=server_base_url,
|
|
@@ -139,10 +206,22 @@ def create_hybrid_runtime(
|
|
|
139
206
|
headers=headers,
|
|
140
207
|
timeout_s=timeout_s,
|
|
141
208
|
)
|
|
142
|
-
tools = AbstractCoreToolExecutor()
|
|
209
|
+
tools = AbstractCoreToolExecutor(timeout_s=tool_timeout_s)
|
|
210
|
+
try:
|
|
211
|
+
setter = getattr(tools, "set_timeout_s", None)
|
|
212
|
+
if callable(setter):
|
|
213
|
+
setter(tool_timeout_s)
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
143
216
|
handlers = build_effect_handlers(llm=llm_client, tools=tools)
|
|
144
217
|
|
|
145
|
-
return Runtime(
|
|
218
|
+
return Runtime(
|
|
219
|
+
run_store=run_store,
|
|
220
|
+
ledger_store=ledger_store,
|
|
221
|
+
effect_handlers=handlers,
|
|
222
|
+
context=context,
|
|
223
|
+
artifact_store=artifact_store,
|
|
224
|
+
)
|
|
146
225
|
|
|
147
226
|
|
|
148
227
|
def create_local_file_runtime(
|
|
@@ -153,8 +232,10 @@ def create_local_file_runtime(
|
|
|
153
232
|
llm_kwargs: Optional[Dict[str, Any]] = None,
|
|
154
233
|
context: Optional[Any] = None,
|
|
155
234
|
config: Optional[RuntimeConfig] = None,
|
|
235
|
+
tool_timeout_s: float = DEFAULT_TOOL_TIMEOUT_S,
|
|
156
236
|
) -> Runtime:
|
|
157
237
|
run_store, ledger_store = _default_file_stores(base_dir=base_dir)
|
|
238
|
+
artifact_store = FileArtifactStore(base_dir)
|
|
158
239
|
return create_local_runtime(
|
|
159
240
|
provider=provider,
|
|
160
241
|
model=model,
|
|
@@ -163,6 +244,8 @@ def create_local_file_runtime(
|
|
|
163
244
|
ledger_store=ledger_store,
|
|
164
245
|
context=context,
|
|
165
246
|
config=config,
|
|
247
|
+
artifact_store=artifact_store,
|
|
248
|
+
tool_timeout_s=tool_timeout_s,
|
|
166
249
|
)
|
|
167
250
|
|
|
168
251
|
|
|
@@ -172,10 +255,11 @@ def create_remote_file_runtime(
|
|
|
172
255
|
server_base_url: str,
|
|
173
256
|
model: str,
|
|
174
257
|
headers: Optional[Dict[str, str]] = None,
|
|
175
|
-
timeout_s: float =
|
|
258
|
+
timeout_s: float = DEFAULT_LLM_TIMEOUT_S,
|
|
176
259
|
context: Optional[Any] = None,
|
|
177
260
|
) -> Runtime:
|
|
178
261
|
run_store, ledger_store = _default_file_stores(base_dir=base_dir)
|
|
262
|
+
artifact_store = FileArtifactStore(base_dir)
|
|
179
263
|
return create_remote_runtime(
|
|
180
264
|
server_base_url=server_base_url,
|
|
181
265
|
model=model,
|
|
@@ -184,4 +268,5 @@ def create_remote_file_runtime(
|
|
|
184
268
|
run_store=run_store,
|
|
185
269
|
ledger_store=ledger_store,
|
|
186
270
|
context=context,
|
|
271
|
+
artifact_store=artifact_store,
|
|
187
272
|
)
|