ltcai 2.0.0 → 2.1.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 +25 -24
- package/docs/CHANGELOG.md +54 -0
- package/docs/MULTI_AGENT_RUNTIME.md +23 -5
- package/docs/PLUGIN_SDK.md +20 -7
- package/docs/REALTIME_COLLABORATION.md +19 -6
- package/docs/V2_ARCHITECTURE.md +26 -14
- package/docs/WORKFLOW_DESIGNER.md +18 -8
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +57 -1
- package/latticeai/api/marketplace.py +81 -0
- package/latticeai/api/plugins.py +1 -1
- package/latticeai/api/realtime.py +1 -1
- package/latticeai/api/workflow_designer.py +10 -1
- package/latticeai/core/marketplace.py +178 -0
- package/latticeai/core/multi_agent.py +359 -68
- package/latticeai/core/plugins.py +29 -13
- package/latticeai/core/realtime.py +1 -1
- package/latticeai/core/workflow_engine.py +1 -1
- package/latticeai/core/workspace_os.py +257 -10
- package/latticeai/server_app.py +16 -4
- package/latticeai/services/platform_runtime.py +9 -5
- package/package.json +2 -2
- package/static/agents.html +47 -3
- package/static/plugins.html +51 -0
- package/static/workflows.html +22 -0
package/static/agents.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<body>
|
|
10
10
|
<main>
|
|
11
11
|
<h1>Multi-Agent Runtime</h1>
|
|
12
|
-
<p class="sub">Planner · Executor · Reviewer · Researcher · Release — with handoff, retry, and
|
|
12
|
+
<p class="sub">Planner · Executor · Reviewer · Researcher · Release — with handoff, context packets, memory, retry, and replay.</p>
|
|
13
13
|
|
|
14
14
|
<div class="section">
|
|
15
15
|
<label>Goal</label>
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
<h3>Recent agent runs</h3>
|
|
29
29
|
<div id="runs"><div class="empty">Loading…</div></div>
|
|
30
30
|
</div>
|
|
31
|
+
|
|
32
|
+
<div class="section">
|
|
33
|
+
<h3>Replay viewer</h3>
|
|
34
|
+
<div id="replay"><div class="empty">Select a recent run.</div></div>
|
|
35
|
+
</div>
|
|
31
36
|
</main>
|
|
32
37
|
|
|
33
38
|
<script type="module">
|
|
@@ -47,13 +52,33 @@
|
|
|
47
52
|
|
|
48
53
|
function renderTimeline(timeline) {
|
|
49
54
|
return (timeline || []).map((t) => {
|
|
50
|
-
const label = t.event
|
|
55
|
+
const label = (t.event || "").startsWith("handoff_") ? `↪ ${escapeHtml(t.event)} ${escapeHtml(t.from||"")} → ${escapeHtml(t.to||"")}`
|
|
56
|
+
: t.event === "handoff" ? `↪ handoff ${escapeHtml(t.from)} → ${escapeHtml(t.to)}`
|
|
51
57
|
: t.event === "role" ? `● ${escapeHtml(t.role)} ${badge(t.status)}`
|
|
58
|
+
: t.event === "retry_requested" ? `↻ retry ${escapeHtml(t.reason||"")}`
|
|
59
|
+
: t.event === "review_approved" ? `✓ review approved`
|
|
52
60
|
: `· ${escapeHtml(t.event)}`;
|
|
53
61
|
return `<div class="timeline-item">${label}<div class="t-meta">${escapeHtml(t.note||t.timestamp||"")}</div></div>`;
|
|
54
62
|
}).join("");
|
|
55
63
|
}
|
|
56
64
|
|
|
65
|
+
function renderHandoffs(handoffs) {
|
|
66
|
+
if (!handoffs?.length) return `<div class="empty">No handoffs recorded.</div>`;
|
|
67
|
+
return handoffs.map((h) => `<div class="timeline-item">
|
|
68
|
+
<strong>${escapeHtml(h.handoff_id)}</strong> ${badge(h.status)}
|
|
69
|
+
<div class="t-meta">${escapeHtml(h.source_agent)} → ${escapeHtml(h.target_agent)} · ${escapeHtml(h.reason||"")}</div>
|
|
70
|
+
</div>`).join("");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderReview(result) {
|
|
74
|
+
const reviews = result.review_history || [];
|
|
75
|
+
const retries = result.retry_history || [];
|
|
76
|
+
return `<div class="grid two">
|
|
77
|
+
<div>${reviews.length ? reviews.map((r) => `<div class="timeline-item">${badge(r.outcome)} ${escapeHtml(r.reason||"")}<div class="t-meta">retry ${r.retry_count}</div></div>`).join("") : `<div class="empty">No review history.</div>`}</div>
|
|
78
|
+
<div>${retries.length ? retries.map((r) => `<div class="timeline-item">${badge("retry " + r.retry)} ${escapeHtml(r.reason||"")}<div class="t-meta">limit ${r.limit}</div></div>`).join("") : `<div class="empty">No retries.</div>`}</div>
|
|
79
|
+
</div>`;
|
|
80
|
+
}
|
|
81
|
+
|
|
57
82
|
document.getElementById("runBtn").addEventListener("click", async () => {
|
|
58
83
|
const btn = document.getElementById("runBtn");
|
|
59
84
|
btn.disabled = true;
|
|
@@ -66,7 +91,9 @@
|
|
|
66
91
|
<div class="card">
|
|
67
92
|
<div class="row"><h3>${escapeHtml(r.output)}</h3><div class="spacer"></div>${badge(r.status)}</div>
|
|
68
93
|
<div class="meta">retries: ${r.retries} · roles: ${(r.roles_run||[]).join(" → ")}</div>
|
|
69
|
-
<div class="section">${
|
|
94
|
+
<div class="section"><h3>Handoff chain</h3>${renderHandoffs(r.handoffs)}</div>
|
|
95
|
+
<div class="section"><h3>Review panel</h3>${renderReview(r)}</div>
|
|
96
|
+
<div class="section"><h3>Timeline</h3>${renderTimeline(r.timeline)}</div>
|
|
70
97
|
</div>`;
|
|
71
98
|
toast(`Agent run: ${r.status}`);
|
|
72
99
|
await loadRuns();
|
|
@@ -82,9 +109,26 @@
|
|
|
82
109
|
<div class="card" style="margin-bottom:10px">
|
|
83
110
|
<div class="row"><h3>${escapeHtml((r.input||"").slice(0,80))}</h3><div class="spacer"></div>${badge(r.status)}</div>
|
|
84
111
|
<div class="meta">${escapeHtml(r.agent_id)} · ${escapeHtml(r.created_at)} · ${(r.timeline||[]).length} timeline events</div>
|
|
112
|
+
<div class="row" style="margin-top:10px"><button class="ghost" data-replay="${r.id}">Replay</button></div>
|
|
85
113
|
</div>`).join("");
|
|
86
114
|
}
|
|
87
115
|
|
|
116
|
+
document.getElementById("runs").addEventListener("click", async (e) => {
|
|
117
|
+
const btn = e.target.closest("button[data-replay]");
|
|
118
|
+
if (!btn) return;
|
|
119
|
+
const out = document.getElementById("replay");
|
|
120
|
+
out.innerHTML = `<div class="empty">Loading replay…</div>`;
|
|
121
|
+
try {
|
|
122
|
+
const data = await api(`/agents/api/runs/${btn.dataset.replay}/replay`);
|
|
123
|
+
const frames = data.replay.frames || [];
|
|
124
|
+
out.innerHTML = frames.map((f) => `<div class="timeline-item">
|
|
125
|
+
<div class="row"><strong>${escapeHtml(f.event)}</strong><div class="spacer"></div>${badge(f.decision || "event")}</div>
|
|
126
|
+
<div class="t-meta">${escapeHtml(String(f.actor||""))} · ${escapeHtml(f.when||"")}</div>
|
|
127
|
+
<pre>${escapeHtml(JSON.stringify({ why: f.why, input: f.input, output: f.output }, null, 2))}</pre>
|
|
128
|
+
</div>`).join("") || `<div class="empty">No replay frames.</div>`;
|
|
129
|
+
} catch (err) { out.innerHTML = `<div class="empty">${escapeHtml(err.message)}</div>`; }
|
|
130
|
+
});
|
|
131
|
+
|
|
88
132
|
loadRoles().catch((e) => toast(e.message));
|
|
89
133
|
loadRuns().catch(() => {});
|
|
90
134
|
</script>
|
package/static/plugins.html
CHANGED
|
@@ -12,6 +12,16 @@
|
|
|
12
12
|
<p class="sub" id="sub">Versioned, permissioned plugins that extend skills, tools, and workflows.</p>
|
|
13
13
|
<div id="list" class="grid"><div class="empty">Loading plugins…</div></div>
|
|
14
14
|
|
|
15
|
+
<div class="section">
|
|
16
|
+
<h3>Template foundation</h3>
|
|
17
|
+
<div id="templates" class="grid"><div class="empty">Loading templates…</div></div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="section">
|
|
21
|
+
<h3>Plugin execution viewer</h3>
|
|
22
|
+
<div id="pluginEvents"><div class="empty">Loading plugin events…</div></div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
15
25
|
<div class="section">
|
|
16
26
|
<h3>Validate a manifest</h3>
|
|
17
27
|
<p class="sub">Paste a <code>plugin.json</code> to check it against the SDK schema and permission allow-list.</p>
|
|
@@ -55,6 +65,45 @@
|
|
|
55
65
|
</div>`).join("");
|
|
56
66
|
}
|
|
57
67
|
|
|
68
|
+
async function loadTemplates() {
|
|
69
|
+
const data = await api("/marketplace/templates");
|
|
70
|
+
const box = document.getElementById("templates");
|
|
71
|
+
if (!data.templates.length) { box.innerHTML = `<div class="empty">No templates available.</div>`; return; }
|
|
72
|
+
box.innerHTML = data.templates.map((t) => `
|
|
73
|
+
<div class="card">
|
|
74
|
+
<div class="row"><h3>${escapeHtml(t.name)}</h3><div class="spacer"></div>${badge(t.kind)}</div>
|
|
75
|
+
<div class="meta">v${escapeHtml(t.version)} · ${escapeHtml((t.metadata||{}).category || "foundation")}</div>
|
|
76
|
+
<p style="font-size:13px;color:#cbd5e1">${escapeHtml(t.description || "")}</p>
|
|
77
|
+
<div class="row" style="margin-top:12px">
|
|
78
|
+
<a class="btn ghost" target="_blank" href="/marketplace/templates/${t.kind}/${t.id}/export">Export</a>
|
|
79
|
+
<button data-template="${t.kind}:${t.id}">Install</button>
|
|
80
|
+
</div>
|
|
81
|
+
</div>`).join("");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function loadPluginEvents() {
|
|
85
|
+
const data = await api("/realtime/feed?limit=80");
|
|
86
|
+
const events = (data.events || []).filter((e) => e.area === "plugins" || e.event_type?.startsWith("plugin_"));
|
|
87
|
+
const box = document.getElementById("pluginEvents");
|
|
88
|
+
if (!events.length) { box.innerHTML = `<div class="empty">No plugin executions yet.</div>`; return; }
|
|
89
|
+
box.innerHTML = events.slice(0, 20).map((ev) => `<div class="timeline-item">
|
|
90
|
+
<div class="row"><strong>${escapeHtml(ev.event_type)}</strong><div class="spacer"></div>${badge((ev.payload||{}).status || ev.area)}</div>
|
|
91
|
+
<div class="t-meta">${escapeHtml((ev.payload||{}).plugin_id || (ev.payload||{}).plugin || "")} · ${escapeHtml(ev.received_at || ev.timestamp || "")}</div>
|
|
92
|
+
</div>`).join("");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
document.getElementById("templates").addEventListener("click", async (e) => {
|
|
96
|
+
const btn = e.target.closest("button[data-template]");
|
|
97
|
+
if (!btn) return;
|
|
98
|
+
btn.disabled = true;
|
|
99
|
+
const [kind, id] = btn.dataset.template.split(":");
|
|
100
|
+
try {
|
|
101
|
+
const exported = await api(`/marketplace/templates/${kind}/${id}/export`);
|
|
102
|
+
await api("/marketplace/templates/install", { method: "POST", body: JSON.stringify({ data: exported }) });
|
|
103
|
+
toast(`Installed template: ${id}`);
|
|
104
|
+
} catch (err) { toast(err.message); } finally { btn.disabled = false; }
|
|
105
|
+
});
|
|
106
|
+
|
|
58
107
|
document.getElementById("list").addEventListener("click", async (e) => {
|
|
59
108
|
const btn = e.target.closest("button[data-act]");
|
|
60
109
|
if (!btn) return;
|
|
@@ -77,6 +126,8 @@
|
|
|
77
126
|
});
|
|
78
127
|
|
|
79
128
|
load().catch((e) => toast(e.message));
|
|
129
|
+
loadTemplates().catch((e) => toast(e.message));
|
|
130
|
+
loadPluginEvents().catch(() => {});
|
|
80
131
|
</script>
|
|
81
132
|
</body>
|
|
82
133
|
</html>
|
package/static/workflows.html
CHANGED
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
<h3>Run history</h3>
|
|
36
36
|
<div id="runs"><div class="empty">No runs yet.</div></div>
|
|
37
37
|
</div>
|
|
38
|
+
|
|
39
|
+
<div class="section">
|
|
40
|
+
<h3>Replay viewer</h3>
|
|
41
|
+
<div id="replay"><div class="empty">Select a workflow run.</div></div>
|
|
42
|
+
</div>
|
|
38
43
|
</main>
|
|
39
44
|
|
|
40
45
|
<script type="module">
|
|
@@ -109,10 +114,27 @@
|
|
|
109
114
|
<div class="card" style="margin-bottom:10px">
|
|
110
115
|
<div class="row"><h3>${escapeHtml(r.name)}</h3><div class="spacer"></div>${badge(r.status)}</div>
|
|
111
116
|
<div class="meta">${escapeHtml(r.created_at)} · ${ (r.timeline||[]).length } steps</div>
|
|
117
|
+
<div class="row" style="margin-top:10px"><button class="ghost" data-replay="${r.id}">Replay</button></div>
|
|
112
118
|
<pre>${escapeHtml(JSON.stringify(r.timeline, null, 2))}</pre>
|
|
113
119
|
</div>`).join("");
|
|
114
120
|
}
|
|
115
121
|
|
|
122
|
+
document.getElementById("runs").addEventListener("click", async (e) => {
|
|
123
|
+
const btn = e.target.closest("button[data-replay]");
|
|
124
|
+
if (!btn) return;
|
|
125
|
+
const out = document.getElementById("replay");
|
|
126
|
+
out.innerHTML = `<div class="empty">Loading replay…</div>`;
|
|
127
|
+
try {
|
|
128
|
+
const data = await api(`/workflows/api/runs/${btn.dataset.replay}/replay`);
|
|
129
|
+
const frames = data.replay.frames || [];
|
|
130
|
+
out.innerHTML = frames.map((f) => `<div class="timeline-item">
|
|
131
|
+
<div class="row"><strong>${escapeHtml(f.event)}</strong><div class="spacer"></div>${badge(f.decision || "event")}</div>
|
|
132
|
+
<div class="t-meta">${escapeHtml(String(f.actor||""))} · ${escapeHtml(f.when||"")}</div>
|
|
133
|
+
<pre>${escapeHtml(JSON.stringify({ why: f.why, input: f.input, output: f.output }, null, 2))}</pre>
|
|
134
|
+
</div>`).join("") || `<div class="empty">No replay frames.</div>`;
|
|
135
|
+
} catch (err) { out.innerHTML = `<div class="empty">${escapeHtml(err.message)}</div>`; }
|
|
136
|
+
});
|
|
137
|
+
|
|
116
138
|
loadTemplate();
|
|
117
139
|
loadList().catch((e) => toast(e.message));
|
|
118
140
|
loadRuns().catch(() => {});
|