ltcai 2.2.2 → 3.0.1
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.
- package/README.md +66 -27
- package/codex_telegram_bot.py +6 -2
- package/docs/CHANGELOG.md +154 -0
- package/docs/V3_BACKEND_ARCHITECTURE.md +138 -0
- package/docs/V3_FRONTEND.md +136 -0
- package/knowledge_graph.py +649 -21
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +47 -0
- package/latticeai/api/agents.py +54 -31
- package/latticeai/api/auth.py +1 -1
- package/latticeai/api/chat.py +10 -2
- package/latticeai/api/search.py +236 -0
- package/latticeai/api/static_routes.py +21 -2
- package/latticeai/core/config.py +16 -0
- package/latticeai/core/embedding_providers.py +502 -0
- package/latticeai/core/local_embeddings.py +86 -0
- package/latticeai/core/logging_safety.py +62 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/server_app.py +49 -1
- package/latticeai/services/agent_runtime.py +245 -0
- package/latticeai/services/search_service.py +346 -0
- package/package.json +8 -4
- package/static/account.html +9 -4
- package/static/activity.html +4 -4
- package/static/admin.html +8 -3
- package/static/agents.html +4 -4
- package/static/chat.html +16 -11
- package/static/css/reference/account.css +439 -0
- package/static/css/reference/admin.css +610 -0
- package/static/css/reference/base.css +1658 -0
- package/static/{lattice-reference.css → css/reference/chat.css} +271 -3633
- package/static/css/reference/graph.css +1016 -0
- package/static/css/responsive.css +248 -1
- package/static/css/tokens.css +132 -126
- package/static/favicon.ico +0 -0
- package/static/graph.html +9 -4
- package/static/manifest.json +3 -3
- package/static/platform.css +1 -1
- package/static/plugins.html +4 -4
- package/static/scripts/account.js +4 -4
- package/static/scripts/chat.js +227 -77
- package/static/scripts/workspace.js +78 -0
- package/static/sw.js +5 -3
- package/static/v3/css/lattice.base.css +128 -0
- package/static/v3/css/lattice.components.css +447 -0
- package/static/v3/css/lattice.shell.css +407 -0
- package/static/v3/css/lattice.tokens.css +132 -0
- package/static/v3/css/lattice.views.css +277 -0
- package/static/v3/index.html +40 -0
- package/static/v3/js/app.js +26 -0
- package/static/v3/js/core/api.js +327 -0
- package/static/v3/js/core/components.js +215 -0
- package/static/v3/js/core/dom.js +148 -0
- package/static/v3/js/core/fixtures.js +171 -0
- package/static/v3/js/core/router.js +37 -0
- package/static/v3/js/core/routes.js +73 -0
- package/static/v3/js/core/shell.js +363 -0
- package/static/v3/js/core/store.js +113 -0
- package/static/v3/js/views/admin-audit.js +185 -0
- package/static/v3/js/views/admin-permissions.js +178 -0
- package/static/v3/js/views/admin-policies.js +103 -0
- package/static/v3/js/views/admin-private-vpc.js +138 -0
- package/static/v3/js/views/admin-security.js +181 -0
- package/static/v3/js/views/admin-users.js +168 -0
- package/static/v3/js/views/agents.js +194 -0
- package/static/v3/js/views/chat.js +450 -0
- package/static/v3/js/views/files.js +180 -0
- package/static/v3/js/views/home.js +119 -0
- package/static/v3/js/views/hybrid-search.js +195 -0
- package/static/v3/js/views/knowledge-graph.js +238 -0
- package/static/v3/js/views/models.js +247 -0
- package/static/v3/js/views/my-computer.js +237 -0
- package/static/v3/js/views/pipeline.js +161 -0
- package/static/v3/js/views/settings.js +258 -0
- package/static/workflows.html +4 -4
- package/static/workspace.css +408 -14
- package/static/workspace.html +43 -24
- package/telegram_bot.py +18 -14
package/latticeai/server_app.py
CHANGED
|
@@ -72,6 +72,9 @@ from latticeai.core.enterprise import (
|
|
|
72
72
|
from latticeai.services.workspace_service import WorkspaceService
|
|
73
73
|
from latticeai.services.model_service import ModelService
|
|
74
74
|
from latticeai.services.chat_service import ChatService
|
|
75
|
+
from latticeai.services.search_service import SearchService
|
|
76
|
+
from latticeai.core.embedding_providers import resolve_embedder
|
|
77
|
+
from latticeai.services.agent_runtime import AgentRuntime
|
|
75
78
|
from latticeai.services.model_runtime import (
|
|
76
79
|
CLOUD_VERIFY_TTL_SECONDS,
|
|
77
80
|
ENGINE_MODEL_CATALOG,
|
|
@@ -105,6 +108,7 @@ from latticeai.api.realtime import create_realtime_router
|
|
|
105
108
|
from latticeai.api.marketplace import create_marketplace_router
|
|
106
109
|
from latticeai.api.models import create_models_router
|
|
107
110
|
from latticeai.api.chat import create_chat_router
|
|
111
|
+
from latticeai.api.search import create_search_router
|
|
108
112
|
from latticeai.api.tools import create_tools_router
|
|
109
113
|
from latticeai.api.static_routes import create_static_routes_router
|
|
110
114
|
from latticeai.api.garden import create_garden_router
|
|
@@ -244,7 +248,26 @@ VPC_FILE = DATA_DIR / "vpc_config.json"
|
|
|
244
248
|
MCP_FILE = DATA_DIR / "mcp_installs.json"
|
|
245
249
|
AUDIT_FILE = DATA_DIR / "audit_log.json"
|
|
246
250
|
SSO_FILE = DATA_DIR / "sso_config.json"
|
|
247
|
-
|
|
251
|
+
# Resolve the configured embedding provider once at startup. Degrades to the
|
|
252
|
+
# offline hash fallback when the requested provider is unavailable, while
|
|
253
|
+
# recording the requested-vs-active provider for the Embeddings status surface.
|
|
254
|
+
EMBEDDER = resolve_embedder(
|
|
255
|
+
CONFIG.embedding_provider,
|
|
256
|
+
model=CONFIG.embedding_model,
|
|
257
|
+
base_url=CONFIG.embedding_base_url,
|
|
258
|
+
api_key=CONFIG.embedding_api_key,
|
|
259
|
+
dim=CONFIG.embedding_dim,
|
|
260
|
+
timeout=CONFIG.embedding_timeout,
|
|
261
|
+
extra={"target": CONFIG.embedding_custom_target},
|
|
262
|
+
probe=CONFIG.embedding_provider not in {"", "hash", "local", "fallback"},
|
|
263
|
+
)
|
|
264
|
+
if EMBEDDER.fell_back:
|
|
265
|
+
logging.warning("Embedding provider %s unavailable: %s", EMBEDDER.requested, EMBEDDER.detail)
|
|
266
|
+
KNOWLEDGE_GRAPH = KnowledgeGraphStore(
|
|
267
|
+
DATA_DIR / "knowledge_graph.sqlite",
|
|
268
|
+
DATA_DIR / "knowledge_graph_blobs",
|
|
269
|
+
embedder=EMBEDDER.provider,
|
|
270
|
+
) if ENABLE_GRAPH else None
|
|
248
271
|
LOCAL_KG_WATCHER = LocalKnowledgeWatcher(lambda: KNOWLEDGE_GRAPH) if ENABLE_GRAPH else None
|
|
249
272
|
# ── v2 Realtime bus: constructed first so the store can fan every timeline
|
|
250
273
|
# event into the realtime feed via a single additive sink (no per-call wiring).
|
|
@@ -1171,6 +1194,9 @@ def _workspace_graph():
|
|
|
1171
1194
|
return KNOWLEDGE_GRAPH if (ENABLE_GRAPH and KNOWLEDGE_GRAPH) else None
|
|
1172
1195
|
|
|
1173
1196
|
|
|
1197
|
+
SEARCH_SERVICE = SearchService(graph_store=_workspace_graph())
|
|
1198
|
+
|
|
1199
|
+
|
|
1174
1200
|
# ── Workspace OS + Organization router (latticeai.api.workspace, v1.2.0) ──────
|
|
1175
1201
|
app.include_router(create_workspace_router(
|
|
1176
1202
|
service=WORKSPACE_SERVICE,
|
|
@@ -1217,6 +1243,14 @@ PLATFORM = PlatformRuntime(
|
|
|
1217
1243
|
get_tool_permission=get_tool_permission,
|
|
1218
1244
|
)
|
|
1219
1245
|
|
|
1246
|
+
# Single AgentRuntime boundary over the orchestrator + run store.
|
|
1247
|
+
AGENT_RUNTIME = AgentRuntime(
|
|
1248
|
+
store=WORKSPACE_OS,
|
|
1249
|
+
orchestrator_factory=PLATFORM.build_orchestrator,
|
|
1250
|
+
workspace_graph=_workspace_graph,
|
|
1251
|
+
append_audit_event=append_audit_event,
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1220
1254
|
app.include_router(create_plugins_router(
|
|
1221
1255
|
registry=PLUGIN_REGISTRY,
|
|
1222
1256
|
require_user=require_user,
|
|
@@ -1252,6 +1286,7 @@ app.include_router(create_agents_router(
|
|
|
1252
1286
|
append_audit_event=append_audit_event,
|
|
1253
1287
|
ui_file_response=ui_file_response,
|
|
1254
1288
|
static_dir=STATIC_DIR,
|
|
1289
|
+
agent_runtime=AGENT_RUNTIME,
|
|
1255
1290
|
))
|
|
1256
1291
|
|
|
1257
1292
|
app.include_router(create_marketplace_router(
|
|
@@ -1351,6 +1386,19 @@ app.include_router(create_chat_router(
|
|
|
1351
1386
|
base_dir=BASE_DIR,
|
|
1352
1387
|
))
|
|
1353
1388
|
|
|
1389
|
+
def _embedding_info() -> dict:
|
|
1390
|
+
from latticeai.core.embedding_providers import PROVIDER_TYPES
|
|
1391
|
+
info = EMBEDDER.as_dict()
|
|
1392
|
+
info["available_providers"] = list(PROVIDER_TYPES)
|
|
1393
|
+
return info
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
app.include_router(create_search_router(
|
|
1397
|
+
service=SEARCH_SERVICE,
|
|
1398
|
+
require_user=require_user,
|
|
1399
|
+
embedding_info=_embedding_info,
|
|
1400
|
+
))
|
|
1401
|
+
|
|
1354
1402
|
app.include_router(create_tools_router(
|
|
1355
1403
|
config=CONFIG,
|
|
1356
1404
|
data_dir=DATA_DIR,
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""AgentRuntime — the single boundary for agent execution and observability.
|
|
2
|
+
|
|
3
|
+
Before this module the agent concern was spread across three places: the
|
|
4
|
+
:class:`~latticeai.core.multi_agent.MultiAgentOrchestrator` (role pipeline),
|
|
5
|
+
the :class:`~latticeai.services.platform_runtime.PlatformRuntime` (cross-system
|
|
6
|
+
wiring + an ad-hoc ``run_agent``), and ``api/agents.py`` (HTTP transport that
|
|
7
|
+
also owned orchestration + persistence + audit). The frontend reached past all
|
|
8
|
+
of them into the workspace store via ``/workspace/agents``.
|
|
9
|
+
|
|
10
|
+
``AgentRuntime`` collapses that into one façade with a small, stable surface:
|
|
11
|
+
|
|
12
|
+
* **configuration** — :meth:`config`, :meth:`roles`
|
|
13
|
+
* **status / health** — :meth:`status`, :meth:`health`
|
|
14
|
+
* **execution** — :meth:`start`, :meth:`stop`
|
|
15
|
+
* **events / state** — :meth:`list_runs`, :meth:`get_run`, :meth:`events`, :meth:`replay`
|
|
16
|
+
|
|
17
|
+
It *wraps* the existing orchestrator and run store rather than reimplementing
|
|
18
|
+
them — execution semantics are unchanged, but every caller (HTTP router and, via
|
|
19
|
+
it, the frontend) now depends on this boundary instead of internal paths.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
25
|
+
|
|
26
|
+
from latticeai.core.multi_agent import (
|
|
27
|
+
AGENT_ROLES,
|
|
28
|
+
CORE_PIPELINE,
|
|
29
|
+
MULTI_AGENT_VERSION,
|
|
30
|
+
ROLE_AGENT_IDS,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
ROLE_DESCRIPTIONS = {
|
|
34
|
+
"researcher": "Gathers workspace context and memory for the goal.",
|
|
35
|
+
"planner": "Decomposes the goal into an ordered, bounded plan.",
|
|
36
|
+
"executor": "Executes each planned step, invoking tools and workflows.",
|
|
37
|
+
"reviewer": "Reviews the executed work and approves, rejects, or retries.",
|
|
38
|
+
"release": "Finalizes and summarizes the approved outcome.",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Run statuses the orchestrator can emit that mean "still working". The default
|
|
42
|
+
# orchestrator runs synchronously, so persisted runs are always terminal; this
|
|
43
|
+
# set lets the runtime report live work if a future async runner lands.
|
|
44
|
+
_ACTIVE_STATUSES = {"running", "in_progress", "queued", "retrying"}
|
|
45
|
+
_TERMINAL_STATUSES = {"ok", "retried_ok", "failed", "rejected", "cancelled"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AgentRuntime:
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
store: Any,
|
|
53
|
+
orchestrator_factory: Callable[[Optional[str], Optional[str]], Any],
|
|
54
|
+
workspace_graph: Callable[[], Any],
|
|
55
|
+
append_audit_event: Callable[..., None],
|
|
56
|
+
max_retries_cap: int = 5,
|
|
57
|
+
):
|
|
58
|
+
self._store = store
|
|
59
|
+
self._orchestrator_factory = orchestrator_factory
|
|
60
|
+
self._workspace_graph = workspace_graph
|
|
61
|
+
self._append_audit_event = append_audit_event
|
|
62
|
+
self._max_retries_cap = int(max_retries_cap)
|
|
63
|
+
|
|
64
|
+
# ── configuration ─────────────────────────────────────────────────────
|
|
65
|
+
def config(self) -> Dict[str, Any]:
|
|
66
|
+
return {
|
|
67
|
+
"version": MULTI_AGENT_VERSION,
|
|
68
|
+
"roles": list(AGENT_ROLES),
|
|
69
|
+
"default_pipeline": list(CORE_PIPELINE),
|
|
70
|
+
"max_retries_cap": self._max_retries_cap,
|
|
71
|
+
"execution_mode": "synchronous",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def roles(self) -> List[Dict[str, Any]]:
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
"role": role,
|
|
78
|
+
"agent_id": ROLE_AGENT_IDS.get(role, f"agent:{role}"),
|
|
79
|
+
"description": ROLE_DESCRIPTIONS.get(role, ""),
|
|
80
|
+
"terminal": role not in {"researcher", "planner", "executor", "reviewer"},
|
|
81
|
+
}
|
|
82
|
+
for role in AGENT_ROLES
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
# ── health ────────────────────────────────────────────────────────────
|
|
86
|
+
def health(self) -> Dict[str, Any]:
|
|
87
|
+
checks: Dict[str, Any] = {}
|
|
88
|
+
ok = True
|
|
89
|
+
try:
|
|
90
|
+
self._store.list_agents(workspace_id=None)
|
|
91
|
+
checks["run_store"] = {"status": "ok"}
|
|
92
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
93
|
+
ok = False
|
|
94
|
+
checks["run_store"] = {"status": "error", "detail": str(exc)}
|
|
95
|
+
try:
|
|
96
|
+
self._orchestrator_factory(None, None)
|
|
97
|
+
checks["orchestrator"] = {"status": "ok"}
|
|
98
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
99
|
+
ok = False
|
|
100
|
+
checks["orchestrator"] = {"status": "error", "detail": str(exc)}
|
|
101
|
+
return {"status": "ok" if ok else "degraded", "checks": checks}
|
|
102
|
+
|
|
103
|
+
# ── roster + status ───────────────────────────────────────────────────
|
|
104
|
+
def _roster(self, runs: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
105
|
+
"""Canonical role roster enriched with real run statistics."""
|
|
106
|
+
by_agent: Dict[str, Dict[str, Any]] = {}
|
|
107
|
+
for run in runs:
|
|
108
|
+
aid = str(run.get("agent_id") or "")
|
|
109
|
+
entry = by_agent.setdefault(aid, {"runs": 0, "last_status": None, "last_at": None})
|
|
110
|
+
entry["runs"] += 1
|
|
111
|
+
if entry["last_at"] is None: # runs are newest-first
|
|
112
|
+
entry["last_status"] = run.get("status")
|
|
113
|
+
entry["last_at"] = run.get("created_at") or run.get("completed_at")
|
|
114
|
+
|
|
115
|
+
roster: List[Dict[str, Any]] = []
|
|
116
|
+
order = list(CORE_PIPELINE) # planner, executor, reviewer first
|
|
117
|
+
ordered_roles = order + [r for r in AGENT_ROLES if r not in order]
|
|
118
|
+
for role in ordered_roles:
|
|
119
|
+
agent_id = ROLE_AGENT_IDS.get(role, f"agent:{role}")
|
|
120
|
+
stats = by_agent.get(agent_id, {"runs": 0, "last_status": None, "last_at": None})
|
|
121
|
+
handoffs = []
|
|
122
|
+
if role == "planner":
|
|
123
|
+
handoffs = [ROLE_AGENT_IDS["executor"]]
|
|
124
|
+
elif role == "executor":
|
|
125
|
+
handoffs = [ROLE_AGENT_IDS["reviewer"]]
|
|
126
|
+
roster.append({
|
|
127
|
+
"id": agent_id,
|
|
128
|
+
"name": role.capitalize(),
|
|
129
|
+
"role": ROLE_DESCRIPTIONS.get(role, ""),
|
|
130
|
+
"state": "available" if role != "release" else "idle",
|
|
131
|
+
"runs": stats["runs"],
|
|
132
|
+
"last_status": stats["last_status"],
|
|
133
|
+
"last_at": stats["last_at"],
|
|
134
|
+
"handoffs": handoffs,
|
|
135
|
+
})
|
|
136
|
+
return roster
|
|
137
|
+
|
|
138
|
+
def status(self, *, scope: Optional[str] = None) -> Dict[str, Any]:
|
|
139
|
+
try:
|
|
140
|
+
listing = self._store.list_agents(workspace_id=scope)
|
|
141
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
142
|
+
listing = {"agents": [], "runs": [], "error": str(exc)}
|
|
143
|
+
runs = list(listing.get("runs") or [])
|
|
144
|
+
active = sum(1 for r in runs if str(r.get("status")) in _ACTIVE_STATUSES)
|
|
145
|
+
return {
|
|
146
|
+
"runtime": {
|
|
147
|
+
"ready": True,
|
|
148
|
+
"version": MULTI_AGENT_VERSION,
|
|
149
|
+
"execution_mode": "synchronous",
|
|
150
|
+
"default_pipeline": list(CORE_PIPELINE),
|
|
151
|
+
"total_runs": len(runs),
|
|
152
|
+
"active_runs": active,
|
|
153
|
+
},
|
|
154
|
+
"health": self.health(),
|
|
155
|
+
"roles": self.roles(),
|
|
156
|
+
"agents": self._roster(runs),
|
|
157
|
+
"runs": runs[:25],
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# ── events / state ────────────────────────────────────────────────────
|
|
161
|
+
def list_runs(self, *, scope: Optional[str] = None) -> Dict[str, Any]:
|
|
162
|
+
return self._store.list_agents(workspace_id=scope)
|
|
163
|
+
|
|
164
|
+
def get_run(self, run_id: str, *, scope: Optional[str] = None) -> Dict[str, Any]:
|
|
165
|
+
return {"run": self._store.get_agent_run(run_id, workspace_id=scope)}
|
|
166
|
+
|
|
167
|
+
def replay(self, run_id: str, *, scope: Optional[str] = None) -> Dict[str, Any]:
|
|
168
|
+
return {"replay": self._store.replay_agent_run(run_id, workspace_id=scope)}
|
|
169
|
+
|
|
170
|
+
def events(self, run_id: str, *, scope: Optional[str] = None) -> Dict[str, Any]:
|
|
171
|
+
run = self._store.get_agent_run(run_id, workspace_id=scope)
|
|
172
|
+
status = str(run.get("status") or "")
|
|
173
|
+
return {
|
|
174
|
+
"run_id": run_id,
|
|
175
|
+
"status": status,
|
|
176
|
+
"is_final": status in _TERMINAL_STATUSES or status not in _ACTIVE_STATUSES,
|
|
177
|
+
"current_role": run.get("current_role"),
|
|
178
|
+
"timeline": run.get("timeline") or [],
|
|
179
|
+
"handoffs": run.get("handoffs") or [],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# ── execution ─────────────────────────────────────────────────────────
|
|
183
|
+
def start(
|
|
184
|
+
self,
|
|
185
|
+
goal: str,
|
|
186
|
+
*,
|
|
187
|
+
user_email: Optional[str],
|
|
188
|
+
scope: Optional[str],
|
|
189
|
+
roles: Optional[List[str]] = None,
|
|
190
|
+
inputs: Optional[Dict[str, Any]] = None,
|
|
191
|
+
max_retries: int = 2,
|
|
192
|
+
) -> Dict[str, Any]:
|
|
193
|
+
if not str(goal or "").strip():
|
|
194
|
+
raise ValueError("goal is required")
|
|
195
|
+
orchestrator = self._orchestrator_factory(user_email or None, scope)
|
|
196
|
+
result = orchestrator.run(
|
|
197
|
+
goal,
|
|
198
|
+
user_email=user_email or None,
|
|
199
|
+
workspace_id=scope,
|
|
200
|
+
inputs=inputs or {},
|
|
201
|
+
roles=roles or None,
|
|
202
|
+
max_retries=max(0, min(int(max_retries or 0), self._max_retries_cap)),
|
|
203
|
+
)
|
|
204
|
+
run = self._store.record_agent_run(
|
|
205
|
+
agent_id=result.agent_id,
|
|
206
|
+
status=result.status,
|
|
207
|
+
input_text=goal,
|
|
208
|
+
output_text=result.output,
|
|
209
|
+
timeline=result.timeline,
|
|
210
|
+
relationships=[ROLE_AGENT_IDS.get(r, f"agent:{r}") for r in result.roles_run],
|
|
211
|
+
handoffs=result.handoffs,
|
|
212
|
+
context_packets=result.context_packets,
|
|
213
|
+
plan=result.plan,
|
|
214
|
+
plan_review=result.plan_review,
|
|
215
|
+
review_history=result.review_history,
|
|
216
|
+
retry_history=result.retry_history,
|
|
217
|
+
memory_snapshots=result.memory_snapshots,
|
|
218
|
+
user_email=user_email or None,
|
|
219
|
+
graph=self._workspace_graph(),
|
|
220
|
+
workspace_id=scope,
|
|
221
|
+
)
|
|
222
|
+
self._append_audit_event(
|
|
223
|
+
"multi_agent_run",
|
|
224
|
+
user_email=user_email,
|
|
225
|
+
agent_id=result.agent_id,
|
|
226
|
+
status=result.status,
|
|
227
|
+
retries=result.retries,
|
|
228
|
+
)
|
|
229
|
+
return {"run": run, "result": result.as_dict()}
|
|
230
|
+
|
|
231
|
+
def stop(self, run_id: str, *, scope: Optional[str] = None) -> Dict[str, Any]:
|
|
232
|
+
"""Best-effort stop.
|
|
233
|
+
|
|
234
|
+
The default runtime executes synchronously, so by the time a run id
|
|
235
|
+
exists the run has already completed. Report that honestly rather than
|
|
236
|
+
pretending a cancellation occurred.
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
run = self._store.get_agent_run(run_id, workspace_id=scope)
|
|
240
|
+
except FileNotFoundError:
|
|
241
|
+
return {"stopped": False, "reason": "run not found", "run_id": run_id}
|
|
242
|
+
status = str(run.get("status") or "")
|
|
243
|
+
if status in _ACTIVE_STATUSES:
|
|
244
|
+
return {"stopped": False, "reason": "asynchronous cancellation is not supported by the synchronous runtime", "run_id": run_id, "status": status}
|
|
245
|
+
return {"stopped": False, "reason": "run already finished", "run_id": run_id, "status": status}
|