ltcai 4.0.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 (108) hide show
  1. package/README.md +37 -33
  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_DIGITAL_BRAIN_RECOVERY.md +86 -43
  6. package/docs/kg-schema.md +6 -2
  7. package/docs/spec-vs-impl.md +10 -10
  8. package/kg_schema.py +2 -603
  9. package/knowledge_graph.py +37 -4958
  10. package/latticeai/__init__.py +1 -1
  11. package/latticeai/api/admin.py +15 -16
  12. package/latticeai/api/agents.py +13 -6
  13. package/latticeai/api/auth.py +19 -11
  14. package/latticeai/api/invitations.py +100 -0
  15. package/latticeai/api/knowledge_graph.py +4 -11
  16. package/latticeai/api/plugins.py +3 -6
  17. package/latticeai/api/realtime.py +4 -7
  18. package/latticeai/api/static_routes.py +9 -12
  19. package/latticeai/api/ui_redirects.py +26 -0
  20. package/latticeai/api/workflow_designer.py +39 -6
  21. package/latticeai/api/workspace.py +24 -10
  22. package/latticeai/app_factory.py +88 -17
  23. package/latticeai/brain/_kg_common.py +1123 -0
  24. package/latticeai/brain/discovery.py +1455 -0
  25. package/latticeai/brain/documents.py +218 -0
  26. package/latticeai/brain/ingest.py +644 -0
  27. package/latticeai/brain/projection.py +561 -0
  28. package/latticeai/brain/provenance.py +401 -0
  29. package/latticeai/brain/retrieval.py +1316 -0
  30. package/latticeai/brain/schema.py +640 -0
  31. package/latticeai/brain/store.py +216 -0
  32. package/latticeai/brain/write_master.py +225 -0
  33. package/latticeai/core/invitations.py +131 -0
  34. package/latticeai/core/marketplace.py +1 -1
  35. package/latticeai/core/multi_agent.py +1 -1
  36. package/latticeai/core/policy.py +54 -0
  37. package/latticeai/core/realtime.py +65 -44
  38. package/latticeai/core/sessions.py +31 -5
  39. package/latticeai/core/users.py +147 -0
  40. package/latticeai/core/workspace_os.py +420 -20
  41. package/latticeai/services/agent_runtime.py +242 -4
  42. package/latticeai/services/run_executor.py +328 -0
  43. package/latticeai/services/workspace_service.py +27 -19
  44. package/package.json +2 -14
  45. package/scripts/lint_v3.mjs +23 -0
  46. package/static/v3/asset-manifest.json +21 -14
  47. package/static/v3/js/{app.356e6452.js → app.c5c80c46.js} +1 -1
  48. package/static/v3/js/core/{api.7a308b89.js → api.ba0fbf14.js} +58 -1
  49. package/static/v3/js/core/api.js +57 -0
  50. package/static/v3/js/core/i18n.880e1fec.js +575 -0
  51. package/static/v3/js/core/i18n.js +575 -0
  52. package/static/v3/js/core/routes.37522821.js +101 -0
  53. package/static/v3/js/core/routes.js +71 -63
  54. package/static/v3/js/core/{shell.a1657f20.js → shell.e3f6bbfa.js} +67 -38
  55. package/static/v3/js/core/shell.js +65 -36
  56. package/static/v3/js/core/{store.204a08b2.js → store.7b2aa044.js} +10 -0
  57. package/static/v3/js/core/store.js +10 -0
  58. package/static/v3/js/views/account.eff40715.js +143 -0
  59. package/static/v3/js/views/account.js +143 -0
  60. package/static/v3/js/views/activity.0d271ef9.js +67 -0
  61. package/static/v3/js/views/activity.js +67 -0
  62. package/static/v3/js/views/{admin-users.03bac88c.js → admin-users.f7ac7b43.js} +4 -6
  63. package/static/v3/js/views/admin-users.js +4 -6
  64. package/static/v3/js/views/{agents.014d0b74.js → agents.17c5288d.js} +35 -12
  65. package/static/v3/js/views/agents.js +35 -12
  66. package/static/v3/js/views/{chat.e6dd7dd0.js → chat.e250e2cc.js} +23 -0
  67. package/static/v3/js/views/chat.js +23 -0
  68. package/static/v3/js/views/{knowledge-graph.5e40cbeb.js → knowledge-graph.4d09c537.js} +27 -7
  69. package/static/v3/js/views/knowledge-graph.js +27 -7
  70. package/static/v3/js/views/network.52a4f181.js +97 -0
  71. package/static/v3/js/views/network.js +97 -0
  72. package/static/v3/js/views/{planning.9ac3e313.js → planning.4876fd77.js} +26 -5
  73. package/static/v3/js/views/planning.js +26 -5
  74. package/static/v3/js/views/runs.b63b2afa.js +144 -0
  75. package/static/v3/js/views/runs.js +144 -0
  76. package/static/v3/js/views/{settings.8631fa5e.js → settings.b7140634.js} +7 -8
  77. package/static/v3/js/views/settings.js +7 -8
  78. package/static/v3/js/views/snapshots.6f5db095.js +135 -0
  79. package/static/v3/js/views/snapshots.js +135 -0
  80. package/static/v3/js/views/{workflows.26c57290.js → workflows.7752225a.js} +87 -2
  81. package/static/v3/js/views/workflows.js +87 -2
  82. package/static/v3/js/views/workspace-admin.c466029b.js +156 -0
  83. package/static/v3/js/views/workspace-admin.js +156 -0
  84. package/static/account.html +0 -113
  85. package/static/activity.html +0 -73
  86. package/static/admin.html +0 -486
  87. package/static/agents.html +0 -139
  88. package/static/chat.html +0 -841
  89. package/static/css/reference/account.css +0 -439
  90. package/static/css/reference/admin.css +0 -610
  91. package/static/css/reference/base.css +0 -1661
  92. package/static/css/reference/chat.css +0 -4623
  93. package/static/css/reference/graph.css +0 -1016
  94. package/static/css/responsive.css +0 -861
  95. package/static/graph.html +0 -122
  96. package/static/platform.css +0 -104
  97. package/static/plugins.html +0 -136
  98. package/static/scripts/account.js +0 -238
  99. package/static/scripts/admin.js +0 -1614
  100. package/static/scripts/chat.js +0 -5081
  101. package/static/scripts/graph.js +0 -1804
  102. package/static/scripts/platform.js +0 -64
  103. package/static/scripts/ux.js +0 -167
  104. package/static/scripts/workspace.js +0 -948
  105. package/static/v3/js/core/routes.7222343d.js +0 -93
  106. package/static/workflows.html +0 -146
  107. package/static/workspace.css +0 -1121
  108. 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),
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { escapeHtml } from "../core/dom.a2773eb0.js";
11
11
  import { createGraphCanvas } from "./graph-canvas.17c15d65.js";
12
+ import { t } from "../core/i18n.880e1fec.js";
12
13
 
13
14
  const TYPE_COLOR = {
14
15
  Topic: "var(--lt3-pillar-graph)",
@@ -247,21 +248,25 @@ function buildExplore(ctx) {
247
248
  async function renderStatus(ctx, host) {
248
249
  const { h, icon, api, c } = ctx;
249
250
  host.replaceChildren(c.loading({ lines: 3 }));
250
- const [port, gs, idx] = await Promise.all([api.kgPortability(), api.graphStats(), api.indexStatus()]);
251
+ const [port, gs, idx, coverage] = await Promise.all([api.kgPortability(), api.graphStats(), api.indexStatus(), api.kgProvenanceCoverage()]);
251
252
  const p = port.data || {};
252
253
  const prov = p.provenance || {};
254
+ const cov = coverage.data || {};
253
255
  const nodes = sumCounts((gs.data && gs.data.nodes) || {});
254
256
  const edges = sumCounts((gs.data && gs.data.edges) || {});
255
257
  const pipelines = (idx.data && idx.data.pipelines) || {};
258
+ const ratio = Number(cov.coverage_ratio ?? cov.ratio ?? 0);
259
+ const covered = Number(cov.nodes_with_provenance ?? cov.covered_nodes ?? 0);
256
260
 
257
261
  host.replaceChildren(
258
- h("div.lt3-row-2", c.sourceBadge(port.source), h("span.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)" } },
262
+ h("div.lt3-row-2", c.sourceBadge(port.source === "live" || coverage.source === "live" ? "live" : port.source), h("span.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)" } },
259
263
  p.graph_schema_version != null ? `Schema v${p.graph_schema_version} · embed dim ${p.embed_dim ?? "—"}` : "Knowledge Graph status")),
260
264
  h("div.lt3-statrow",
261
265
  c.stat({ label: "Entities", value: c.fmtNum(nodes), icon: "circles" }),
262
266
  c.stat({ label: "Relations", value: c.fmtNum(edges), icon: "vector-triangle" }),
263
267
  c.stat({ label: "Ingested items", value: c.fmtNum(prov.total || 0), icon: "package-import" }),
264
268
  c.stat({ label: "Embedded (RAG-ready)", value: c.fmtNum(prov.embedded || 0), icon: "vector" }),
269
+ c.stat({ label: t("kg.provenanceCoverage"), value: `${Math.round(ratio * 100)}%`, icon: "shield-check", delta: `${covered}/${cov.total_nodes ?? nodes}` }),
265
270
  ),
266
271
  c.card(
267
272
  h("div.lt3-stack-3",
@@ -271,6 +276,14 @@ async function renderStatus(ctx, host) {
271
276
  pipelineRow(ctx, "Hybrid retrieval", pipelines.hybrid),
272
277
  ),
273
278
  ),
279
+ c.card(h("div.lt3-stack-3",
280
+ h("div.lt3-eyebrow", t("kg.provenanceCoverage")),
281
+ h("dl.lt3-keyval",
282
+ h("dt", t("kg.coveredNodes")), h("dd", `${covered}/${cov.total_nodes ?? nodes}`),
283
+ h("dt", t("kg.sourceTypes")), h("dd", compactCounts(cov.provenance_by_source_type || cov.by_source_type || {})),
284
+ h("dt", t("kg.uncoveredTypes")), h("dd", compactCounts(cov.uncovered_by_type || {})),
285
+ ),
286
+ )),
274
287
  prov.last_ingested_at
275
288
  ? h("p.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)" } }, `Last ingestion: ${fmtWhen(prov.last_ingested_at)} · ${prov.duplicates || 0} duplicate(s) linked, not re-stored.`)
276
289
  : c.emptyState({ icon: "package-import", title: "Nothing ingested yet", body: "Add files or capture a page to populate the graph." }),
@@ -327,18 +340,18 @@ function buildCapture(ctx) {
327
340
 
328
341
  async function run() {
329
342
  const url = (input.value || "").trim();
330
- if (!url) { result.replaceChildren(c.banner({ tone: "warn", text: "Enter a URL first." })); return; }
343
+ if (!url) { result.replaceChildren(c.banner("Enter a URL first.", "warn", "alert-triangle")); return; }
331
344
  result.replaceChildren(c.loading({ lines: 1 }));
332
345
  const res = await api.browserReadUrl(url);
333
346
  const d = res.data || {};
334
347
  if (res.ok && d.status === "ok") {
335
- result.replaceChildren(c.banner({ tone: "ok", text: `Added to your Knowledge Graph${d.duplicate ? " (already present — linked)" : ""}. ${d.chunk_count || 0} chunk(s) indexed.` }));
348
+ result.replaceChildren(c.banner(`Added to your Knowledge Graph${d.duplicate ? " (already present — linked)" : ""}. ${d.chunk_count || 0} chunk(s) indexed.`, "ok", "circle-check"));
336
349
  ctx.toast && ctx.toast("Page added to Knowledge Graph");
337
350
  } else if (d.status === "empty") {
338
- result.replaceChildren(c.banner({ tone: "warn", text: "No readable text was found on that page." }));
351
+ result.replaceChildren(c.banner("No readable text was found on that page.", "warn", "alert-triangle"));
339
352
  } else {
340
353
  const detail = d.detail || (res.status === 422 ? "The page is blocked or login-required." : "Could not read that URL.");
341
- result.replaceChildren(c.banner({ tone: "err", text: detail }));
354
+ result.replaceChildren(c.banner(detail, "err", "alert-triangle"));
342
355
  }
343
356
  }
344
357
 
@@ -366,7 +379,9 @@ async function renderPortability(ctx, host) {
366
379
  const port = await api.kgPortability();
367
380
  const status = h("div");
368
381
 
369
- function note(tone, text) { status.replaceChildren(c.banner({ tone, text })); }
382
+ function note(tone, text) {
383
+ status.replaceChildren(c.banner(text, tone, tone === "err" ? "alert-triangle" : tone === "ok" ? "circle-check" : "info-circle"));
384
+ }
370
385
 
371
386
  async function doExport() {
372
387
  note("info", "Exporting…");
@@ -439,6 +454,11 @@ function sumCounts(obj) {
439
454
  return Object.values(obj || {}).reduce((a, b) => a + (Number(b) || 0), 0);
440
455
  }
441
456
 
457
+ function compactCounts(obj) {
458
+ const entries = Object.entries(obj || {});
459
+ return entries.length ? entries.map(([k, v]) => `${k}: ${v}`).join(" · ") : "—";
460
+ }
461
+
442
462
  function prettySource(k) {
443
463
  return ({ web_url: "Web URL", browser_tab: "Browser tab", file: "Files", local_file: "Local files",
444
464
  note: "Notes", text: "Text", markdown: "Markdown", code: "Code", upload: "Uploads" })[k] || k;