ltcai 1.7.0 → 2.0.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 +31 -21
- package/docs/CHANGELOG.md +65 -0
- package/docs/EDITION_STRATEGY.md +10 -4
- package/docs/ENTERPRISE.md +3 -1
- package/docs/MULTI_AGENT_RUNTIME.md +410 -0
- package/docs/PLUGIN_SDK.md +651 -0
- package/docs/REALTIME_COLLABORATION.md +410 -0
- package/docs/V2_ARCHITECTURE.md +528 -0
- package/docs/WORKFLOW_DESIGNER.md +475 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +98 -0
- package/latticeai/api/plugins.py +115 -0
- package/latticeai/api/realtime.py +91 -0
- package/latticeai/api/workflow_designer.py +207 -0
- package/latticeai/core/multi_agent.py +270 -0
- package/latticeai/core/plugins.py +400 -0
- package/latticeai/core/realtime.py +190 -0
- package/latticeai/core/workflow_engine.py +329 -0
- package/latticeai/core/workspace_os.py +155 -2
- package/latticeai/server_app.py +76 -2
- package/latticeai/services/platform_runtime.py +200 -0
- package/package.json +8 -2
- package/plugins/README.md +35 -0
- package/plugins/git-insights/plugin.json +15 -0
- package/plugins/hello-world/plugin.json +16 -0
- package/plugins/hello-world/skills/hello_skill/SKILL.md +15 -0
- package/static/activity.html +70 -0
- package/static/agents.html +92 -0
- package/static/platform.css +75 -0
- package/static/plugins.html +82 -0
- package/static/scripts/platform.js +64 -0
- package/static/workflows.html +121 -0
- package/static/workspace.html +5 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""v2.0 Agentic Workspace Platform runtime — cross-system wiring.
|
|
2
|
+
|
|
3
|
+
This is the single place the four v2.0 subsystems (Plugin SDK, Workflow
|
|
4
|
+
Designer, Multi-Agent Runtime, Realtime) connect to one another and to the
|
|
5
|
+
workspace. Keeping it out of ``server_app`` honours the AGENTS.md preference for
|
|
6
|
+
small, composable modules and keeps the wiring independently testable.
|
|
7
|
+
|
|
8
|
+
Recursion is bounded by construction: a workflow's ``agent`` node runs an
|
|
9
|
+
orchestrator *without* a workflow runner, and an orchestrator's workflow runner
|
|
10
|
+
runs an engine *without* an ``agent`` runner — so the deepest chains are
|
|
11
|
+
``agent → workflow → (tool|skill|plugin)`` and ``workflow → agent → plugin``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, Callable, Dict, Optional, Set
|
|
17
|
+
|
|
18
|
+
from fastapi import HTTPException, Request
|
|
19
|
+
|
|
20
|
+
from latticeai.core.multi_agent import MultiAgentOrchestrator, default_role_runner
|
|
21
|
+
from latticeai.core.workflow_engine import WorkflowEngine
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PlatformRuntime:
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
store,
|
|
29
|
+
workspace_service,
|
|
30
|
+
plugin_registry,
|
|
31
|
+
get_current_user: Callable[[Request], Optional[str]],
|
|
32
|
+
workspace_graph: Callable[[], Any],
|
|
33
|
+
workspace_scope_from_request: Callable[[Request], Optional[str]],
|
|
34
|
+
get_tool_permission: Callable[..., Dict[str, Any]],
|
|
35
|
+
):
|
|
36
|
+
self.store = store
|
|
37
|
+
self.svc = workspace_service
|
|
38
|
+
self.registry = plugin_registry
|
|
39
|
+
self.get_current_user = get_current_user
|
|
40
|
+
self.workspace_graph = workspace_graph
|
|
41
|
+
self.scope_from_request = workspace_scope_from_request
|
|
42
|
+
self.get_tool_permission = get_tool_permission
|
|
43
|
+
|
|
44
|
+
# ── request gating ────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
def gate_read(self, request: Request) -> Optional[str]:
|
|
47
|
+
try:
|
|
48
|
+
return self.svc.resolve_read_scope(self.scope_from_request(request), self.get_current_user(request))
|
|
49
|
+
except PermissionError as exc:
|
|
50
|
+
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
51
|
+
|
|
52
|
+
def gate_write(self, request: Request) -> Optional[str]:
|
|
53
|
+
try:
|
|
54
|
+
return self.svc.resolve_write_scope(self.scope_from_request(request), self.get_current_user(request))
|
|
55
|
+
except PermissionError as exc:
|
|
56
|
+
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
57
|
+
|
|
58
|
+
def allowed_scopes(self, user: Optional[str]) -> Optional[Set[str]]:
|
|
59
|
+
try:
|
|
60
|
+
workspaces = self.svc.list_workspaces(user or None).get("workspaces", [])
|
|
61
|
+
return {ws.get("workspace_id") for ws in workspaces if ws.get("workspace_id")}
|
|
62
|
+
except Exception:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
# ── plugin lifecycle hooks ────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
def register_plugin_skill(self, skill_name: str, plugin_id: str):
|
|
68
|
+
return self.store.mark_skill_installed(
|
|
69
|
+
skill_name, version=f"plugin:{plugin_id}", metadata={"source": f"plugin:{plugin_id}"}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# ── shared node runners ───────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def _tool_node_runner(self):
|
|
75
|
+
"""Workflow tool node: records the invocation + governance decision but
|
|
76
|
+
never silently executes exec/destructive tools (those need approval)."""
|
|
77
|
+
def runner(*, node, context):
|
|
78
|
+
cfg = node.get("config") or {}
|
|
79
|
+
name = cfg.get("tool") or ""
|
|
80
|
+
try:
|
|
81
|
+
permission = dict(self.get_tool_permission(name))
|
|
82
|
+
except Exception:
|
|
83
|
+
permission = {"tool": name, "risk": "unknown"}
|
|
84
|
+
return {"tool": name, "args": cfg.get("args") or {}, "recorded": True, "permission": permission}
|
|
85
|
+
return runner
|
|
86
|
+
|
|
87
|
+
def _skill_node_runner(self):
|
|
88
|
+
def runner(*, node, context):
|
|
89
|
+
cfg = node.get("config") or {}
|
|
90
|
+
name = cfg.get("skill") or ""
|
|
91
|
+
entry = self.store.load_state().get("skill_registry", {}).get(name) or {}
|
|
92
|
+
return {"skill": name, "found": bool(entry), "enabled": bool(entry.get("enabled"))}
|
|
93
|
+
return runner
|
|
94
|
+
|
|
95
|
+
def _context_provider(self, user, scope):
|
|
96
|
+
def provider(goal: str):
|
|
97
|
+
try:
|
|
98
|
+
mems = self.store.search_memories(goal, user_email=user, workspace_id=scope).get("memories", [])
|
|
99
|
+
return [str(m.get("content") or "")[:160] for m in mems[:5]]
|
|
100
|
+
except Exception:
|
|
101
|
+
return []
|
|
102
|
+
return provider
|
|
103
|
+
|
|
104
|
+
def plugin_capability_runners(self, user, scope) -> Dict[str, Callable[..., Any]]:
|
|
105
|
+
"""Runners the Plugin SDK boundary dispatches to (one per capability)."""
|
|
106
|
+
def run_skill(*, plugin_id, action, args, manifest):
|
|
107
|
+
return {"plugin": plugin_id, "ran_skills": manifest.provides.get("skills", [])}
|
|
108
|
+
|
|
109
|
+
def run_tool(*, plugin_id, action, args, manifest):
|
|
110
|
+
tool = args.get("tool") or (manifest.provides.get("tools") or [None])[0]
|
|
111
|
+
try:
|
|
112
|
+
permission = dict(self.get_tool_permission(tool)) if tool else {}
|
|
113
|
+
except Exception:
|
|
114
|
+
permission = {}
|
|
115
|
+
return {"plugin": plugin_id, "tool": tool, "permission": permission, "recorded": True}
|
|
116
|
+
|
|
117
|
+
def run_workflow(*, plugin_id, action, args, manifest):
|
|
118
|
+
wf_id = args.get("workflow_id")
|
|
119
|
+
if not wf_id:
|
|
120
|
+
return {"plugin": plugin_id, "skipped": "no workflow_id"}
|
|
121
|
+
return self.run_workflow_by_id(wf_id, user, scope, with_agent=False, inputs=args.get("inputs"))
|
|
122
|
+
|
|
123
|
+
def run_agent(*, plugin_id, action, args, manifest):
|
|
124
|
+
goal = args.get("goal") or f"Plugin {plugin_id} agent task"
|
|
125
|
+
return self.run_agent(goal, user, scope, with_workflow=False, inputs=args.get("inputs"))
|
|
126
|
+
|
|
127
|
+
return {"skills": run_skill, "tools": run_tool, "workflows": run_workflow, "agents": run_agent}
|
|
128
|
+
|
|
129
|
+
def _plugin_node_runner(self, user, scope):
|
|
130
|
+
def runner(*, node, context):
|
|
131
|
+
cfg = node.get("config") or {}
|
|
132
|
+
plugin_id = cfg.get("plugin_id") or cfg.get("plugin") or ""
|
|
133
|
+
action = cfg.get("action") or "run_skill"
|
|
134
|
+
result = self.registry.execute_action(
|
|
135
|
+
plugin_id, action, cfg.get("args") or {}, runners=self.plugin_capability_runners(user, scope)
|
|
136
|
+
)
|
|
137
|
+
return result.as_dict()
|
|
138
|
+
return runner
|
|
139
|
+
|
|
140
|
+
def _agent_node_runner(self, user, scope):
|
|
141
|
+
def runner(*, node, context):
|
|
142
|
+
cfg = node.get("config") or {}
|
|
143
|
+
goal = cfg.get("goal") or context.get("goal") or "Run agent"
|
|
144
|
+
return self.run_agent(goal, user, scope, with_workflow=False, roles=cfg.get("roles"), inputs=context.get("inputs"))
|
|
145
|
+
return runner
|
|
146
|
+
|
|
147
|
+
# ── cross-system runs ─────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
def run_workflow_by_id(self, workflow_id, user, scope, *, with_agent: bool, inputs=None) -> Dict[str, Any]:
|
|
150
|
+
try:
|
|
151
|
+
workflow = self.store.get_workflow(workflow_id, workspace_id=scope)
|
|
152
|
+
except FileNotFoundError:
|
|
153
|
+
return {"error": f"workflow not found: {workflow_id}"}
|
|
154
|
+
runners = {
|
|
155
|
+
"tool": self._tool_node_runner(),
|
|
156
|
+
"skill": self._skill_node_runner(),
|
|
157
|
+
"plugin": self._plugin_node_runner(user, scope),
|
|
158
|
+
}
|
|
159
|
+
if with_agent:
|
|
160
|
+
runners["agent"] = self._agent_node_runner(user, scope)
|
|
161
|
+
result = WorkflowEngine(runners).run(workflow, inputs=inputs or {})
|
|
162
|
+
run = self.store.record_workflow_run(
|
|
163
|
+
workflow_id=workflow_id, name=workflow.get("name") or "workflow",
|
|
164
|
+
status=result.status, timeline=result.timeline, outputs=result.outputs,
|
|
165
|
+
user_email=user, graph=self.workspace_graph(), workspace_id=scope,
|
|
166
|
+
)
|
|
167
|
+
return {"workflow_run_id": run["id"], "status": result.status}
|
|
168
|
+
|
|
169
|
+
def run_agent(self, goal, user, scope, *, with_workflow: bool, roles=None, inputs=None) -> Dict[str, Any]:
|
|
170
|
+
role_runner = default_role_runner(
|
|
171
|
+
workflow_runner=(lambda wf_ref, ctx: self.run_workflow_by_id(wf_ref, user, scope, with_agent=False, inputs=ctx.inputs)) if with_workflow else None,
|
|
172
|
+
plugin_runner=lambda pid, ctx: self.registry.execute_action(pid, "run_skill", {}, runners=self.plugin_capability_runners(user, scope)).as_dict(),
|
|
173
|
+
context_provider=self._context_provider(user, scope),
|
|
174
|
+
)
|
|
175
|
+
result = MultiAgentOrchestrator(role_runner=role_runner).run(
|
|
176
|
+
goal, user_email=user, workspace_id=scope, roles=roles, inputs=inputs or {}
|
|
177
|
+
)
|
|
178
|
+
run = self.store.record_agent_run(
|
|
179
|
+
agent_id=result.agent_id, status=result.status, input_text=goal,
|
|
180
|
+
output_text=result.output, timeline=result.timeline, relationships=[],
|
|
181
|
+
user_email=user, graph=self.workspace_graph(), workspace_id=scope,
|
|
182
|
+
)
|
|
183
|
+
return {"agent_run_id": run["id"], "status": result.status, "output": result.output}
|
|
184
|
+
|
|
185
|
+
# ── factories passed to routers ───────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
def build_workflow_runners(self, user, scope) -> Dict[str, Callable[..., Any]]:
|
|
188
|
+
return {
|
|
189
|
+
"tool": self._tool_node_runner(),
|
|
190
|
+
"skill": self._skill_node_runner(),
|
|
191
|
+
"plugin": self._plugin_node_runner(user, scope),
|
|
192
|
+
"agent": self._agent_node_runner(user, scope),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
def build_orchestrator(self, user, scope) -> MultiAgentOrchestrator:
|
|
196
|
+
return MultiAgentOrchestrator(role_runner=default_role_runner(
|
|
197
|
+
workflow_runner=lambda wf_ref, ctx: self.run_workflow_by_id(wf_ref, user, scope, with_agent=False, inputs=ctx.inputs),
|
|
198
|
+
plugin_runner=lambda pid, ctx: self.registry.execute_action(pid, "run_skill", {}, runners=self.plugin_capability_runners(user, scope)).as_dict(),
|
|
199
|
+
context_provider=self._context_provider(user, scope),
|
|
200
|
+
))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Lattice AI Workspace OS for local-first graph, memory, agent, workflow, and skill operations",
|
|
5
5
|
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
|
|
6
6
|
"repository": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dev": "python3 ltcai_cli.py --reload",
|
|
20
20
|
"build": "npm run build:python",
|
|
21
21
|
"build:python": "python3 -m build",
|
|
22
|
-
"check:python": "python3 -m py_compile ltcai_cli.py server.py latticeai/server_app.py latticeai/api/chat.py latticeai/api/computer_use.py latticeai/api/deps.py latticeai/api/garden.py latticeai/api/local_files.py latticeai/api/permissions.py latticeai/api/setup.py latticeai/api/static_routes.py latticeai/api/tools.py latticeai/services/app_context.py latticeai/services/model_runtime.py latticeai/services/model_catalog.py latticeai/services/model_recommendation.py latticeai/services/tool_dispatch.py latticeai/services/upload_service.py latticeai/core/tool_registry.py latticeai/core/enterprise.py latticeai/core/enterprise_admin.py latticeai/core/agent_prompts.py latticeai/core/workspace_os.py knowledge_graph.py knowledge_graph_api.py local_knowledge_api.py llm_router.py p_reinforce.py telegram_bot.py tools.py codex_telegram_bot.py",
|
|
22
|
+
"check:python": "python3 -m py_compile ltcai_cli.py server.py latticeai/server_app.py latticeai/api/chat.py latticeai/api/computer_use.py latticeai/api/deps.py latticeai/api/garden.py latticeai/api/local_files.py latticeai/api/permissions.py latticeai/api/setup.py latticeai/api/static_routes.py latticeai/api/tools.py latticeai/api/plugins.py latticeai/api/workflow_designer.py latticeai/api/agents.py latticeai/api/realtime.py latticeai/services/app_context.py latticeai/services/model_runtime.py latticeai/services/model_catalog.py latticeai/services/model_recommendation.py latticeai/services/tool_dispatch.py latticeai/services/upload_service.py latticeai/core/tool_registry.py latticeai/core/enterprise.py latticeai/core/enterprise_admin.py latticeai/core/agent_prompts.py latticeai/core/workspace_os.py latticeai/core/plugins.py latticeai/core/workflow_engine.py latticeai/core/multi_agent.py latticeai/core/realtime.py knowledge_graph.py knowledge_graph_api.py local_knowledge_api.py llm_router.py p_reinforce.py telegram_bot.py tools.py codex_telegram_bot.py",
|
|
23
23
|
"test": "python3 -m pytest tests/ -v",
|
|
24
24
|
"test:unit": "python3 -m pytest tests/unit/ -v",
|
|
25
25
|
"test:integration": "python3 -m pytest tests/integration/ -v",
|
|
@@ -68,13 +68,19 @@
|
|
|
68
68
|
"static/admin.html",
|
|
69
69
|
"static/graph.html",
|
|
70
70
|
"static/workspace.html",
|
|
71
|
+
"static/plugins.html",
|
|
72
|
+
"static/workflows.html",
|
|
73
|
+
"static/agents.html",
|
|
74
|
+
"static/activity.html",
|
|
71
75
|
"static/manifest.json",
|
|
72
76
|
"static/sw.js",
|
|
73
77
|
"static/lattice-reference.css",
|
|
74
78
|
"static/workspace.css",
|
|
79
|
+
"static/platform.css",
|
|
75
80
|
"static/scripts/",
|
|
76
81
|
"static/css/",
|
|
77
82
|
"static/icons/",
|
|
83
|
+
"plugins/",
|
|
78
84
|
"docs/",
|
|
79
85
|
"requirements.txt",
|
|
80
86
|
"README.md"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Lattice AI Plugins
|
|
2
|
+
|
|
3
|
+
This directory holds **Plugin SDK** packages discovered by the Lattice AI server
|
|
4
|
+
at runtime. Each plugin is a folder containing a `plugin.json` manifest.
|
|
5
|
+
|
|
6
|
+
A plugin is an additive, versioned, permissioned unit that **extends** the
|
|
7
|
+
existing Skill / Tool / Workflow surfaces — it never replaces them. Installed
|
|
8
|
+
standalone skills keep working unchanged.
|
|
9
|
+
|
|
10
|
+
See [`docs/PLUGIN_SDK.md`](../docs/PLUGIN_SDK.md) for the full manifest schema,
|
|
11
|
+
permission model, lifecycle, and execution boundary.
|
|
12
|
+
|
|
13
|
+
## Bundled examples
|
|
14
|
+
|
|
15
|
+
| Plugin | What it shows |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `hello-world` | Bundling a skill + a workflow template + a declarative action |
|
|
18
|
+
| `git-insights` | Declaring `run_tools` and surfacing read-only git insights through the permission boundary |
|
|
19
|
+
|
|
20
|
+
## Minimal manifest
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"id": "my-plugin",
|
|
25
|
+
"name": "My Plugin",
|
|
26
|
+
"version": "1.0.0",
|
|
27
|
+
"description": "What it does.",
|
|
28
|
+
"lattice_version": ">=2.0.0",
|
|
29
|
+
"permissions": ["read_workspace"],
|
|
30
|
+
"provides": { "skills": [], "tools": [], "workflows": [], "actions": [] }
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Permissions must be drawn from the SDK allow-list; the execution boundary
|
|
35
|
+
refuses any capability a plugin did not declare and was not granted at install.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "git-insights",
|
|
3
|
+
"name": "Git Insights",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Example plugin that surfaces read-only git status and log insights through the permissioned tool execution boundary.",
|
|
6
|
+
"author": "Lattice AI",
|
|
7
|
+
"lattice_version": ">=2.0.0",
|
|
8
|
+
"permissions": ["read_workspace", "run_tools"],
|
|
9
|
+
"provides": {
|
|
10
|
+
"tools": ["git_status", "git_log"],
|
|
11
|
+
"actions": ["summarize_repo"]
|
|
12
|
+
},
|
|
13
|
+
"entrypoint": "",
|
|
14
|
+
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI/blob/main/docs/PLUGIN_SDK.md"
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "hello-world",
|
|
3
|
+
"name": "Hello World",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Example plugin demonstrating the Lattice AI Plugin SDK: bundles a skill, a workflow template, and a greet action.",
|
|
6
|
+
"author": "Lattice AI",
|
|
7
|
+
"lattice_version": "2.0.0",
|
|
8
|
+
"permissions": ["read_workspace", "run_skills"],
|
|
9
|
+
"provides": {
|
|
10
|
+
"skills": ["hello_skill"],
|
|
11
|
+
"workflows": ["hello-workflow"],
|
|
12
|
+
"actions": ["greet"]
|
|
13
|
+
},
|
|
14
|
+
"entrypoint": "",
|
|
15
|
+
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI/blob/main/docs/PLUGIN_SDK.md"
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hello_skill
|
|
3
|
+
description: A minimal example skill bundled by the hello-world plugin to show that plugins extend (not replace) the existing skill system.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Hello Skill
|
|
7
|
+
|
|
8
|
+
This skill is contributed by the `hello-world` plugin. When the plugin is
|
|
9
|
+
installed, the Plugin SDK registers this skill into the existing Workspace skill
|
|
10
|
+
registry via the same `mark_skill_installed` path used by standalone skills —
|
|
11
|
+
demonstrating that plugins are an additive layer on top of skills.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Ask Lattice AI to greet a workspace member, or invoke the plugin action `greet`.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Realtime Activity — Lattice AI</title>
|
|
7
|
+
<link rel="stylesheet" href="/static/platform.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main>
|
|
11
|
+
<h1>Realtime Activity</h1>
|
|
12
|
+
<p class="sub" id="sub">Live presence + activity feed across workspace, graph, agent, and workflow events (SSE).</p>
|
|
13
|
+
|
|
14
|
+
<div class="row">
|
|
15
|
+
<span class="badge" id="connBadge">connecting…</span>
|
|
16
|
+
<span class="badge" id="presenceBadge">presence: 0</span>
|
|
17
|
+
<div class="spacer"></div>
|
|
18
|
+
<button class="ghost" id="clearBtn">Clear view</button>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="section">
|
|
22
|
+
<h3>Live feed</h3>
|
|
23
|
+
<div id="feed"><div class="empty">Waiting for events…</div></div>
|
|
24
|
+
</div>
|
|
25
|
+
</main>
|
|
26
|
+
|
|
27
|
+
<script type="module">
|
|
28
|
+
import { mountHeader, api, escapeHtml, badge } from "/static/scripts/platform.js";
|
|
29
|
+
mountHeader("/activity");
|
|
30
|
+
|
|
31
|
+
const feed = document.getElementById("feed");
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
|
|
34
|
+
function renderEvent(ev) {
|
|
35
|
+
if (ev.seq && seen.has(ev.seq)) return;
|
|
36
|
+
if (ev.seq) seen.add(ev.seq);
|
|
37
|
+
if (feed.querySelector(".empty")) feed.innerHTML = "";
|
|
38
|
+
const node = document.createElement("div");
|
|
39
|
+
node.className = "timeline-item";
|
|
40
|
+
node.innerHTML = `<div class="row">${badge(ev.area || "event")} <strong>${escapeHtml(ev.event_type || "")}</strong>
|
|
41
|
+
<span class="t-meta">${escapeHtml(ev.workspace_id || "personal")} · ${escapeHtml(ev.received_at || ev.timestamp || "")}</span></div>
|
|
42
|
+
<pre style="max-height:160px">${escapeHtml(JSON.stringify(ev.payload || {}, null, 2))}</pre>`;
|
|
43
|
+
feed.prepend(node);
|
|
44
|
+
while (feed.children.length > 100) feed.lastChild.remove();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function refreshPresence() {
|
|
48
|
+
try {
|
|
49
|
+
const data = await api("/realtime/presence");
|
|
50
|
+
document.getElementById("presenceBadge").textContent = `presence: ${data.presence.length}`;
|
|
51
|
+
document.getElementById("sub").textContent =
|
|
52
|
+
`Transport: ${data.stats.transport.toUpperCase()} · subscribers: ${data.stats.subscribers} · feed: ${data.stats.feed_size}`;
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
document.getElementById("clearBtn").addEventListener("click", () => { feed.innerHTML = `<div class="empty">Cleared.</div>`; });
|
|
57
|
+
|
|
58
|
+
// Seed with recent feed, then connect to the SSE stream.
|
|
59
|
+
api("/realtime/feed?limit=30").then((d) => (d.events || []).reverse().forEach(renderEvent)).catch(() => {});
|
|
60
|
+
|
|
61
|
+
const es = new EventSource("/realtime/stream");
|
|
62
|
+
es.onopen = () => { document.getElementById("connBadge").textContent = "● live"; document.getElementById("connBadge").className = "badge ok"; };
|
|
63
|
+
es.onerror = () => { document.getElementById("connBadge").textContent = "○ reconnecting"; document.getElementById("connBadge").className = "badge warn"; };
|
|
64
|
+
es.onmessage = (e) => { try { renderEvent(JSON.parse(e.data)); } catch {} };
|
|
65
|
+
|
|
66
|
+
refreshPresence();
|
|
67
|
+
setInterval(refreshPresence, 8000);
|
|
68
|
+
</script>
|
|
69
|
+
</body>
|
|
70
|
+
</html>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Multi-Agent Runtime — Lattice AI</title>
|
|
7
|
+
<link rel="stylesheet" href="/static/platform.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main>
|
|
11
|
+
<h1>Multi-Agent Runtime</h1>
|
|
12
|
+
<p class="sub">Planner · Executor · Reviewer · Researcher · Release — with handoff, retry, and an observable timeline.</p>
|
|
13
|
+
|
|
14
|
+
<div class="section">
|
|
15
|
+
<label>Goal</label>
|
|
16
|
+
<textarea id="goal" placeholder="e.g. Draft a release checklist for v2.0.0">Summarize and verify the latest workspace activity</textarea>
|
|
17
|
+
<label>Roles (pipeline)</label>
|
|
18
|
+
<div id="roleChips" class="row"></div>
|
|
19
|
+
<div class="row" style="margin-top:14px"><button id="runBtn">Run agents</button></div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="section">
|
|
23
|
+
<h3>Run result</h3>
|
|
24
|
+
<div id="result"><div class="empty">No run yet.</div></div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="section">
|
|
28
|
+
<h3>Recent agent runs</h3>
|
|
29
|
+
<div id="runs"><div class="empty">Loading…</div></div>
|
|
30
|
+
</div>
|
|
31
|
+
</main>
|
|
32
|
+
|
|
33
|
+
<script type="module">
|
|
34
|
+
import { mountHeader, api, escapeHtml, badge, toast } from "/static/scripts/platform.js";
|
|
35
|
+
mountHeader("/agents");
|
|
36
|
+
|
|
37
|
+
const selected = new Set(["planner", "executor", "reviewer"]);
|
|
38
|
+
async function loadRoles() {
|
|
39
|
+
const data = await api("/agents/api/roles");
|
|
40
|
+
document.getElementById("roleChips").innerHTML = data.roles.map((r) =>
|
|
41
|
+
`<label class="badge" style="cursor:pointer"><input type="checkbox" value="${r.role}" ${selected.has(r.role)?"checked":""} style="width:auto;margin-right:6px">${escapeHtml(r.role)}</label>`
|
|
42
|
+
).join(" ");
|
|
43
|
+
document.getElementById("roleChips").addEventListener("change", (e) => {
|
|
44
|
+
if (e.target.checked) selected.add(e.target.value); else selected.delete(e.target.value);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderTimeline(timeline) {
|
|
49
|
+
return (timeline || []).map((t) => {
|
|
50
|
+
const label = t.event === "handoff" ? `↪ handoff ${escapeHtml(t.from)} → ${escapeHtml(t.to)}`
|
|
51
|
+
: t.event === "role" ? `● ${escapeHtml(t.role)} ${badge(t.status)}`
|
|
52
|
+
: `· ${escapeHtml(t.event)}`;
|
|
53
|
+
return `<div class="timeline-item">${label}<div class="t-meta">${escapeHtml(t.note||t.timestamp||"")}</div></div>`;
|
|
54
|
+
}).join("");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
document.getElementById("runBtn").addEventListener("click", async () => {
|
|
58
|
+
const btn = document.getElementById("runBtn");
|
|
59
|
+
btn.disabled = true;
|
|
60
|
+
try {
|
|
61
|
+
const res = await api("/agents/api/run", { method: "POST", body: JSON.stringify({
|
|
62
|
+
goal: document.getElementById("goal").value, roles: [...selected], inputs: {}
|
|
63
|
+
}) });
|
|
64
|
+
const r = res.result;
|
|
65
|
+
document.getElementById("result").innerHTML = `
|
|
66
|
+
<div class="card">
|
|
67
|
+
<div class="row"><h3>${escapeHtml(r.output)}</h3><div class="spacer"></div>${badge(r.status)}</div>
|
|
68
|
+
<div class="meta">retries: ${r.retries} · roles: ${(r.roles_run||[]).join(" → ")}</div>
|
|
69
|
+
<div class="section">${renderTimeline(r.timeline)}</div>
|
|
70
|
+
</div>`;
|
|
71
|
+
toast(`Agent run: ${r.status}`);
|
|
72
|
+
await loadRuns();
|
|
73
|
+
} catch (err) { toast(err.message); } finally { btn.disabled = false; }
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
async function loadRuns() {
|
|
77
|
+
const data = await api("/agents/api/runs");
|
|
78
|
+
const runs = data.runs || [];
|
|
79
|
+
const box = document.getElementById("runs");
|
|
80
|
+
if (!runs.length) { box.innerHTML = `<div class="empty">No agent runs yet.</div>`; return; }
|
|
81
|
+
box.innerHTML = runs.slice(0, 20).map((r) => `
|
|
82
|
+
<div class="card" style="margin-bottom:10px">
|
|
83
|
+
<div class="row"><h3>${escapeHtml((r.input||"").slice(0,80))}</h3><div class="spacer"></div>${badge(r.status)}</div>
|
|
84
|
+
<div class="meta">${escapeHtml(r.agent_id)} · ${escapeHtml(r.created_at)} · ${(r.timeline||[]).length} timeline events</div>
|
|
85
|
+
</div>`).join("");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
loadRoles().catch((e) => toast(e.message));
|
|
89
|
+
loadRuns().catch(() => {});
|
|
90
|
+
</script>
|
|
91
|
+
</body>
|
|
92
|
+
</html>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* Lattice AI v2.0 — shared styling for the Agentic Workspace Platform pages
|
|
2
|
+
(Plugin SDK, Workflow Designer, Multi-Agent Runtime, Realtime Activity). */
|
|
3
|
+
:root {
|
|
4
|
+
--bg: #0f1115;
|
|
5
|
+
--panel: #16191f;
|
|
6
|
+
--panel-2: #1c2027;
|
|
7
|
+
--border: rgba(255, 255, 255, 0.08);
|
|
8
|
+
--text: #e7ecf3;
|
|
9
|
+
--muted: #94a3b8;
|
|
10
|
+
--accent: #378ADD;
|
|
11
|
+
--accent-2: #5ea7ec;
|
|
12
|
+
--ok: #34d399;
|
|
13
|
+
--warn: #fbbf24;
|
|
14
|
+
--err: #f87171;
|
|
15
|
+
}
|
|
16
|
+
* { box-sizing: border-box; }
|
|
17
|
+
body {
|
|
18
|
+
margin: 0;
|
|
19
|
+
background: var(--bg);
|
|
20
|
+
color: var(--text);
|
|
21
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
|
22
|
+
line-height: 1.5;
|
|
23
|
+
}
|
|
24
|
+
a { color: var(--accent-2); text-decoration: none; }
|
|
25
|
+
header.app {
|
|
26
|
+
display: flex; align-items: center; gap: 18px;
|
|
27
|
+
padding: 14px 24px; border-bottom: 1px solid var(--border);
|
|
28
|
+
background: rgba(22, 25, 31, 0.8); position: sticky; top: 0; backdrop-filter: blur(8px); z-index: 5;
|
|
29
|
+
}
|
|
30
|
+
header.app .brand { font-weight: 700; font-size: 16px; color: #fff; letter-spacing: .3px; }
|
|
31
|
+
header.app .brand small { color: var(--accent); font-weight: 600; margin-left: 6px; }
|
|
32
|
+
header.app nav { display: flex; gap: 14px; flex-wrap: wrap; }
|
|
33
|
+
header.app nav a { color: var(--muted); font-size: 13px; padding: 4px 6px; border-radius: 6px; }
|
|
34
|
+
header.app nav a:hover, header.app nav a.active { color: #fff; background: var(--panel-2); }
|
|
35
|
+
main { max-width: 1080px; margin: 0 auto; padding: 28px 24px 80px; }
|
|
36
|
+
h1 { font-size: 22px; margin: 0 0 4px; }
|
|
37
|
+
.sub { color: var(--muted); font-size: 13px; margin: 0 0 24px; }
|
|
38
|
+
.grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
|
|
39
|
+
.card {
|
|
40
|
+
background: var(--panel); border: 1px solid var(--border); border-radius: 14px;
|
|
41
|
+
padding: 16px 18px;
|
|
42
|
+
}
|
|
43
|
+
.card h3 { margin: 0 0 6px; font-size: 15px; }
|
|
44
|
+
.card .meta { color: var(--muted); font-size: 12px; }
|
|
45
|
+
.row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
46
|
+
.spacer { flex: 1; }
|
|
47
|
+
.badge {
|
|
48
|
+
display: inline-block; font-size: 11px; padding: 2px 8px; border-radius: 999px;
|
|
49
|
+
background: var(--panel-2); color: var(--muted); border: 1px solid var(--border);
|
|
50
|
+
}
|
|
51
|
+
.badge.ok { color: var(--ok); border-color: rgba(52,211,153,.4); }
|
|
52
|
+
.badge.warn { color: var(--warn); border-color: rgba(251,191,36,.4); }
|
|
53
|
+
.badge.err { color: var(--err); border-color: rgba(248,113,113,.4); }
|
|
54
|
+
button, .btn {
|
|
55
|
+
background: var(--accent); color: #fff; border: none; border-radius: 8px;
|
|
56
|
+
padding: 7px 14px; font-size: 13px; cursor: pointer; font-weight: 600;
|
|
57
|
+
}
|
|
58
|
+
button.ghost { background: var(--panel-2); color: var(--text); border: 1px solid var(--border); }
|
|
59
|
+
button:hover { filter: brightness(1.08); }
|
|
60
|
+
button:disabled { opacity: .5; cursor: not-allowed; }
|
|
61
|
+
textarea, input, select {
|
|
62
|
+
width: 100%; background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
|
|
63
|
+
border-radius: 8px; padding: 9px 11px; font-size: 13px; font-family: inherit;
|
|
64
|
+
}
|
|
65
|
+
textarea { min-height: 90px; resize: vertical; }
|
|
66
|
+
label { display: block; font-size: 12px; color: var(--muted); margin: 10px 0 4px; }
|
|
67
|
+
pre {
|
|
68
|
+
background: #0b0d11; border: 1px solid var(--border); border-radius: 10px;
|
|
69
|
+
padding: 12px; overflow: auto; font-size: 12px; color: #cbd5e1; max-height: 360px;
|
|
70
|
+
}
|
|
71
|
+
.empty { color: var(--muted); text-align: center; padding: 50px 0; }
|
|
72
|
+
.section { margin-top: 28px; }
|
|
73
|
+
.timeline-item { border-left: 2px solid var(--border); padding: 6px 0 6px 14px; margin-left: 6px; font-size: 13px; }
|
|
74
|
+
.timeline-item .t-meta { color: var(--muted); font-size: 11px; }
|
|
75
|
+
.toast { position: fixed; bottom: 20px; right: 20px; background: var(--panel-2); border: 1px solid var(--border); padding: 12px 16px; border-radius: 10px; font-size: 13px; max-width: 360px; }
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Plugin SDK — Lattice AI</title>
|
|
7
|
+
<link rel="stylesheet" href="/static/platform.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main>
|
|
11
|
+
<h1>Plugin SDK</h1>
|
|
12
|
+
<p class="sub" id="sub">Versioned, permissioned plugins that extend skills, tools, and workflows.</p>
|
|
13
|
+
<div id="list" class="grid"><div class="empty">Loading plugins…</div></div>
|
|
14
|
+
|
|
15
|
+
<div class="section">
|
|
16
|
+
<h3>Validate a manifest</h3>
|
|
17
|
+
<p class="sub">Paste a <code>plugin.json</code> to check it against the SDK schema and permission allow-list.</p>
|
|
18
|
+
<textarea id="manifest" spellcheck="false">{
|
|
19
|
+
"id": "my-plugin",
|
|
20
|
+
"name": "My Plugin",
|
|
21
|
+
"version": "1.0.0",
|
|
22
|
+
"lattice_version": ">=2.0.0",
|
|
23
|
+
"permissions": ["read_workspace"],
|
|
24
|
+
"provides": { "skills": [] }
|
|
25
|
+
}</textarea>
|
|
26
|
+
<div class="row" style="margin-top:10px"><button id="validate">Validate</button></div>
|
|
27
|
+
<pre id="validateOut" style="display:none"></pre>
|
|
28
|
+
</div>
|
|
29
|
+
</main>
|
|
30
|
+
|
|
31
|
+
<script type="module">
|
|
32
|
+
import { mountHeader, api, escapeHtml, badge, toast } from "/static/scripts/platform.js";
|
|
33
|
+
mountHeader("/plugins/sdk");
|
|
34
|
+
|
|
35
|
+
async function load() {
|
|
36
|
+
const data = await api("/plugins/registry");
|
|
37
|
+
document.getElementById("sub").textContent =
|
|
38
|
+
`SDK v${data.sdk_version} · ${data.total} plugin(s) discovered in ${data.plugins_dir}`;
|
|
39
|
+
const list = document.getElementById("list");
|
|
40
|
+
if (!data.plugins.length) { list.innerHTML = `<div class="empty">No plugins found.</div>`; return; }
|
|
41
|
+
list.innerHTML = data.plugins.map((p) => `
|
|
42
|
+
<div class="card">
|
|
43
|
+
<div class="row"><h3>${escapeHtml(p.name)}</h3><div class="spacer"></div>
|
|
44
|
+
${badge(p.installed ? (p.enabled ? "ready" : "available") : "available")}</div>
|
|
45
|
+
<div class="meta">v${escapeHtml(p.version)} · ${escapeHtml(p.author || "unknown")} · ${p.compatible ? "compatible" : "<span class='badge err'>incompatible</span>"}</div>
|
|
46
|
+
<p style="font-size:13px;color:#cbd5e1">${escapeHtml(p.description)}</p>
|
|
47
|
+
<div class="meta">Permissions: ${(p.permissions||[]).map(x=>`<span class="badge">${escapeHtml(x)}</span>`).join(" ") || "none"}</div>
|
|
48
|
+
<div class="meta" style="margin-top:6px">Provides: ${Object.entries(p.provides||{}).map(([k,v])=>`${k}(${(v||[]).length})`).join(", ") || "—"}</div>
|
|
49
|
+
<div class="row" style="margin-top:12px">
|
|
50
|
+
${p.installed
|
|
51
|
+
? `<button class="ghost" data-act="${p.enabled?"disable":"enable"}" data-id="${p.id}">${p.enabled?"Disable":"Enable"}</button>
|
|
52
|
+
<button class="ghost" data-act="uninstall" data-id="${p.id}">Uninstall</button>`
|
|
53
|
+
: `<button data-act="install" data-id="${p.id}" ${p.compatible?"":"disabled"}>Install</button>`}
|
|
54
|
+
</div>
|
|
55
|
+
</div>`).join("");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
document.getElementById("list").addEventListener("click", async (e) => {
|
|
59
|
+
const btn = e.target.closest("button[data-act]");
|
|
60
|
+
if (!btn) return;
|
|
61
|
+
btn.disabled = true;
|
|
62
|
+
try {
|
|
63
|
+
await api(`/plugins/${btn.dataset.act}`, { method: "POST", body: JSON.stringify({ plugin_id: btn.dataset.id }) });
|
|
64
|
+
toast(`${btn.dataset.act}: ${btn.dataset.id}`);
|
|
65
|
+
await load();
|
|
66
|
+
} catch (err) { toast(err.message); btn.disabled = false; }
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
document.getElementById("validate").addEventListener("click", async () => {
|
|
70
|
+
const out = document.getElementById("validateOut");
|
|
71
|
+
out.style.display = "block";
|
|
72
|
+
try {
|
|
73
|
+
const manifest = JSON.parse(document.getElementById("manifest").value);
|
|
74
|
+
const res = await api("/plugins/validate", { method: "POST", body: JSON.stringify({ manifest }) });
|
|
75
|
+
out.textContent = JSON.stringify(res, null, 2);
|
|
76
|
+
} catch (err) { out.textContent = "Error: " + err.message; }
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
load().catch((e) => toast(e.message));
|
|
80
|
+
</script>
|
|
81
|
+
</body>
|
|
82
|
+
</html>
|