ltcai 3.1.0 → 3.2.0
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 +34 -8
- package/docs/CHANGELOG.md +53 -0
- package/docs/V3_2_AUDIT.md +82 -0
- package/docs/V3_FRONTEND.md +1 -1
- package/docs/architecture.md +6 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agent_registry.py +103 -0
- package/latticeai/api/hooks.py +113 -0
- package/latticeai/api/marketplace.py +13 -0
- package/latticeai/api/memory.py +109 -0
- package/latticeai/core/agent_registry.py +234 -0
- package/latticeai/core/hooks.py +284 -0
- package/latticeai/core/marketplace.py +87 -2
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/server_app.py +41 -0
- package/latticeai/services/memory_service.py +324 -0
- package/package.json +2 -2
- package/scripts/build_v3_assets.mjs +1 -1
- package/static/v3/asset-manifest.json +16 -8
- package/static/v3/js/{app.46fb61d9.js → app.a5adc0f3.js} +1 -1
- package/static/v3/js/core/{api.22a41d42.js → api.603b978f.js} +64 -0
- package/static/v3/js/core/api.js +64 -0
- package/static/v3/js/core/{routes.f935dd50.js → routes.07ad6696.js} +11 -0
- package/static/v3/js/core/routes.js +11 -0
- package/static/v3/js/core/{shell.1b6199d6.js → shell.ea0b9ae5.js} +2 -2
- package/static/v3/js/views/{agents.14e48bdd.js → agents.c373d48c.js} +100 -0
- package/static/v3/js/views/agents.js +100 -0
- package/static/v3/js/views/hooks.f3edebca.js +99 -0
- package/static/v3/js/views/hooks.js +99 -0
- package/static/v3/js/views/marketplace.ab0583d4.js +141 -0
- package/static/v3/js/views/marketplace.js +141 -0
- package/static/v3/js/views/mcp.99b5c6a7.js +114 -0
- package/static/v3/js/views/mcp.js +114 -0
- package/static/v3/js/views/memory.d2ed7a7c.js +146 -0
- package/static/v3/js/views/memory.js +146 -0
- package/static/v3/js/views/planning.9ac3e313.js +153 -0
- package/static/v3/js/views/planning.js +153 -0
- package/static/v3/js/views/skills.c6c2f965.js +109 -0
- package/static/v3/js/views/skills.js +109 -0
- package/static/v3/js/views/tools.e4f11276.js +108 -0
- package/static/v3/js/views/tools.js +108 -0
- package/static/v3/js/views/workflows.26c57290.js +128 -0
- package/static/v3/js/views/workflows.js +128 -0
|
@@ -14,6 +14,7 @@ export const GROUPS = [
|
|
|
14
14
|
{ id: "retrieval", label: "Retrieval" },
|
|
15
15
|
{ id: "data", label: "Data" },
|
|
16
16
|
{ id: "compute", label: "Compute" },
|
|
17
|
+
{ id: "platform", label: "Platform" },
|
|
17
18
|
{ id: "system", label: "System" },
|
|
18
19
|
{ id: "admin", label: "Administration", adminOnly: true },
|
|
19
20
|
];
|
|
@@ -30,6 +31,7 @@ export const ROUTES = [
|
|
|
30
31
|
// Retrieval (the product identity)
|
|
31
32
|
{ key: "knowledge-graph", label: "Knowledge Graph", icon: "chart-dots-3", group: "retrieval", minMode: "basic", view: "knowledge-graph", title: "Knowledge Graph", desc: "Entities and relations extracted from your workspace." },
|
|
32
33
|
{ key: "hybrid-search", label: "Hybrid Search", icon: "arrows-join", group: "retrieval", minMode: "basic", view: "hybrid-search", title: "Hybrid Search", desc: "Graph structure fused with vector similarity." },
|
|
34
|
+
{ key: "memory", label: "Memory", icon: "brain", group: "retrieval", minMode: "basic", view: "memory", title: "Memory", desc: "Long-term workspace, project, agent, and conversation memory." },
|
|
33
35
|
|
|
34
36
|
// Data
|
|
35
37
|
{ key: "files", label: "Files", icon: "folders", group: "data", minMode: "basic", view: "files", title: "Files", desc: "Connected sources and indexed documents." },
|
|
@@ -37,9 +39,18 @@ export const ROUTES = [
|
|
|
37
39
|
|
|
38
40
|
// Compute
|
|
39
41
|
{ key: "agents", label: "Agents", icon: "robot", group: "compute", minMode: "advanced", view: "agents", title: "Agents", desc: "Multi-agent roles, runs, and handoffs." },
|
|
42
|
+
{ key: "workflows", label: "Workflows", icon: "sitemap", group: "compute", minMode: "advanced", view: "workflows", title: "Workflow Agents", desc: "Trigger → agent chain → tools → memory → result." },
|
|
43
|
+
{ key: "planning", label: "Planning", icon: "target-arrow", group: "compute", minMode: "advanced", view: "planning", title: "Autonomous Planning", desc: "Goal → plan → execute → review → replan." },
|
|
40
44
|
{ key: "models", label: "Models", icon: "cpu", group: "compute", minMode: "basic", view: "models", title: "Models", desc: "Local MLX models and embeddings." },
|
|
41
45
|
{ key: "my-computer", label: "My Computer", icon: "device-desktop-analytics", group: "compute", minMode: "advanced", view: "my-computer", title: "My Computer", desc: "Local hardware, memory, and runtime." },
|
|
42
46
|
|
|
47
|
+
// Platform (the agent ecosystem)
|
|
48
|
+
{ key: "marketplace", label: "Marketplace", icon: "building-store", group: "platform", minMode: "advanced", view: "marketplace", title: "Marketplace", desc: "Agent templates, agents, plugins, and skills." },
|
|
49
|
+
{ key: "skills", label: "Skills", icon: "puzzle", group: "platform", minMode: "advanced", view: "skills", title: "Skills", desc: "Install, enable, and manage skills." },
|
|
50
|
+
{ key: "hooks", label: "Hooks", icon: "webhook", group: "platform", minMode: "advanced", view: "hooks", title: "Hooks", desc: "Lifecycle hooks across runs, tools, and workflows." },
|
|
51
|
+
{ key: "tools", label: "Tools", icon: "tools", group: "platform", minMode: "advanced", view: "tools", title: "Tool Registry", desc: "Local, workspace, and MCP tools with governance." },
|
|
52
|
+
{ key: "mcp", label: "MCP", icon: "plug-connected", group: "platform", minMode: "advanced", view: "mcp", title: "MCP Manager", desc: "Connected MCP servers, available tools, and health." },
|
|
53
|
+
|
|
43
54
|
// System
|
|
44
55
|
{ key: "settings", label: "Settings", icon: "settings", group: "system", minMode: "basic", view: "settings", title: "Settings", desc: "Appearance, workspace, and integrations." },
|
|
45
56
|
|
|
@@ -14,6 +14,7 @@ export const GROUPS = [
|
|
|
14
14
|
{ id: "retrieval", label: "Retrieval" },
|
|
15
15
|
{ id: "data", label: "Data" },
|
|
16
16
|
{ id: "compute", label: "Compute" },
|
|
17
|
+
{ id: "platform", label: "Platform" },
|
|
17
18
|
{ id: "system", label: "System" },
|
|
18
19
|
{ id: "admin", label: "Administration", adminOnly: true },
|
|
19
20
|
];
|
|
@@ -30,6 +31,7 @@ export const ROUTES = [
|
|
|
30
31
|
// Retrieval (the product identity)
|
|
31
32
|
{ key: "knowledge-graph", label: "Knowledge Graph", icon: "chart-dots-3", group: "retrieval", minMode: "basic", view: "knowledge-graph", title: "Knowledge Graph", desc: "Entities and relations extracted from your workspace." },
|
|
32
33
|
{ key: "hybrid-search", label: "Hybrid Search", icon: "arrows-join", group: "retrieval", minMode: "basic", view: "hybrid-search", title: "Hybrid Search", desc: "Graph structure fused with vector similarity." },
|
|
34
|
+
{ key: "memory", label: "Memory", icon: "brain", group: "retrieval", minMode: "basic", view: "memory", title: "Memory", desc: "Long-term workspace, project, agent, and conversation memory." },
|
|
33
35
|
|
|
34
36
|
// Data
|
|
35
37
|
{ key: "files", label: "Files", icon: "folders", group: "data", minMode: "basic", view: "files", title: "Files", desc: "Connected sources and indexed documents." },
|
|
@@ -37,9 +39,18 @@ export const ROUTES = [
|
|
|
37
39
|
|
|
38
40
|
// Compute
|
|
39
41
|
{ key: "agents", label: "Agents", icon: "robot", group: "compute", minMode: "advanced", view: "agents", title: "Agents", desc: "Multi-agent roles, runs, and handoffs." },
|
|
42
|
+
{ key: "workflows", label: "Workflows", icon: "sitemap", group: "compute", minMode: "advanced", view: "workflows", title: "Workflow Agents", desc: "Trigger → agent chain → tools → memory → result." },
|
|
43
|
+
{ key: "planning", label: "Planning", icon: "target-arrow", group: "compute", minMode: "advanced", view: "planning", title: "Autonomous Planning", desc: "Goal → plan → execute → review → replan." },
|
|
40
44
|
{ key: "models", label: "Models", icon: "cpu", group: "compute", minMode: "basic", view: "models", title: "Models", desc: "Local MLX models and embeddings." },
|
|
41
45
|
{ key: "my-computer", label: "My Computer", icon: "device-desktop-analytics", group: "compute", minMode: "advanced", view: "my-computer", title: "My Computer", desc: "Local hardware, memory, and runtime." },
|
|
42
46
|
|
|
47
|
+
// Platform (the agent ecosystem)
|
|
48
|
+
{ key: "marketplace", label: "Marketplace", icon: "building-store", group: "platform", minMode: "advanced", view: "marketplace", title: "Marketplace", desc: "Agent templates, agents, plugins, and skills." },
|
|
49
|
+
{ key: "skills", label: "Skills", icon: "puzzle", group: "platform", minMode: "advanced", view: "skills", title: "Skills", desc: "Install, enable, and manage skills." },
|
|
50
|
+
{ key: "hooks", label: "Hooks", icon: "webhook", group: "platform", minMode: "advanced", view: "hooks", title: "Hooks", desc: "Lifecycle hooks across runs, tools, and workflows." },
|
|
51
|
+
{ key: "tools", label: "Tools", icon: "tools", group: "platform", minMode: "advanced", view: "tools", title: "Tool Registry", desc: "Local, workspace, and MCP tools with governance." },
|
|
52
|
+
{ key: "mcp", label: "MCP", icon: "plug-connected", group: "platform", minMode: "advanced", view: "mcp", title: "MCP Manager", desc: "Connected MCP servers, available tools, and health." },
|
|
53
|
+
|
|
43
54
|
// System
|
|
44
55
|
{ key: "settings", label: "Settings", icon: "settings", group: "system", minMode: "basic", view: "settings", title: "Settings", desc: "Appearance, workspace, and integrations." },
|
|
45
56
|
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import { h, icon, $, $$ } from "./dom.a2773eb0.js";
|
|
9
9
|
import { store } from "./store.34ebd5e6.js";
|
|
10
|
-
import { api } from "./api.
|
|
10
|
+
import { api } from "./api.603b978f.js";
|
|
11
11
|
import * as c from "./components.4c83e0a9.js";
|
|
12
12
|
import { createRouter } from "./router.584570f2.js";
|
|
13
|
-
import { GROUPS, ROUTES, ROUTE_BY_KEY, MODE_RANK, visibleRoutes, loadView } from "./routes.
|
|
13
|
+
import { GROUPS, ROUTES, ROUTE_BY_KEY, MODE_RANK, visibleRoutes, loadView } from "./routes.07ad6696.js";
|
|
14
14
|
|
|
15
15
|
const MODES = [
|
|
16
16
|
{ key: "basic", label: "Basic", icon: "circle" },
|
|
@@ -9,8 +9,10 @@ export async function render(ctx) {
|
|
|
9
9
|
const { h, icon, c } = ctx;
|
|
10
10
|
|
|
11
11
|
const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
|
|
12
|
+
const registryHost = h("div", c.loading({ lines: 3, block: true }));
|
|
12
13
|
const rosterHost = h("div", c.loading({ lines: 2, block: true }));
|
|
13
14
|
const runsHost = h("div", c.loading({ lines: 4 }));
|
|
15
|
+
const registrySrc = h("span", c.sourceBadge("pending"));
|
|
14
16
|
const rosterSrc = h("span", c.sourceBadge("pending"));
|
|
15
17
|
const runsSrc = h("span", c.sourceBadge("pending"));
|
|
16
18
|
const healthSlot = h("span", c.sourceBadge("pending"));
|
|
@@ -23,6 +25,10 @@ export async function render(ctx) {
|
|
|
23
25
|
actions: [healthSlot],
|
|
24
26
|
}),
|
|
25
27
|
statHost,
|
|
28
|
+
h("section",
|
|
29
|
+
c.sectionHead("Agent Registry", registrySrc),
|
|
30
|
+
registryHost,
|
|
31
|
+
),
|
|
26
32
|
h("section",
|
|
27
33
|
c.sectionHead("Agent roster", rosterSrc),
|
|
28
34
|
rosterHost,
|
|
@@ -40,6 +46,7 @@ export async function render(ctx) {
|
|
|
40
46
|
);
|
|
41
47
|
|
|
42
48
|
hydrate(ctx, { statHost, rosterHost, runsHost, rosterSrc, runsSrc, healthSlot });
|
|
49
|
+
loadRegistry(ctx, { registryHost, registrySrc });
|
|
43
50
|
return root;
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -109,6 +116,83 @@ async function hydrate(ctx, hosts) {
|
|
|
109
116
|
);
|
|
110
117
|
}
|
|
111
118
|
|
|
119
|
+
async function loadRegistry(ctx, hosts) {
|
|
120
|
+
const { h, c } = ctx;
|
|
121
|
+
const { registryHost, registrySrc } = hosts;
|
|
122
|
+
const [registryRes, capsRes] = await Promise.all([ctx.api.agentRegistry(), ctx.api.agentCapabilities()]);
|
|
123
|
+
const agents = normalizeRegistry(registryRes.data);
|
|
124
|
+
const caps = (capsRes.data && capsRes.data.capabilities) || {};
|
|
125
|
+
registrySrc.replaceChildren(c.sourceBadge(registryRes.source === "live" || capsRes.source === "live" ? "live" : "unavailable"));
|
|
126
|
+
|
|
127
|
+
const nameInput = h("input.lt3-input", { type: "text", placeholder: "Custom agent name", "aria-label": "Custom agent name" });
|
|
128
|
+
const capsInput = h("input.lt3-input", { type: "text", placeholder: "capability-a, capability-b", "aria-label": "Custom agent capabilities" });
|
|
129
|
+
const registerBtn = h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: register } }, c.icon("plus"), "Register");
|
|
130
|
+
|
|
131
|
+
const capList = Object.keys(caps).sort();
|
|
132
|
+
const body = h("div.lt3-stack-4",
|
|
133
|
+
h("div.lt3-grid-2",
|
|
134
|
+
h("div.lt3-field", h("label", "Name"), nameInput),
|
|
135
|
+
h("div.lt3-field", h("label", "Capabilities"), capsInput),
|
|
136
|
+
),
|
|
137
|
+
h("div.lt3-row-2", registerBtn,
|
|
138
|
+
h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } }, "Custom agents persist in the local registry.")),
|
|
139
|
+
capList.length
|
|
140
|
+
? h("div.lt3-cluster", capList.slice(0, 18).map((cap) => h("span.lt3-chip", c.icon("sparkles"), `${cap} (${caps[cap].length})`)))
|
|
141
|
+
: h("p.lt3-faint", { style: { margin: 0 } }, "Capabilities appear here when the registry is live."),
|
|
142
|
+
agents.length
|
|
143
|
+
? h("div.lt3-grid-auto", agents.map((agent) => registryCard(ctx, agent)))
|
|
144
|
+
: c.emptyState({ icon: "robot-off", title: "Agent registry unavailable", body: "Start the local server to register and configure agents." }),
|
|
145
|
+
);
|
|
146
|
+
registryHost.replaceChildren(c.panel({ title: "Registry controls", sub: "Register, discover, and configure built-in or custom agents.", children: body }));
|
|
147
|
+
|
|
148
|
+
async function register() {
|
|
149
|
+
const name = nameInput.value.trim();
|
|
150
|
+
if (!name) { ctx.toast("Enter an agent name", "info"); return; }
|
|
151
|
+
const capabilities = capsInput.value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
152
|
+
registerBtn.disabled = true;
|
|
153
|
+
const res = await ctx.api.registerAgent({ name, type: "custom", capabilities });
|
|
154
|
+
registerBtn.disabled = false;
|
|
155
|
+
if (res && res.ok) {
|
|
156
|
+
ctx.toast(`Registered ${name}`, "ok");
|
|
157
|
+
loadRegistry(ctx, hosts);
|
|
158
|
+
} else {
|
|
159
|
+
ctx.toast("Register unavailable", "err");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function registryCard(ctx, agent) {
|
|
165
|
+
const { h, c } = ctx;
|
|
166
|
+
return c.card(h("div.lt3-stack-3",
|
|
167
|
+
h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start" } },
|
|
168
|
+
h("div",
|
|
169
|
+
h("b", agent.name),
|
|
170
|
+
h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, agent.id),
|
|
171
|
+
),
|
|
172
|
+
c.pill(agent.source === "builtin" ? "built-in" : "custom", agent.source === "builtin" ? "info" : "warn"),
|
|
173
|
+
),
|
|
174
|
+
h("p.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)", margin: 0 } }, agent.description || "No description."),
|
|
175
|
+
h("div.lt3-cluster", [c.statePill(agent.enabled ? "ready" : "idle"), c.pill(agent.type), c.pill(`v${agent.version || "1.0.0"}`)]),
|
|
176
|
+
agent.capabilities.length ? h("div.lt3-cluster", agent.capabilities.slice(0, 8).map((cap) => h("span.lt3-chip", cap))) : null,
|
|
177
|
+
h("div.lt3-row-2",
|
|
178
|
+
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => toggleAgent(ctx, agent) } }, c.icon(agent.enabled ? "toggle-right" : "toggle-left"), agent.enabled ? "Disable" : "Enable"),
|
|
179
|
+
agent.removable ? h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => removeAgent(ctx, agent) } }, c.icon("trash"), "Remove") : null,
|
|
180
|
+
),
|
|
181
|
+
), { interactive: false });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function toggleAgent(ctx, agent) {
|
|
185
|
+
const res = await ctx.api.updateAgent(agent.id, { config: agent.config || {}, enabled: !agent.enabled });
|
|
186
|
+
ctx.toast(res && res.ok ? `${agent.name}: ${agent.enabled ? "disabled" : "enabled"}` : "Agent update unavailable", res && res.ok ? "ok" : "err");
|
|
187
|
+
if (res && res.ok) ctx.navigate("agents");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function removeAgent(ctx, agent) {
|
|
191
|
+
const res = await ctx.api.removeAgent(agent.id);
|
|
192
|
+
ctx.toast(res && res.ok ? `Removed ${agent.name}` : "Agent remove unavailable", res && res.ok ? "ok" : "err");
|
|
193
|
+
if (res && res.ok) ctx.navigate("agents");
|
|
194
|
+
}
|
|
195
|
+
|
|
112
196
|
/* ── Agent card ──────────────────────────────────────────────────────────── */
|
|
113
197
|
function agentCard(ctx, agent, byId) {
|
|
114
198
|
const { h, icon, c } = ctx;
|
|
@@ -158,6 +242,22 @@ function normalize(data) {
|
|
|
158
242
|
}));
|
|
159
243
|
}
|
|
160
244
|
|
|
245
|
+
function normalizeRegistry(data) {
|
|
246
|
+
const list = Array.isArray(data) ? data : (data && Array.isArray(data.agents) ? data.agents : []);
|
|
247
|
+
return list.map((agent, i) => ({
|
|
248
|
+
id: agent.id || `agent:${i}`,
|
|
249
|
+
name: agent.name || agent.id || `Agent ${i + 1}`,
|
|
250
|
+
type: agent.type || "custom",
|
|
251
|
+
version: agent.version || "1.0.0",
|
|
252
|
+
description: agent.description || "",
|
|
253
|
+
capabilities: Array.isArray(agent.capabilities) ? agent.capabilities : [],
|
|
254
|
+
source: agent.source || "user",
|
|
255
|
+
enabled: agent.enabled !== false,
|
|
256
|
+
removable: !!agent.removable,
|
|
257
|
+
config: agent.config || {},
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
|
|
161
261
|
const AVAILABLE_STATES = new Set(["available", "ready", "active", "ok", "idle"]);
|
|
162
262
|
function isAvailable(state) {
|
|
163
263
|
return AVAILABLE_STATES.has(String(state).toLowerCase());
|
|
@@ -9,8 +9,10 @@ export async function render(ctx) {
|
|
|
9
9
|
const { h, icon, c } = ctx;
|
|
10
10
|
|
|
11
11
|
const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
|
|
12
|
+
const registryHost = h("div", c.loading({ lines: 3, block: true }));
|
|
12
13
|
const rosterHost = h("div", c.loading({ lines: 2, block: true }));
|
|
13
14
|
const runsHost = h("div", c.loading({ lines: 4 }));
|
|
15
|
+
const registrySrc = h("span", c.sourceBadge("pending"));
|
|
14
16
|
const rosterSrc = h("span", c.sourceBadge("pending"));
|
|
15
17
|
const runsSrc = h("span", c.sourceBadge("pending"));
|
|
16
18
|
const healthSlot = h("span", c.sourceBadge("pending"));
|
|
@@ -23,6 +25,10 @@ export async function render(ctx) {
|
|
|
23
25
|
actions: [healthSlot],
|
|
24
26
|
}),
|
|
25
27
|
statHost,
|
|
28
|
+
h("section",
|
|
29
|
+
c.sectionHead("Agent Registry", registrySrc),
|
|
30
|
+
registryHost,
|
|
31
|
+
),
|
|
26
32
|
h("section",
|
|
27
33
|
c.sectionHead("Agent roster", rosterSrc),
|
|
28
34
|
rosterHost,
|
|
@@ -40,6 +46,7 @@ export async function render(ctx) {
|
|
|
40
46
|
);
|
|
41
47
|
|
|
42
48
|
hydrate(ctx, { statHost, rosterHost, runsHost, rosterSrc, runsSrc, healthSlot });
|
|
49
|
+
loadRegistry(ctx, { registryHost, registrySrc });
|
|
43
50
|
return root;
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -109,6 +116,83 @@ async function hydrate(ctx, hosts) {
|
|
|
109
116
|
);
|
|
110
117
|
}
|
|
111
118
|
|
|
119
|
+
async function loadRegistry(ctx, hosts) {
|
|
120
|
+
const { h, c } = ctx;
|
|
121
|
+
const { registryHost, registrySrc } = hosts;
|
|
122
|
+
const [registryRes, capsRes] = await Promise.all([ctx.api.agentRegistry(), ctx.api.agentCapabilities()]);
|
|
123
|
+
const agents = normalizeRegistry(registryRes.data);
|
|
124
|
+
const caps = (capsRes.data && capsRes.data.capabilities) || {};
|
|
125
|
+
registrySrc.replaceChildren(c.sourceBadge(registryRes.source === "live" || capsRes.source === "live" ? "live" : "unavailable"));
|
|
126
|
+
|
|
127
|
+
const nameInput = h("input.lt3-input", { type: "text", placeholder: "Custom agent name", "aria-label": "Custom agent name" });
|
|
128
|
+
const capsInput = h("input.lt3-input", { type: "text", placeholder: "capability-a, capability-b", "aria-label": "Custom agent capabilities" });
|
|
129
|
+
const registerBtn = h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: register } }, c.icon("plus"), "Register");
|
|
130
|
+
|
|
131
|
+
const capList = Object.keys(caps).sort();
|
|
132
|
+
const body = h("div.lt3-stack-4",
|
|
133
|
+
h("div.lt3-grid-2",
|
|
134
|
+
h("div.lt3-field", h("label", "Name"), nameInput),
|
|
135
|
+
h("div.lt3-field", h("label", "Capabilities"), capsInput),
|
|
136
|
+
),
|
|
137
|
+
h("div.lt3-row-2", registerBtn,
|
|
138
|
+
h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } }, "Custom agents persist in the local registry.")),
|
|
139
|
+
capList.length
|
|
140
|
+
? h("div.lt3-cluster", capList.slice(0, 18).map((cap) => h("span.lt3-chip", c.icon("sparkles"), `${cap} (${caps[cap].length})`)))
|
|
141
|
+
: h("p.lt3-faint", { style: { margin: 0 } }, "Capabilities appear here when the registry is live."),
|
|
142
|
+
agents.length
|
|
143
|
+
? h("div.lt3-grid-auto", agents.map((agent) => registryCard(ctx, agent)))
|
|
144
|
+
: c.emptyState({ icon: "robot-off", title: "Agent registry unavailable", body: "Start the local server to register and configure agents." }),
|
|
145
|
+
);
|
|
146
|
+
registryHost.replaceChildren(c.panel({ title: "Registry controls", sub: "Register, discover, and configure built-in or custom agents.", children: body }));
|
|
147
|
+
|
|
148
|
+
async function register() {
|
|
149
|
+
const name = nameInput.value.trim();
|
|
150
|
+
if (!name) { ctx.toast("Enter an agent name", "info"); return; }
|
|
151
|
+
const capabilities = capsInput.value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
152
|
+
registerBtn.disabled = true;
|
|
153
|
+
const res = await ctx.api.registerAgent({ name, type: "custom", capabilities });
|
|
154
|
+
registerBtn.disabled = false;
|
|
155
|
+
if (res && res.ok) {
|
|
156
|
+
ctx.toast(`Registered ${name}`, "ok");
|
|
157
|
+
loadRegistry(ctx, hosts);
|
|
158
|
+
} else {
|
|
159
|
+
ctx.toast("Register unavailable", "err");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function registryCard(ctx, agent) {
|
|
165
|
+
const { h, c } = ctx;
|
|
166
|
+
return c.card(h("div.lt3-stack-3",
|
|
167
|
+
h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start" } },
|
|
168
|
+
h("div",
|
|
169
|
+
h("b", agent.name),
|
|
170
|
+
h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, agent.id),
|
|
171
|
+
),
|
|
172
|
+
c.pill(agent.source === "builtin" ? "built-in" : "custom", agent.source === "builtin" ? "info" : "warn"),
|
|
173
|
+
),
|
|
174
|
+
h("p.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)", margin: 0 } }, agent.description || "No description."),
|
|
175
|
+
h("div.lt3-cluster", [c.statePill(agent.enabled ? "ready" : "idle"), c.pill(agent.type), c.pill(`v${agent.version || "1.0.0"}`)]),
|
|
176
|
+
agent.capabilities.length ? h("div.lt3-cluster", agent.capabilities.slice(0, 8).map((cap) => h("span.lt3-chip", cap))) : null,
|
|
177
|
+
h("div.lt3-row-2",
|
|
178
|
+
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => toggleAgent(ctx, agent) } }, c.icon(agent.enabled ? "toggle-right" : "toggle-left"), agent.enabled ? "Disable" : "Enable"),
|
|
179
|
+
agent.removable ? h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => removeAgent(ctx, agent) } }, c.icon("trash"), "Remove") : null,
|
|
180
|
+
),
|
|
181
|
+
), { interactive: false });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function toggleAgent(ctx, agent) {
|
|
185
|
+
const res = await ctx.api.updateAgent(agent.id, { config: agent.config || {}, enabled: !agent.enabled });
|
|
186
|
+
ctx.toast(res && res.ok ? `${agent.name}: ${agent.enabled ? "disabled" : "enabled"}` : "Agent update unavailable", res && res.ok ? "ok" : "err");
|
|
187
|
+
if (res && res.ok) ctx.navigate("agents");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function removeAgent(ctx, agent) {
|
|
191
|
+
const res = await ctx.api.removeAgent(agent.id);
|
|
192
|
+
ctx.toast(res && res.ok ? `Removed ${agent.name}` : "Agent remove unavailable", res && res.ok ? "ok" : "err");
|
|
193
|
+
if (res && res.ok) ctx.navigate("agents");
|
|
194
|
+
}
|
|
195
|
+
|
|
112
196
|
/* ── Agent card ──────────────────────────────────────────────────────────── */
|
|
113
197
|
function agentCard(ctx, agent, byId) {
|
|
114
198
|
const { h, icon, c } = ctx;
|
|
@@ -158,6 +242,22 @@ function normalize(data) {
|
|
|
158
242
|
}));
|
|
159
243
|
}
|
|
160
244
|
|
|
245
|
+
function normalizeRegistry(data) {
|
|
246
|
+
const list = Array.isArray(data) ? data : (data && Array.isArray(data.agents) ? data.agents : []);
|
|
247
|
+
return list.map((agent, i) => ({
|
|
248
|
+
id: agent.id || `agent:${i}`,
|
|
249
|
+
name: agent.name || agent.id || `Agent ${i + 1}`,
|
|
250
|
+
type: agent.type || "custom",
|
|
251
|
+
version: agent.version || "1.0.0",
|
|
252
|
+
description: agent.description || "",
|
|
253
|
+
capabilities: Array.isArray(agent.capabilities) ? agent.capabilities : [],
|
|
254
|
+
source: agent.source || "user",
|
|
255
|
+
enabled: agent.enabled !== false,
|
|
256
|
+
removable: !!agent.removable,
|
|
257
|
+
config: agent.config || {},
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
|
|
161
261
|
const AVAILABLE_STATES = new Set(["available", "ready", "active", "ok", "idle"]);
|
|
162
262
|
function isAvailable(state) {
|
|
163
263
|
return AVAILABLE_STATES.has(String(state).toLowerCase());
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* View: Hooks — the lifecycle hooks registry.
|
|
3
|
+
* Reads /api/hooks (built-in + user hooks across pre_run/post_run/pre_tool/
|
|
4
|
+
* post_tool/agent/pipeline/workflow), toggles enabled state, reorders, and
|
|
5
|
+
* registers custom hooks. Built-in hooks are platform-managed and labelled.
|
|
6
|
+
* ========================================================================== */
|
|
7
|
+
|
|
8
|
+
const KIND_LABEL = {
|
|
9
|
+
pre_run: "Pre-run", post_run: "Post-run", pre_tool: "Pre-tool", post_tool: "Post-tool",
|
|
10
|
+
agent: "Agent", pipeline: "Pipeline", workflow: "Workflow",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function render(ctx) {
|
|
14
|
+
const { h, c } = ctx;
|
|
15
|
+
const src = h("span", c.sourceBadge("pending"));
|
|
16
|
+
const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
|
|
17
|
+
const groupsHost = h("div", c.loading({ lines: 4, block: true }));
|
|
18
|
+
|
|
19
|
+
const nameInput = h("input.lt3-input", { type: "text", placeholder: "Hook name" });
|
|
20
|
+
const kindSelect = h("select.lt3-select", Object.keys(KIND_LABEL).map((k) => h("option", { value: k }, KIND_LABEL[k])));
|
|
21
|
+
const descInput = h("input.lt3-input", { type: "text", placeholder: "What it does (optional)" });
|
|
22
|
+
|
|
23
|
+
const root = h("div.lt3-stack-6",
|
|
24
|
+
c.viewHeader({
|
|
25
|
+
eyebrow: "Platform",
|
|
26
|
+
title: "Hooks",
|
|
27
|
+
sub: "Lifecycle extension points across runs, tools, agents, pipelines, and workflows — visible, ordered, and individually toggleable.",
|
|
28
|
+
actions: [src],
|
|
29
|
+
}),
|
|
30
|
+
statHost,
|
|
31
|
+
c.panel({
|
|
32
|
+
title: "Register a hook", sub: "Custom hooks are listed, ordered, and inspectable.",
|
|
33
|
+
children: h("div.lt3-stack-3",
|
|
34
|
+
h("div.lt3-grid-2", h("div.lt3-field", h("label", "Name"), nameInput), h("div.lt3-field", h("label", "Kind"), kindSelect)),
|
|
35
|
+
h("div.lt3-field", h("label", "Description"), descInput),
|
|
36
|
+
h("div.lt3-row-2", h("button.lt3-btn.lt3-btn--primary", { on: { click: register } }, c.icon("plus"), "Register hook")),
|
|
37
|
+
),
|
|
38
|
+
}),
|
|
39
|
+
groupsHost,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
load();
|
|
43
|
+
return root;
|
|
44
|
+
|
|
45
|
+
async function load() {
|
|
46
|
+
const res = await ctx.api.hooks();
|
|
47
|
+
src.replaceChildren(c.sourceBadge(res.source));
|
|
48
|
+
const hooks = (res.data && res.data.hooks) || [];
|
|
49
|
+
if (!hooks.length) {
|
|
50
|
+
statHost.replaceChildren(c.stat({ label: "Hooks", value: "—", icon: "webhook" }));
|
|
51
|
+
groupsHost.replaceChildren(c.emptyState({ icon: "webhook-off", title: "Hooks unavailable", body: "Start the backend to read the hooks registry." }));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const en = hooks.filter((x) => x.enabled).length;
|
|
55
|
+
statHost.replaceChildren(
|
|
56
|
+
c.stat({ label: "Hooks", value: c.fmtNum(hooks.length), icon: "webhook" }),
|
|
57
|
+
c.stat({ label: "Enabled", value: c.fmtNum(en), icon: "circle-check" }),
|
|
58
|
+
c.stat({ label: "Kinds", value: c.fmtNum((res.data.kinds || []).length), icon: "layers" }),
|
|
59
|
+
);
|
|
60
|
+
const byKind = {};
|
|
61
|
+
for (const hk of hooks) (byKind[hk.kind] = byKind[hk.kind] || []).push(hk);
|
|
62
|
+
groupsHost.replaceChildren(h("div.lt3-stack-6", Object.keys(byKind).map((kind) =>
|
|
63
|
+
h("section", c.sectionHead(KIND_LABEL[kind] || kind, c.pill(String(byKind[kind].length))),
|
|
64
|
+
h("div.lt3-stack-2", byKind[kind].map((hk) => hookRow(ctx, hk)))))));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hookRow(ctx2, hk) {
|
|
68
|
+
return c.card(h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "center", gap: "var(--lt3-space-3)" } },
|
|
69
|
+
h("div", { style: { "min-width": 0 } },
|
|
70
|
+
h("div.lt3-row-2", h("b", hk.name), c.pill(hk.source === "builtin" ? "built-in" : "custom", hk.source === "builtin" ? "info" : ""), hk.managed === "platform" ? c.pill("managed", "") : null),
|
|
71
|
+
h("p.lt3-muted", { style: { margin: "2px 0 0", "font-size": "var(--lt3-text-sm)" } }, hk.description || ""),
|
|
72
|
+
hk.binding ? h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, hk.binding) : null,
|
|
73
|
+
),
|
|
74
|
+
h("div.lt3-row-2", { style: { "flex-shrink": 0 } },
|
|
75
|
+
h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } }, `#${hk.order}`),
|
|
76
|
+
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => toggle(ctx2, hk) } }, c.icon(hk.enabled ? "toggle-right" : "toggle-left"), hk.enabled ? "On" : "Off"),
|
|
77
|
+
hk.removable ? h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => remove(ctx2, hk) } }, c.icon("trash")) : null,
|
|
78
|
+
),
|
|
79
|
+
), { flat: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function toggle(ctx2, hk) {
|
|
83
|
+
const res = hk.enabled ? await ctx2.api.hookDisable(hk.id) : await ctx2.api.hookEnable(hk.id, true);
|
|
84
|
+
ctx2.toast(res && res.ok ? `${hk.name}: ${hk.enabled ? "disabled" : "enabled"}` : "Action unavailable", res && res.ok ? "ok" : "err");
|
|
85
|
+
load();
|
|
86
|
+
}
|
|
87
|
+
async function remove(ctx2, hk) {
|
|
88
|
+
const res = await ctx2.api.hookRemove(hk.id);
|
|
89
|
+
ctx2.toast(res && res.ok ? `Removed ${hk.name}` : "Remove unavailable", res && res.ok ? "ok" : "err");
|
|
90
|
+
load();
|
|
91
|
+
}
|
|
92
|
+
async function register() {
|
|
93
|
+
const name = nameInput.value.trim();
|
|
94
|
+
if (!name) { ctx.toast("Enter a hook name", "info"); return; }
|
|
95
|
+
const res = await ctx.api.hookRegister({ name, kind: kindSelect.value, description: descInput.value.trim() });
|
|
96
|
+
if (res && res.ok) { ctx.toast(`Registered ${name}`, "ok"); nameInput.value = ""; descInput.value = ""; load(); }
|
|
97
|
+
else { ctx.toast("Register unavailable", "err"); }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* View: Hooks — the lifecycle hooks registry.
|
|
3
|
+
* Reads /api/hooks (built-in + user hooks across pre_run/post_run/pre_tool/
|
|
4
|
+
* post_tool/agent/pipeline/workflow), toggles enabled state, reorders, and
|
|
5
|
+
* registers custom hooks. Built-in hooks are platform-managed and labelled.
|
|
6
|
+
* ========================================================================== */
|
|
7
|
+
|
|
8
|
+
const KIND_LABEL = {
|
|
9
|
+
pre_run: "Pre-run", post_run: "Post-run", pre_tool: "Pre-tool", post_tool: "Post-tool",
|
|
10
|
+
agent: "Agent", pipeline: "Pipeline", workflow: "Workflow",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function render(ctx) {
|
|
14
|
+
const { h, c } = ctx;
|
|
15
|
+
const src = h("span", c.sourceBadge("pending"));
|
|
16
|
+
const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
|
|
17
|
+
const groupsHost = h("div", c.loading({ lines: 4, block: true }));
|
|
18
|
+
|
|
19
|
+
const nameInput = h("input.lt3-input", { type: "text", placeholder: "Hook name" });
|
|
20
|
+
const kindSelect = h("select.lt3-select", Object.keys(KIND_LABEL).map((k) => h("option", { value: k }, KIND_LABEL[k])));
|
|
21
|
+
const descInput = h("input.lt3-input", { type: "text", placeholder: "What it does (optional)" });
|
|
22
|
+
|
|
23
|
+
const root = h("div.lt3-stack-6",
|
|
24
|
+
c.viewHeader({
|
|
25
|
+
eyebrow: "Platform",
|
|
26
|
+
title: "Hooks",
|
|
27
|
+
sub: "Lifecycle extension points across runs, tools, agents, pipelines, and workflows — visible, ordered, and individually toggleable.",
|
|
28
|
+
actions: [src],
|
|
29
|
+
}),
|
|
30
|
+
statHost,
|
|
31
|
+
c.panel({
|
|
32
|
+
title: "Register a hook", sub: "Custom hooks are listed, ordered, and inspectable.",
|
|
33
|
+
children: h("div.lt3-stack-3",
|
|
34
|
+
h("div.lt3-grid-2", h("div.lt3-field", h("label", "Name"), nameInput), h("div.lt3-field", h("label", "Kind"), kindSelect)),
|
|
35
|
+
h("div.lt3-field", h("label", "Description"), descInput),
|
|
36
|
+
h("div.lt3-row-2", h("button.lt3-btn.lt3-btn--primary", { on: { click: register } }, c.icon("plus"), "Register hook")),
|
|
37
|
+
),
|
|
38
|
+
}),
|
|
39
|
+
groupsHost,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
load();
|
|
43
|
+
return root;
|
|
44
|
+
|
|
45
|
+
async function load() {
|
|
46
|
+
const res = await ctx.api.hooks();
|
|
47
|
+
src.replaceChildren(c.sourceBadge(res.source));
|
|
48
|
+
const hooks = (res.data && res.data.hooks) || [];
|
|
49
|
+
if (!hooks.length) {
|
|
50
|
+
statHost.replaceChildren(c.stat({ label: "Hooks", value: "—", icon: "webhook" }));
|
|
51
|
+
groupsHost.replaceChildren(c.emptyState({ icon: "webhook-off", title: "Hooks unavailable", body: "Start the backend to read the hooks registry." }));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const en = hooks.filter((x) => x.enabled).length;
|
|
55
|
+
statHost.replaceChildren(
|
|
56
|
+
c.stat({ label: "Hooks", value: c.fmtNum(hooks.length), icon: "webhook" }),
|
|
57
|
+
c.stat({ label: "Enabled", value: c.fmtNum(en), icon: "circle-check" }),
|
|
58
|
+
c.stat({ label: "Kinds", value: c.fmtNum((res.data.kinds || []).length), icon: "layers" }),
|
|
59
|
+
);
|
|
60
|
+
const byKind = {};
|
|
61
|
+
for (const hk of hooks) (byKind[hk.kind] = byKind[hk.kind] || []).push(hk);
|
|
62
|
+
groupsHost.replaceChildren(h("div.lt3-stack-6", Object.keys(byKind).map((kind) =>
|
|
63
|
+
h("section", c.sectionHead(KIND_LABEL[kind] || kind, c.pill(String(byKind[kind].length))),
|
|
64
|
+
h("div.lt3-stack-2", byKind[kind].map((hk) => hookRow(ctx, hk)))))));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hookRow(ctx2, hk) {
|
|
68
|
+
return c.card(h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "center", gap: "var(--lt3-space-3)" } },
|
|
69
|
+
h("div", { style: { "min-width": 0 } },
|
|
70
|
+
h("div.lt3-row-2", h("b", hk.name), c.pill(hk.source === "builtin" ? "built-in" : "custom", hk.source === "builtin" ? "info" : ""), hk.managed === "platform" ? c.pill("managed", "") : null),
|
|
71
|
+
h("p.lt3-muted", { style: { margin: "2px 0 0", "font-size": "var(--lt3-text-sm)" } }, hk.description || ""),
|
|
72
|
+
hk.binding ? h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, hk.binding) : null,
|
|
73
|
+
),
|
|
74
|
+
h("div.lt3-row-2", { style: { "flex-shrink": 0 } },
|
|
75
|
+
h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } }, `#${hk.order}`),
|
|
76
|
+
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => toggle(ctx2, hk) } }, c.icon(hk.enabled ? "toggle-right" : "toggle-left"), hk.enabled ? "On" : "Off"),
|
|
77
|
+
hk.removable ? h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => remove(ctx2, hk) } }, c.icon("trash")) : null,
|
|
78
|
+
),
|
|
79
|
+
), { flat: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function toggle(ctx2, hk) {
|
|
83
|
+
const res = hk.enabled ? await ctx2.api.hookDisable(hk.id) : await ctx2.api.hookEnable(hk.id, true);
|
|
84
|
+
ctx2.toast(res && res.ok ? `${hk.name}: ${hk.enabled ? "disabled" : "enabled"}` : "Action unavailable", res && res.ok ? "ok" : "err");
|
|
85
|
+
load();
|
|
86
|
+
}
|
|
87
|
+
async function remove(ctx2, hk) {
|
|
88
|
+
const res = await ctx2.api.hookRemove(hk.id);
|
|
89
|
+
ctx2.toast(res && res.ok ? `Removed ${hk.name}` : "Remove unavailable", res && res.ok ? "ok" : "err");
|
|
90
|
+
load();
|
|
91
|
+
}
|
|
92
|
+
async function register() {
|
|
93
|
+
const name = nameInput.value.trim();
|
|
94
|
+
if (!name) { ctx.toast("Enter a hook name", "info"); return; }
|
|
95
|
+
const res = await ctx.api.hookRegister({ name, kind: kindSelect.value, description: descInput.value.trim() });
|
|
96
|
+
if (res && res.ok) { ctx.toast(`Registered ${name}`, "ok"); nameInput.value = ""; descInput.value = ""; load(); }
|
|
97
|
+
else { ctx.toast("Register unavailable", "err"); }
|
|
98
|
+
}
|
|
99
|
+
}
|