ltcai 3.6.0 → 4.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.
Files changed (238) hide show
  1. package/README.md +39 -31
  2. package/docs/CHANGELOG.md +64 -0
  3. package/docs/REALTIME_COLLABORATION.md +3 -3
  4. package/docs/V3_FRONTEND.md +9 -8
  5. package/docs/V4_BRAIN_ARCHITECTURE.md +322 -0
  6. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +552 -0
  7. package/docs/V4_IMPLEMENTATION_PLAN.md +470 -0
  8. package/docs/kg-schema.md +51 -53
  9. package/docs/spec-vs-impl.md +10 -10
  10. package/kg_schema.py +2 -520
  11. package/knowledge_graph.py +37 -4629
  12. package/knowledge_graph_api.py +11 -127
  13. package/latticeai/__init__.py +1 -1
  14. package/latticeai/api/admin.py +16 -17
  15. package/latticeai/api/agents.py +20 -7
  16. package/latticeai/api/auth.py +46 -15
  17. package/latticeai/api/chat.py +112 -76
  18. package/latticeai/api/health.py +1 -1
  19. package/latticeai/api/hooks.py +1 -1
  20. package/latticeai/api/invitations.py +100 -0
  21. package/latticeai/api/knowledge_graph.py +139 -0
  22. package/latticeai/api/local_files.py +1 -1
  23. package/latticeai/api/mcp.py +23 -11
  24. package/latticeai/api/memory.py +1 -1
  25. package/latticeai/api/models.py +1 -1
  26. package/latticeai/api/network.py +81 -0
  27. package/latticeai/api/plugins.py +3 -6
  28. package/latticeai/api/realtime.py +5 -8
  29. package/latticeai/api/search.py +26 -2
  30. package/latticeai/api/security_dashboard.py +2 -3
  31. package/latticeai/api/setup.py +2 -2
  32. package/latticeai/api/static_routes.py +11 -16
  33. package/latticeai/api/tools.py +3 -0
  34. package/latticeai/api/ui_redirects.py +26 -0
  35. package/latticeai/api/workflow_designer.py +85 -6
  36. package/latticeai/api/workspace.py +93 -57
  37. package/latticeai/app_factory.py +1781 -0
  38. package/latticeai/brain/__init__.py +18 -0
  39. package/latticeai/brain/_kg_common.py +1123 -0
  40. package/latticeai/brain/context.py +213 -0
  41. package/latticeai/brain/conversations.py +236 -0
  42. package/latticeai/brain/discovery.py +1455 -0
  43. package/latticeai/brain/documents.py +218 -0
  44. package/latticeai/brain/identity.py +175 -0
  45. package/latticeai/brain/ingest.py +644 -0
  46. package/latticeai/brain/memory.py +102 -0
  47. package/latticeai/brain/network.py +205 -0
  48. package/latticeai/brain/projection.py +561 -0
  49. package/latticeai/brain/provenance.py +401 -0
  50. package/latticeai/brain/retrieval.py +1316 -0
  51. package/latticeai/brain/schema.py +640 -0
  52. package/latticeai/brain/store.py +216 -0
  53. package/latticeai/brain/write_master.py +225 -0
  54. package/latticeai/core/agent.py +31 -7
  55. package/latticeai/core/audit.py +0 -7
  56. package/latticeai/core/config.py +1 -1
  57. package/latticeai/core/context_builder.py +1 -2
  58. package/latticeai/core/enterprise.py +1 -1
  59. package/latticeai/core/graph_curator.py +2 -2
  60. package/latticeai/core/invitations.py +131 -0
  61. package/latticeai/core/marketplace.py +1 -1
  62. package/latticeai/core/mcp_registry.py +791 -0
  63. package/latticeai/core/model_compat.py +1 -1
  64. package/latticeai/core/model_resolution.py +0 -1
  65. package/latticeai/core/multi_agent.py +238 -4
  66. package/latticeai/core/policy.py +54 -0
  67. package/latticeai/core/realtime.py +65 -44
  68. package/latticeai/core/security.py +1 -1
  69. package/latticeai/core/sessions.py +66 -10
  70. package/latticeai/core/users.py +147 -0
  71. package/latticeai/core/workflow_engine.py +114 -2
  72. package/latticeai/core/workspace_os.py +477 -29
  73. package/latticeai/models/__init__.py +7 -0
  74. package/latticeai/models/router.py +779 -0
  75. package/latticeai/server_app.py +29 -1536
  76. package/latticeai/services/agent_runtime.py +243 -4
  77. package/latticeai/services/app_context.py +75 -14
  78. package/latticeai/services/ingestion.py +47 -0
  79. package/latticeai/services/kg_portability.py +33 -3
  80. package/latticeai/services/memory_service.py +39 -11
  81. package/latticeai/services/model_runtime.py +2 -5
  82. package/latticeai/services/platform_runtime.py +100 -23
  83. package/latticeai/services/run_executor.py +328 -0
  84. package/latticeai/services/search_service.py +17 -8
  85. package/latticeai/services/tool_dispatch.py +12 -2
  86. package/latticeai/services/triggers.py +241 -0
  87. package/latticeai/services/upload_service.py +37 -12
  88. package/latticeai/services/workspace_service.py +55 -16
  89. package/llm_router.py +29 -772
  90. package/ltcai_cli.py +1 -2
  91. package/mcp_registry.py +25 -788
  92. package/p_reinforce.py +124 -14
  93. package/package.json +10 -20
  94. package/scripts/bump_version.py +99 -0
  95. package/scripts/generate_diagrams.py +0 -1
  96. package/scripts/lint_v3.mjs +105 -18
  97. package/scripts/validate_release_artifacts.py +0 -1
  98. package/scripts/wheel_smoke.py +142 -0
  99. package/server.py +11 -7
  100. package/setup_wizard.py +1142 -0
  101. package/static/sw.js +81 -52
  102. package/static/v3/asset-manifest.json +33 -25
  103. package/static/v3/css/{lattice.base.e4cdd05d.css → lattice.base.49deefb5.css} +1 -1
  104. package/static/v3/css/lattice.base.css +1 -1
  105. package/static/v3/css/{lattice.components.9b49d614.css → lattice.components.cde18231.css} +1 -1
  106. package/static/v3/css/lattice.components.css +1 -1
  107. package/static/v3/css/{lattice.shell.8fcc9d33.css → lattice.shell.29d36d85.css} +1 -1
  108. package/static/v3/css/lattice.shell.css +1 -1
  109. package/static/v3/css/{lattice.tokens.e7018963.css → lattice.tokens.304cbc40.css} +3 -0
  110. package/static/v3/css/lattice.tokens.css +3 -0
  111. package/static/v3/css/{lattice.views.22f69117.css → lattice.views.0a18b6c5.css} +2 -2
  112. package/static/v3/css/lattice.views.css +2 -2
  113. package/static/v3/index.html +3 -4
  114. package/static/v3/js/{app.c541f955.js → app.c5c80c46.js} +1 -1
  115. package/static/v3/js/core/{api.33d6320e.js → api.ba0fbf14.js} +58 -1
  116. package/static/v3/js/core/api.js +57 -0
  117. package/static/v3/js/core/i18n.880e1fec.js +575 -0
  118. package/static/v3/js/core/i18n.js +575 -0
  119. package/static/v3/js/core/routes.37522821.js +101 -0
  120. package/static/v3/js/core/routes.js +71 -63
  121. package/static/v3/js/core/{shell.8c163e0e.js → shell.e3f6bbfa.js} +68 -39
  122. package/static/v3/js/core/shell.js +66 -37
  123. package/static/v3/js/core/{store.34ebd5e6.js → store.7b2aa044.js} +11 -1
  124. package/static/v3/js/core/store.js +11 -1
  125. package/static/v3/js/views/account.eff40715.js +143 -0
  126. package/static/v3/js/views/account.js +143 -0
  127. package/static/v3/js/views/activity.0d271ef9.js +67 -0
  128. package/static/v3/js/views/activity.js +67 -0
  129. package/static/v3/js/views/{admin-users.03bac88c.js → admin-users.f7ac7b43.js} +4 -6
  130. package/static/v3/js/views/admin-users.js +4 -6
  131. package/static/v3/js/views/{agents.014d0b74.js → agents.17c5288d.js} +35 -12
  132. package/static/v3/js/views/agents.js +35 -12
  133. package/static/v3/js/views/{chat.e6dd7dd0.js → chat.e250e2cc.js} +23 -0
  134. package/static/v3/js/views/chat.js +23 -0
  135. package/static/v3/js/views/graph-canvas.17c15d65.js +509 -0
  136. package/static/v3/js/views/graph-canvas.js +509 -0
  137. package/static/v3/js/views/{hybrid-search.b22b97e0.js → hybrid-search.2fb63ed9.js} +1 -2
  138. package/static/v3/js/views/hybrid-search.js +1 -2
  139. package/static/v3/js/views/{knowledge-graph.a96040a5.js → knowledge-graph.4d09c537.js} +60 -44
  140. package/static/v3/js/views/knowledge-graph.js +60 -44
  141. package/static/v3/js/views/network.52a4f181.js +97 -0
  142. package/static/v3/js/views/network.js +97 -0
  143. package/static/v3/js/views/{planning.9ac3e313.js → planning.4876fd77.js} +26 -5
  144. package/static/v3/js/views/planning.js +26 -5
  145. package/static/v3/js/views/runs.b63b2afa.js +144 -0
  146. package/static/v3/js/views/runs.js +144 -0
  147. package/static/v3/js/views/{settings.8631fa5e.js → settings.b7140634.js} +7 -8
  148. package/static/v3/js/views/settings.js +7 -8
  149. package/static/v3/js/views/snapshots.6f5db095.js +135 -0
  150. package/static/v3/js/views/snapshots.js +135 -0
  151. package/static/v3/js/views/{workflows.26c57290.js → workflows.7752225a.js} +87 -2
  152. package/static/v3/js/views/workflows.js +87 -2
  153. package/static/v3/js/views/workspace-admin.c466029b.js +156 -0
  154. package/static/v3/js/views/workspace-admin.js +156 -0
  155. package/static/vendor/chart.umd.min.js +20 -0
  156. package/static/vendor/fonts/inter-latin-300-normal.woff2 +0 -0
  157. package/static/vendor/fonts/inter-latin-400-normal.woff2 +0 -0
  158. package/static/vendor/fonts/inter-latin-500-normal.woff2 +0 -0
  159. package/static/vendor/fonts/inter-latin-600-normal.woff2 +0 -0
  160. package/static/vendor/fonts/inter-latin-700-normal.woff2 +0 -0
  161. package/static/vendor/fonts/inter-latin-800-normal.woff2 +0 -0
  162. package/static/vendor/fonts/inter.css +44 -0
  163. package/static/vendor/icons/tabler-icons.min.css +4 -0
  164. package/static/vendor/icons/tabler-icons.woff2 +0 -0
  165. package/static/vendor/marked.min.js +69 -0
  166. package/telegram_bot.py +1 -2
  167. package/tools/commands.py +4 -2
  168. package/tools/computer.py +1 -1
  169. package/tools/documents.py +1 -3
  170. package/tools/filesystem.py +0 -4
  171. package/tools/knowledge.py +1 -3
  172. package/tools/network.py +1 -3
  173. package/codex_telegram_bot.py +0 -195
  174. package/docs/assets/v3.4.0/agent-run.png +0 -0
  175. package/docs/assets/v3.4.0/agents.png +0 -0
  176. package/docs/assets/v3.4.0/before/chat-before.png +0 -0
  177. package/docs/assets/v3.4.0/before/files-before.png +0 -0
  178. package/docs/assets/v3.4.0/chat.png +0 -0
  179. package/docs/assets/v3.4.0/connect-folder.png +0 -0
  180. package/docs/assets/v3.4.0/files.png +0 -0
  181. package/docs/assets/v3.4.0/home.png +0 -0
  182. package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
  183. package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
  184. package/docs/assets/v3.4.0/local-agent.png +0 -0
  185. package/docs/assets/v3.4.0/memory.png +0 -0
  186. package/docs/assets/v3.4.0/settings.png +0 -0
  187. package/docs/assets/v3.4.0/vision-input.png +0 -0
  188. package/docs/assets/v3.4.0/workflows.png +0 -0
  189. package/docs/assets/v3.4.1/e2e_runtime_log.txt +0 -42
  190. package/docs/assets/v3.4.1/hooks-dispatch.png +0 -0
  191. package/docs/assets/v3.4.1/local-agent.png +0 -0
  192. package/docs/images/admin-dashboard.png +0 -0
  193. package/docs/images/architecture.png +0 -0
  194. package/docs/images/enterprise.png +0 -0
  195. package/docs/images/graph.png +0 -0
  196. package/docs/images/hero.gif +0 -0
  197. package/docs/images/knowledge-graph.png +0 -0
  198. package/docs/images/lattice-ai-demo.gif +0 -0
  199. package/docs/images/lattice-ai-hero.png +0 -0
  200. package/docs/images/logo.svg +0 -33
  201. package/docs/images/mobile-responsive.png +0 -0
  202. package/docs/images/model-recommendation.png +0 -0
  203. package/docs/images/onboarding.png +0 -0
  204. package/docs/images/organization.png +0 -0
  205. package/docs/images/pipeline.png +0 -0
  206. package/docs/images/screenshot-admin.png +0 -0
  207. package/docs/images/screenshot-chat.png +0 -0
  208. package/docs/images/screenshot-graph.png +0 -0
  209. package/docs/images/skills.png +0 -0
  210. package/docs/images/workspace-dark.png +0 -0
  211. package/docs/images/workspace-light.png +0 -0
  212. package/docs/images/workspace.png +0 -0
  213. package/requirements.txt +0 -16
  214. package/static/account.html +0 -115
  215. package/static/activity.html +0 -73
  216. package/static/admin.html +0 -488
  217. package/static/agents.html +0 -139
  218. package/static/chat.html +0 -844
  219. package/static/css/reference/account.css +0 -439
  220. package/static/css/reference/admin.css +0 -610
  221. package/static/css/reference/base.css +0 -1661
  222. package/static/css/reference/chat.css +0 -4623
  223. package/static/css/reference/graph.css +0 -1016
  224. package/static/css/responsive.css +0 -861
  225. package/static/graph.html +0 -124
  226. package/static/platform.css +0 -104
  227. package/static/plugins.html +0 -136
  228. package/static/scripts/account.js +0 -238
  229. package/static/scripts/admin.js +0 -1614
  230. package/static/scripts/chat.js +0 -5081
  231. package/static/scripts/graph.js +0 -1804
  232. package/static/scripts/platform.js +0 -64
  233. package/static/scripts/ux.js +0 -167
  234. package/static/scripts/workspace.js +0 -948
  235. package/static/v3/js/core/routes.2ce3815a.js +0 -93
  236. package/static/workflows.html +0 -146
  237. package/static/workspace.css +0 -1121
  238. package/static/workspace.html +0 -357
@@ -0,0 +1,67 @@
1
+ import { t } from "../core/i18n.880e1fec.js";
2
+
3
+ export async function render(ctx) {
4
+ const { h, api, c } = ctx;
5
+ const feedHost = h("div", c.loading({ lines: 4 }));
6
+ const presenceHost = h("div", c.loading({ lines: 2 }));
7
+ const timelineHost = h("div", c.loading({ lines: 4 }));
8
+
9
+ const root = h("div.lt3-stack-6",
10
+ c.viewHeader({ eyebrow: t("activity.eyebrow"), title: t("activity.title"), sub: t("activity.sub") }),
11
+ c.panel({ title: t("activity.feed"), children: feedHost }),
12
+ c.panel({ title: t("activity.presence"), children: presenceHost }),
13
+ c.panel({ title: t("activity.timeMachine"), children: timelineHost }),
14
+ );
15
+
16
+ await load();
17
+ wireLiveFeed();
18
+ return root;
19
+
20
+ async function load() {
21
+ const [feed, presence, timeline] = await Promise.all([api.realtimeFeed(80), api.presence(), api.timeMachine(80)]);
22
+ feedHost.replaceChildren(listEvents(ctx, feed.data?.events || [], feed.source));
23
+ presenceHost.replaceChildren(listPresence(ctx, presence.data?.presence || [], presence.source));
24
+ timelineHost.replaceChildren(listEvents(ctx, timeline.data?.events || [], timeline.source));
25
+ }
26
+
27
+ function wireLiveFeed() {
28
+ if (!window.EventSource) return;
29
+ try {
30
+ const stream = new EventSource("/realtime/stream");
31
+ stream.onmessage = () => load();
32
+ setTimeout(() => stream.close(), 120000);
33
+ } catch {}
34
+ }
35
+ }
36
+
37
+ function listEvents(ctx, events, source) {
38
+ const { h, c } = ctx;
39
+ return h("div.lt3-stack-3",
40
+ h("div.lt3-row-2", c.sourceBadge(source)),
41
+ events.length ? c.table([
42
+ { key: "event", label: t("common.status"), render: (e) => h("div", h("b", e.event_type || e.type || e.area || "event"), h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } }, e.payload?.run_id || e.payload?.workflow_id || e.id || "")) },
43
+ { key: "area", label: t("common.type"), width: "1%", render: (e) => c.pill(e.area || e.kind || "system") },
44
+ { key: "when", label: t("common.created"), width: "1%", render: (e) => h("span.lt3-faint", { style: { "white-space": "nowrap" } }, fmt(e.timestamp || e.created_at || e.at)) },
45
+ ], events.slice(0, 50)) : c.emptyState({ icon: "activity", title: t("activity.feed"), body: t("common.none") }),
46
+ );
47
+ }
48
+
49
+ function listPresence(ctx, rows, source) {
50
+ const { h, c } = ctx;
51
+ return h("div.lt3-stack-3",
52
+ h("div.lt3-row-2", c.sourceBadge(source)),
53
+ rows.length ? c.table([
54
+ { key: "user", label: t("account.email"), render: (p) => p.user || p.email || p.client_id || "local" },
55
+ { key: "workspace", label: "workspace_id", render: (p) => h("span.lt3-mono", p.workspace_id || "personal") },
56
+ { key: "when", label: t("common.updated"), width: "1%", render: (p) => fmt(p.last_seen || p.joined_at) },
57
+ ], rows) : c.emptyState({ icon: "users", title: t("activity.presence"), body: t("common.none") }),
58
+ );
59
+ }
60
+
61
+ function fmt(ts) {
62
+ if (!ts) return "—";
63
+ try {
64
+ const d = typeof ts === "number" ? new Date(ts * 1000) : new Date(ts);
65
+ return Number.isNaN(d.getTime()) ? String(ts) : d.toLocaleString();
66
+ } catch { return String(ts); }
67
+ }
@@ -0,0 +1,67 @@
1
+ import { t } from "../core/i18n.js";
2
+
3
+ export async function render(ctx) {
4
+ const { h, api, c } = ctx;
5
+ const feedHost = h("div", c.loading({ lines: 4 }));
6
+ const presenceHost = h("div", c.loading({ lines: 2 }));
7
+ const timelineHost = h("div", c.loading({ lines: 4 }));
8
+
9
+ const root = h("div.lt3-stack-6",
10
+ c.viewHeader({ eyebrow: t("activity.eyebrow"), title: t("activity.title"), sub: t("activity.sub") }),
11
+ c.panel({ title: t("activity.feed"), children: feedHost }),
12
+ c.panel({ title: t("activity.presence"), children: presenceHost }),
13
+ c.panel({ title: t("activity.timeMachine"), children: timelineHost }),
14
+ );
15
+
16
+ await load();
17
+ wireLiveFeed();
18
+ return root;
19
+
20
+ async function load() {
21
+ const [feed, presence, timeline] = await Promise.all([api.realtimeFeed(80), api.presence(), api.timeMachine(80)]);
22
+ feedHost.replaceChildren(listEvents(ctx, feed.data?.events || [], feed.source));
23
+ presenceHost.replaceChildren(listPresence(ctx, presence.data?.presence || [], presence.source));
24
+ timelineHost.replaceChildren(listEvents(ctx, timeline.data?.events || [], timeline.source));
25
+ }
26
+
27
+ function wireLiveFeed() {
28
+ if (!window.EventSource) return;
29
+ try {
30
+ const stream = new EventSource("/realtime/stream");
31
+ stream.onmessage = () => load();
32
+ setTimeout(() => stream.close(), 120000);
33
+ } catch {}
34
+ }
35
+ }
36
+
37
+ function listEvents(ctx, events, source) {
38
+ const { h, c } = ctx;
39
+ return h("div.lt3-stack-3",
40
+ h("div.lt3-row-2", c.sourceBadge(source)),
41
+ events.length ? c.table([
42
+ { key: "event", label: t("common.status"), render: (e) => h("div", h("b", e.event_type || e.type || e.area || "event"), h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } }, e.payload?.run_id || e.payload?.workflow_id || e.id || "")) },
43
+ { key: "area", label: t("common.type"), width: "1%", render: (e) => c.pill(e.area || e.kind || "system") },
44
+ { key: "when", label: t("common.created"), width: "1%", render: (e) => h("span.lt3-faint", { style: { "white-space": "nowrap" } }, fmt(e.timestamp || e.created_at || e.at)) },
45
+ ], events.slice(0, 50)) : c.emptyState({ icon: "activity", title: t("activity.feed"), body: t("common.none") }),
46
+ );
47
+ }
48
+
49
+ function listPresence(ctx, rows, source) {
50
+ const { h, c } = ctx;
51
+ return h("div.lt3-stack-3",
52
+ h("div.lt3-row-2", c.sourceBadge(source)),
53
+ rows.length ? c.table([
54
+ { key: "user", label: t("account.email"), render: (p) => p.user || p.email || p.client_id || "local" },
55
+ { key: "workspace", label: "workspace_id", render: (p) => h("span.lt3-mono", p.workspace_id || "personal") },
56
+ { key: "when", label: t("common.updated"), width: "1%", render: (p) => fmt(p.last_seen || p.joined_at) },
57
+ ], rows) : c.emptyState({ icon: "users", title: t("activity.presence"), body: t("common.none") }),
58
+ );
59
+ }
60
+
61
+ function fmt(ts) {
62
+ if (!ts) return "—";
63
+ try {
64
+ const d = typeof ts === "number" ? new Date(ts * 1000) : new Date(ts);
65
+ return Number.isNaN(d.getTime()) ? String(ts) : d.toLocaleString();
66
+ } catch { return String(ts); }
67
+ }
@@ -8,10 +8,8 @@
8
8
 
9
9
  import { timeAgo } from "../core/dom.a2773eb0.js";
10
10
 
11
- const UNAVAILABLE = "not available from this read-only users view.";
12
-
13
11
  export async function render(ctx) {
14
- const { h, icon, api, c, toast } = ctx;
12
+ const { h, icon, api, c, toast, navigate } = ctx;
15
13
 
16
14
  const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
17
15
  const tableHost = h("div", c.loading({ lines: 4 }));
@@ -24,7 +22,7 @@ export async function render(ctx) {
24
22
  sub: "Workspace members and access.",
25
23
  actions: [
26
24
  h("button.lt3-btn.lt3-btn--primary",
27
- { on: { click: () => toast("Invite user is " + UNAVAILABLE, "info") } },
25
+ { on: { click: () => navigate("workspace-admin") } },
28
26
  icon("user-plus"), "Invite user"),
29
27
  ],
30
28
  }),
@@ -67,7 +65,7 @@ export async function render(ctx) {
67
65
  title: "No members yet",
68
66
  body: "Invite teammates to give them access to this workspace.",
69
67
  action: h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm",
70
- { on: { click: () => toast("Invite user is " + UNAVAILABLE, "info") } },
68
+ { on: { click: () => navigate("workspace-admin") } },
71
69
  icon("user-plus"), "Invite user"),
72
70
  }));
73
71
  return;
@@ -105,7 +103,7 @@ export async function render(ctx) {
105
103
  render: (r) => h("button.lt3-iconbtn.lt3-iconbtn--sm",
106
104
  {
107
105
  "aria-label": `Manage ${r.nickname || r.email}`,
108
- on: { click: () => toast(`Manage ${r.nickname || r.email} is ` + UNAVAILABLE, "info") },
106
+ on: { click: () => toast(`Manage ${r.nickname || r.email} in Workspaces`, "info") },
109
107
  },
110
108
  icon("dots-vertical")),
111
109
  },
@@ -8,10 +8,8 @@
8
8
 
9
9
  import { timeAgo } from "../core/dom.js";
10
10
 
11
- const UNAVAILABLE = "not available from this read-only users view.";
12
-
13
11
  export async function render(ctx) {
14
- const { h, icon, api, c, toast } = ctx;
12
+ const { h, icon, api, c, toast, navigate } = ctx;
15
13
 
16
14
  const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
17
15
  const tableHost = h("div", c.loading({ lines: 4 }));
@@ -24,7 +22,7 @@ export async function render(ctx) {
24
22
  sub: "Workspace members and access.",
25
23
  actions: [
26
24
  h("button.lt3-btn.lt3-btn--primary",
27
- { on: { click: () => toast("Invite user is " + UNAVAILABLE, "info") } },
25
+ { on: { click: () => navigate("workspace-admin") } },
28
26
  icon("user-plus"), "Invite user"),
29
27
  ],
30
28
  }),
@@ -67,7 +65,7 @@ export async function render(ctx) {
67
65
  title: "No members yet",
68
66
  body: "Invite teammates to give them access to this workspace.",
69
67
  action: h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm",
70
- { on: { click: () => toast("Invite user is " + UNAVAILABLE, "info") } },
68
+ { on: { click: () => navigate("workspace-admin") } },
71
69
  icon("user-plus"), "Invite user"),
72
70
  }));
73
71
  return;
@@ -105,7 +103,7 @@ export async function render(ctx) {
105
103
  render: (r) => h("button.lt3-iconbtn.lt3-iconbtn--sm",
106
104
  {
107
105
  "aria-label": `Manage ${r.nickname || r.email}`,
108
- on: { click: () => toast(`Manage ${r.nickname || r.email} is ` + UNAVAILABLE, "info") },
106
+ on: { click: () => toast(`Manage ${r.nickname || r.email} in Workspaces`, "info") },
109
107
  },
110
108
  icon("dots-vertical")),
111
109
  },
@@ -4,7 +4,7 @@
4
4
  * role roster enriched with real run counts, the live recent-runs ledger, and
5
5
  * runtime health. Reports unavailable state when the runtime is unreachable.
6
6
  * Also drives runs directly: a goal + role selection → POST /agents/api/run →
7
- * a real timeline (logs), final status/output, queue/status, and stop.
7
+ * a durable async run, live logs, final status/output, queue/status, and stop.
8
8
  * ========================================================================== */
9
9
 
10
10
  import { timeAgo } from "../core/dom.a2773eb0.js";
@@ -37,7 +37,7 @@ export async function render(ctx) {
37
37
  h("div",
38
38
  h("div.lt3-eyebrow", "Run"),
39
39
  h("h3.lt3-panel__title", "Run agents"),
40
- h("p.lt3-panel__sub", "Give the pipeline a goal. Planner → executor → reviewer run synchronously and locally no model required."),
40
+ h("p.lt3-panel__sub", "Give the pipeline a goal. Planner → executor → reviewer run locally with durable progress and cooperative cancellation."),
41
41
  ),
42
42
  runSrc,
43
43
  ),
@@ -134,7 +134,7 @@ function makeRunConsole(ctx, hosts) {
134
134
  if (!roles.length) { ctx.toast("Select at least one role", "info"); return; }
135
135
 
136
136
  runBtn.disabled = true;
137
- runBtn.replaceChildren(c.icon("loader-2"), "Running…");
137
+ runBtn.replaceChildren(c.icon("loader-2"), "Starting…");
138
138
  runSrc.replaceChildren(c.sourceBadge("pending"));
139
139
  logsHost.replaceChildren(h("div", { style: { "margin-top": "var(--lt3-space-3)" } }, c.loading({ lines: 4 })));
140
140
 
@@ -157,18 +157,40 @@ function makeRunConsole(ctx, hosts) {
157
157
  const run = data.run || {};
158
158
  const result = data.result || {};
159
159
  logsHost.replaceChildren(renderRunResult(run, result));
160
- ctx.toast(`Run ${mapStatus(result.status) === "failed" ? "completed with failure" : "complete"}`, mapStatus(result.status) === "failed" ? "warn" : "ok");
160
+ if (data.accepted && (run.id || run.run_id)) {
161
+ ctx.toast("Run queued", "ok");
162
+ pollRun(run.id || run.run_id);
163
+ } else {
164
+ ctx.toast(`Run ${mapStatus(result.status) === "failed" ? "completed with failure" : "complete"}`, mapStatus(result.status) === "failed" ? "warn" : "ok");
165
+ }
161
166
 
162
167
  // Refresh runtime so queue/total/recent-runs reflect this run.
163
168
  hydrate();
164
169
  }
165
170
 
171
+ async function pollRun(runId) {
172
+ for (let i = 0; i < 80; i += 1) {
173
+ await sleep(i < 10 ? 400 : 1200);
174
+ const res = await ctx.api.agentRunDetail(runId);
175
+ const data = (res && res.data) || {};
176
+ if (!res || !res.ok) return;
177
+ const run = data.run || {};
178
+ logsHost.replaceChildren(renderRunResult(run, run));
179
+ hydrate();
180
+ if (!isActiveStatus(run.status)) {
181
+ const mapped = mapStatus(run.status);
182
+ ctx.toast(`Run ${mapped === "failed" ? "completed with failure" : "finished"}`, mapped === "failed" ? "warn" : "ok");
183
+ return;
184
+ }
185
+ }
186
+ }
187
+
166
188
  /* ── Render a run's result as logs + summary ───────────────────────────── */
167
189
  function renderRunResult(run, result) {
168
190
  const runId = run.id || run.run_id || result.run_id || result.id;
169
191
  const status = mapStatus(result.status || run.status);
170
- const timeline = Array.isArray(result.timeline) ? result.timeline : [];
171
- const output = result.output != null ? String(result.output) : "";
192
+ const timeline = Array.isArray(result.timeline) ? result.timeline : (Array.isArray(run.timeline) ? run.timeline : []);
193
+ const output = result.output != null ? String(result.output) : String(run.output_preview || "");
172
194
  const retries = Number(result.retries) || 0;
173
195
  const active = isActiveStatus(result.status || run.status);
174
196
 
@@ -183,9 +205,6 @@ function makeRunConsole(ctx, hosts) {
183
205
  ),
184
206
  runId
185
207
  ? h("button.lt3-btn.lt3-btn--danger.lt3-btn--sm", {
186
- // The synchronous runtime finishes inline; Stop is offered but
187
- // reports honestly (stopped:false + reason) when there's nothing
188
- // left to interrupt.
189
208
  title: active ? "Stop this run" : "This run has already finished",
190
209
  on: { click: (e) => stopRun(runId, e.currentTarget) },
191
210
  }, c.icon("player-stop"), "Stop")
@@ -242,7 +261,6 @@ function makeRunConsole(ctx, hosts) {
242
261
  return;
243
262
  }
244
263
  if (data.stopped === false || data.stopped == null) {
245
- // The synchronous runtime cannot interrupt an already-finished run.
246
264
  ctx.toast(String(data.reason || "Run already finished — nothing to stop"), "warn");
247
265
  } else {
248
266
  ctx.toast("Run stopped", "ok");
@@ -510,16 +528,21 @@ function mapStatus(status) {
510
528
  const s = String(status || "").toLowerCase();
511
529
  if (s === "ok" || s === "retried_ok") return "ready";
512
530
  if (s === "failed" || s === "rejected") return "failed";
513
- if (s === "running" || s === "in_progress") return "active";
531
+ if (s === "running" || s === "in_progress" || s === "queued" || s === "cancelling") return "active";
532
+ if (s === "cancelled" || s === "interrupted") return "warn";
514
533
  return s || "idle";
515
534
  }
516
535
 
517
536
  // An active run is one that could (in principle) still be stopped.
518
- const ACTIVE_STATES = new Set(["running", "in_progress", "queued", "pending", "active"]);
537
+ const ACTIVE_STATES = new Set(["running", "in_progress", "queued", "pending", "active", "cancelling"]);
519
538
  function isActiveStatus(status) {
520
539
  return ACTIVE_STATES.has(String(status || "").toLowerCase());
521
540
  }
522
541
 
542
+ function sleep(ms) {
543
+ return new Promise((resolve) => setTimeout(resolve, ms));
544
+ }
545
+
523
546
  function runNote(r) {
524
547
  const out = String(r.output || r.input || "").trim();
525
548
  if (out) return out.length > 96 ? out.slice(0, 96) + "…" : out;
@@ -4,7 +4,7 @@
4
4
  * role roster enriched with real run counts, the live recent-runs ledger, and
5
5
  * runtime health. Reports unavailable state when the runtime is unreachable.
6
6
  * Also drives runs directly: a goal + role selection → POST /agents/api/run →
7
- * a real timeline (logs), final status/output, queue/status, and stop.
7
+ * a durable async run, live logs, final status/output, queue/status, and stop.
8
8
  * ========================================================================== */
9
9
 
10
10
  import { timeAgo } from "../core/dom.js";
@@ -37,7 +37,7 @@ export async function render(ctx) {
37
37
  h("div",
38
38
  h("div.lt3-eyebrow", "Run"),
39
39
  h("h3.lt3-panel__title", "Run agents"),
40
- h("p.lt3-panel__sub", "Give the pipeline a goal. Planner → executor → reviewer run synchronously and locally no model required."),
40
+ h("p.lt3-panel__sub", "Give the pipeline a goal. Planner → executor → reviewer run locally with durable progress and cooperative cancellation."),
41
41
  ),
42
42
  runSrc,
43
43
  ),
@@ -134,7 +134,7 @@ function makeRunConsole(ctx, hosts) {
134
134
  if (!roles.length) { ctx.toast("Select at least one role", "info"); return; }
135
135
 
136
136
  runBtn.disabled = true;
137
- runBtn.replaceChildren(c.icon("loader-2"), "Running…");
137
+ runBtn.replaceChildren(c.icon("loader-2"), "Starting…");
138
138
  runSrc.replaceChildren(c.sourceBadge("pending"));
139
139
  logsHost.replaceChildren(h("div", { style: { "margin-top": "var(--lt3-space-3)" } }, c.loading({ lines: 4 })));
140
140
 
@@ -157,18 +157,40 @@ function makeRunConsole(ctx, hosts) {
157
157
  const run = data.run || {};
158
158
  const result = data.result || {};
159
159
  logsHost.replaceChildren(renderRunResult(run, result));
160
- ctx.toast(`Run ${mapStatus(result.status) === "failed" ? "completed with failure" : "complete"}`, mapStatus(result.status) === "failed" ? "warn" : "ok");
160
+ if (data.accepted && (run.id || run.run_id)) {
161
+ ctx.toast("Run queued", "ok");
162
+ pollRun(run.id || run.run_id);
163
+ } else {
164
+ ctx.toast(`Run ${mapStatus(result.status) === "failed" ? "completed with failure" : "complete"}`, mapStatus(result.status) === "failed" ? "warn" : "ok");
165
+ }
161
166
 
162
167
  // Refresh runtime so queue/total/recent-runs reflect this run.
163
168
  hydrate();
164
169
  }
165
170
 
171
+ async function pollRun(runId) {
172
+ for (let i = 0; i < 80; i += 1) {
173
+ await sleep(i < 10 ? 400 : 1200);
174
+ const res = await ctx.api.agentRunDetail(runId);
175
+ const data = (res && res.data) || {};
176
+ if (!res || !res.ok) return;
177
+ const run = data.run || {};
178
+ logsHost.replaceChildren(renderRunResult(run, run));
179
+ hydrate();
180
+ if (!isActiveStatus(run.status)) {
181
+ const mapped = mapStatus(run.status);
182
+ ctx.toast(`Run ${mapped === "failed" ? "completed with failure" : "finished"}`, mapped === "failed" ? "warn" : "ok");
183
+ return;
184
+ }
185
+ }
186
+ }
187
+
166
188
  /* ── Render a run's result as logs + summary ───────────────────────────── */
167
189
  function renderRunResult(run, result) {
168
190
  const runId = run.id || run.run_id || result.run_id || result.id;
169
191
  const status = mapStatus(result.status || run.status);
170
- const timeline = Array.isArray(result.timeline) ? result.timeline : [];
171
- const output = result.output != null ? String(result.output) : "";
192
+ const timeline = Array.isArray(result.timeline) ? result.timeline : (Array.isArray(run.timeline) ? run.timeline : []);
193
+ const output = result.output != null ? String(result.output) : String(run.output_preview || "");
172
194
  const retries = Number(result.retries) || 0;
173
195
  const active = isActiveStatus(result.status || run.status);
174
196
 
@@ -183,9 +205,6 @@ function makeRunConsole(ctx, hosts) {
183
205
  ),
184
206
  runId
185
207
  ? h("button.lt3-btn.lt3-btn--danger.lt3-btn--sm", {
186
- // The synchronous runtime finishes inline; Stop is offered but
187
- // reports honestly (stopped:false + reason) when there's nothing
188
- // left to interrupt.
189
208
  title: active ? "Stop this run" : "This run has already finished",
190
209
  on: { click: (e) => stopRun(runId, e.currentTarget) },
191
210
  }, c.icon("player-stop"), "Stop")
@@ -242,7 +261,6 @@ function makeRunConsole(ctx, hosts) {
242
261
  return;
243
262
  }
244
263
  if (data.stopped === false || data.stopped == null) {
245
- // The synchronous runtime cannot interrupt an already-finished run.
246
264
  ctx.toast(String(data.reason || "Run already finished — nothing to stop"), "warn");
247
265
  } else {
248
266
  ctx.toast("Run stopped", "ok");
@@ -510,16 +528,21 @@ function mapStatus(status) {
510
528
  const s = String(status || "").toLowerCase();
511
529
  if (s === "ok" || s === "retried_ok") return "ready";
512
530
  if (s === "failed" || s === "rejected") return "failed";
513
- if (s === "running" || s === "in_progress") return "active";
531
+ if (s === "running" || s === "in_progress" || s === "queued" || s === "cancelling") return "active";
532
+ if (s === "cancelled" || s === "interrupted") return "warn";
514
533
  return s || "idle";
515
534
  }
516
535
 
517
536
  // An active run is one that could (in principle) still be stopped.
518
- const ACTIVE_STATES = new Set(["running", "in_progress", "queued", "pending", "active"]);
537
+ const ACTIVE_STATES = new Set(["running", "in_progress", "queued", "pending", "active", "cancelling"]);
519
538
  function isActiveStatus(status) {
520
539
  return ACTIVE_STATES.has(String(status || "").toLowerCase());
521
540
  }
522
541
 
542
+ function sleep(ms) {
543
+ return new Promise((resolve) => setTimeout(resolve, ms));
544
+ }
545
+
523
546
  function runNote(r) {
524
547
  const out = String(r.output || r.input || "").trim();
525
548
  if (out) return out.length > 96 ? out.slice(0, 96) + "…" : out;
@@ -12,6 +12,7 @@
12
12
  * ========================================================================== */
13
13
 
14
14
  import { timeAgo } from "../core/dom.a2773eb0.js";
15
+ import { t } from "../core/i18n.880e1fec.js";
15
16
 
16
17
  export const layout = "flush";
17
18
 
@@ -520,10 +521,32 @@ export async function render(ctx) {
520
521
  ? fileRefs.slice(0, 6).map((p) => ctxItem("var(--faint)", p, null))
521
522
  : [ctxEmpty("No file references yet")]),
522
523
 
524
+ ctxSection(t("chat.whyContext"), "route",
525
+ traceItems(state.lastTrace, q)),
526
+
523
527
  h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm.lt3-btn--block", { on: { click: () => navigate("hybrid-search", q ? { q } : undefined) } }, icon("arrows-join"), "Open Hybrid Search"),
524
528
  );
525
529
  }
526
530
 
531
+ function traceItems(trace, query) {
532
+ if (!trace) return [ctxEmpty(t("chat.traceUnavailable"))];
533
+ const items = [];
534
+ const meta = trace.retrieval_metadata || {};
535
+ items.push(ctxItem("var(--lt3-pillar-hybrid)", t("chat.traceQuestion"), trace.question || query || "—"));
536
+ if (trace.confidence != null) items.push(ctxItem("var(--lt3-pillar-vector)", t("chat.traceConfidence"), Number(trace.confidence).toFixed(2)));
537
+ if (meta.mode || meta.strategy) items.push(ctxItem("var(--lt3-pillar-hybrid)", "retrieval", meta.mode || meta.strategy));
538
+ if (Array.isArray(trace.graph_nodes) && trace.graph_nodes.length) {
539
+ items.push(ctxItem("var(--lt3-pillar-graph)", t("chat.traceReasons"), `${trace.graph_nodes.length} graph node(s)`));
540
+ }
541
+ if (Array.isArray(trace.vector_matches) && trace.vector_matches.length) {
542
+ items.push(ctxItem("var(--lt3-pillar-vector)", "vector", `${trace.vector_matches.length} match(es)`));
543
+ }
544
+ if (Array.isArray(trace.source_files) && trace.source_files.length) {
545
+ items.push(ctxItem("var(--faint)", "files", `${trace.source_files.length} source file(s)`));
546
+ }
547
+ return items.length ? items : [ctxEmpty(t("chat.traceUnavailable"))];
548
+ }
549
+
527
550
  function ctxSection(title, icn, children) {
528
551
  return h("section",
529
552
  h("div.lt3-ctx-sec__title", icon(icn), title),
@@ -12,6 +12,7 @@
12
12
  * ========================================================================== */
13
13
 
14
14
  import { timeAgo } from "../core/dom.js";
15
+ import { t } from "../core/i18n.js";
15
16
 
16
17
  export const layout = "flush";
17
18
 
@@ -520,10 +521,32 @@ export async function render(ctx) {
520
521
  ? fileRefs.slice(0, 6).map((p) => ctxItem("var(--faint)", p, null))
521
522
  : [ctxEmpty("No file references yet")]),
522
523
 
524
+ ctxSection(t("chat.whyContext"), "route",
525
+ traceItems(state.lastTrace, q)),
526
+
523
527
  h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm.lt3-btn--block", { on: { click: () => navigate("hybrid-search", q ? { q } : undefined) } }, icon("arrows-join"), "Open Hybrid Search"),
524
528
  );
525
529
  }
526
530
 
531
+ function traceItems(trace, query) {
532
+ if (!trace) return [ctxEmpty(t("chat.traceUnavailable"))];
533
+ const items = [];
534
+ const meta = trace.retrieval_metadata || {};
535
+ items.push(ctxItem("var(--lt3-pillar-hybrid)", t("chat.traceQuestion"), trace.question || query || "—"));
536
+ if (trace.confidence != null) items.push(ctxItem("var(--lt3-pillar-vector)", t("chat.traceConfidence"), Number(trace.confidence).toFixed(2)));
537
+ if (meta.mode || meta.strategy) items.push(ctxItem("var(--lt3-pillar-hybrid)", "retrieval", meta.mode || meta.strategy));
538
+ if (Array.isArray(trace.graph_nodes) && trace.graph_nodes.length) {
539
+ items.push(ctxItem("var(--lt3-pillar-graph)", t("chat.traceReasons"), `${trace.graph_nodes.length} graph node(s)`));
540
+ }
541
+ if (Array.isArray(trace.vector_matches) && trace.vector_matches.length) {
542
+ items.push(ctxItem("var(--lt3-pillar-vector)", "vector", `${trace.vector_matches.length} match(es)`));
543
+ }
544
+ if (Array.isArray(trace.source_files) && trace.source_files.length) {
545
+ items.push(ctxItem("var(--faint)", "files", `${trace.source_files.length} source file(s)`));
546
+ }
547
+ return items.length ? items : [ctxEmpty(t("chat.traceUnavailable"))];
548
+ }
549
+
527
550
  function ctxSection(title, icn, children) {
528
551
  return h("section",
529
552
  h("div.lt3-ctx-sec__title", icon(icn), title),