ltcai 1.6.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.
Files changed (40) hide show
  1. package/README.md +40 -19
  2. package/docs/CHANGELOG.md +107 -0
  3. package/docs/EDITION_STRATEGY.md +14 -4
  4. package/docs/ENTERPRISE.md +11 -3
  5. package/docs/MULTI_AGENT_RUNTIME.md +410 -0
  6. package/docs/PLUGIN_SDK.md +651 -0
  7. package/docs/REALTIME_COLLABORATION.md +410 -0
  8. package/docs/V2_ARCHITECTURE.md +528 -0
  9. package/docs/WORKFLOW_DESIGNER.md +475 -0
  10. package/latticeai/__init__.py +1 -1
  11. package/latticeai/api/agents.py +98 -0
  12. package/latticeai/api/plugins.py +115 -0
  13. package/latticeai/api/realtime.py +91 -0
  14. package/latticeai/api/workflow_designer.py +207 -0
  15. package/latticeai/core/multi_agent.py +270 -0
  16. package/latticeai/core/plugins.py +400 -0
  17. package/latticeai/core/realtime.py +190 -0
  18. package/latticeai/core/workflow_engine.py +329 -0
  19. package/latticeai/core/workspace_os.py +165 -2
  20. package/latticeai/server_app.py +76 -2
  21. package/latticeai/services/platform_runtime.py +200 -0
  22. package/package.json +17 -2
  23. package/plugins/README.md +35 -0
  24. package/plugins/git-insights/plugin.json +15 -0
  25. package/plugins/hello-world/plugin.json +16 -0
  26. package/plugins/hello-world/skills/hello_skill/SKILL.md +15 -0
  27. package/static/activity.html +70 -0
  28. package/static/admin.html +62 -0
  29. package/static/agents.html +92 -0
  30. package/static/graph.html +7 -1
  31. package/static/lattice-reference.css +184 -0
  32. package/static/platform.css +75 -0
  33. package/static/plugins.html +82 -0
  34. package/static/scripts/admin.js +121 -1
  35. package/static/scripts/graph.js +296 -14
  36. package/static/scripts/platform.js +64 -0
  37. package/static/scripts/workspace.js +107 -10
  38. package/static/workflows.html +121 -0
  39. package/static/workspace.css +73 -0
  40. package/static/workspace.html +18 -2
@@ -0,0 +1,410 @@
1
+ # Lattice AI Multi-Agent Runtime 2.0
2
+
3
+ The Multi-Agent Runtime is the **orchestration layer** introduced in v2.0.0. It sits
4
+ *above* the v1.x single-agent state machine ([`AgentRuntime`](../latticeai/core/agent.py))
5
+ and coordinates a pipeline of named **roles** that hand off work to one another,
6
+ retry on a failing review, and emit a fully observable timeline.
7
+
8
+ - **Source of truth:** `latticeai/core/multi_agent.py`
9
+ - **HTTP surface:** `latticeai/api/agents.py`
10
+ - **Persistence / Knowledge Graph integration:** `latticeai/core/workspace_os.py`
11
+ (`WorkspaceOSStore.record_agent_run`)
12
+
13
+ ```python
14
+ MULTI_AGENT_VERSION = "2.0.0"
15
+ ```
16
+
17
+ ## How it relates to the v1 single-agent runtime
18
+
19
+ v1.x shipped a single-agent state machine — `AgentRuntime` driving
20
+ `PLAN → EXECUTE → VERIFY → DONE` (with `ROLLBACK` / `FAILED` recovery paths) over an
21
+ injected `AgentDeps` port. That runtime is unchanged.
22
+
23
+ v2.0 adds the *orchestration* layer on top: instead of one agent looping through
24
+ internal phases, the `MultiAgentOrchestrator` drives a **pipeline of distinct roles**
25
+ (researcher, planner, executor, reviewer, release) that hand off to one another and
26
+ can rewind on a failing review. The two layers are complementary — the v2
27
+ orchestrator coordinates roles; an individual role's runner could itself be backed by
28
+ the v1 `AgentRuntime` loop or an LLM, but that is an implementation choice behind the
29
+ injected runner port.
30
+
31
+ > **Compatibility.** The Multi-Agent Runtime is purely additive. The v1
32
+ > `AgentRuntime` state machine and its `/agent` endpoints are untouched, and the v2
33
+ > API is namespaced under `/agents` (plural) so it never collides with the existing
34
+ > single-agent `/agent` routes. Existing v1.x data is preserved; new runs are appended
35
+ > to workspace state and the Knowledge Graph alongside it.
36
+
37
+ ## Built-in roles (`AGENT_ROLES`)
38
+
39
+ The runtime defines five built-in roles. Each role id matches an entry in
40
+ `latticeai.core.workspace_os.DEFAULT_AGENTS`, so orchestrated runs reference the same
41
+ agents that already appear in the Workspace.
42
+
43
+ ```python
44
+ AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
45
+
46
+ ROLE_AGENT_IDS = {
47
+ "researcher": "agent:researcher",
48
+ "planner": "agent:planner",
49
+ "executor": "agent:executor",
50
+ "reviewer": "agent:reviewer",
51
+ "release": "agent:release",
52
+ }
53
+ ```
54
+
55
+ | Role | Agent id | Responsibility |
56
+ | --- | --- | --- |
57
+ | `researcher` | `agent:researcher` | Gathers relevant context (workspace memory / graph) via an injected `context_provider`. |
58
+ | `planner` | `agent:planner` | Decomposes the goal into ordered, inspectable steps. |
59
+ | `executor` | `agent:executor` | Carries out steps; may drive an injected workflow / plugin runner. |
60
+ | `reviewer` | `agent:reviewer` | Judges the result and returns a `pass` / `retry` verdict. |
61
+ | `release` | `agent:release` | Finalizes / packages the outcome (optional). |
62
+
63
+ The `agent:*` ids correspond one-to-one with `DEFAULT_AGENTS` in `workspace_os`
64
+ (`agent:planner`, `agent:executor`, `agent:reviewer`, `agent:researcher`,
65
+ `agent:release`), which is why an orchestrated run can record relationships against
66
+ agents that the Workspace already knows about.
67
+
68
+ ### The core pipeline
69
+
70
+ `researcher` and `release` are optional stages — they run only when explicitly
71
+ requested. A quick, default run is therefore three stages:
72
+
73
+ ```python
74
+ CORE_PIPELINE = ("planner", "executor", "reviewer")
75
+ ```
76
+
77
+ When `MultiAgentOrchestrator.run(...)` is called without an explicit `roles` list, it
78
+ uses `CORE_PIPELINE`. Any roles passed in are filtered to the set of known
79
+ `AGENT_ROLES`; if nothing valid remains, it falls back to `CORE_PIPELINE`.
80
+
81
+ ## Orchestration: `MultiAgentOrchestrator.run`
82
+
83
+ ```python
84
+ def run(
85
+ self,
86
+ goal: str,
87
+ *,
88
+ user_email: Optional[str] = None,
89
+ workspace_id: Optional[str] = None,
90
+ inputs: Optional[Dict[str, Any]] = None,
91
+ roles: Optional[List[str]] = None,
92
+ max_retries: int = 2,
93
+ ) -> AgentRunResult:
94
+ ...
95
+ ```
96
+
97
+ `run` walks the resolved pipeline, threading a single `OrchestrationContext` through
98
+ every stage. As it goes it appends two kinds of events to an observable timeline:
99
+
100
+ - **`role` events** — emitted by `_run_role` for each stage, recording the role, its
101
+ `agent_id`, status (`ok` / `error`), the raw runner result, and start/end timestamps.
102
+ - **`handoff` events** — emitted via `OrchestrationContext.handoff(frm, to, note)`
103
+ whenever control passes from one role to the next (and on a retry rewind).
104
+
105
+ The timeline is also bracketed by a `start` event (carrying the goal and resolved
106
+ pipeline) and an `end` event (carrying the final status and retry count).
107
+
108
+ ### Retry: reviewer rewinds to executor
109
+
110
+ The pipeline is ordinarily linear, but the **reviewer can rewind** the pipeline to the
111
+ executor. After the reviewer runs, if its verdict is `retry` and the retry budget is
112
+ not yet exhausted, the orchestrator:
113
+
114
+ 1. increments `ctx.retries`,
115
+ 2. emits a `handoff("reviewer", "executor", note="retry #N: <reason>")`,
116
+ 3. resets the pipeline index back to the executor stage, and
117
+ 4. re-runs executor → reviewer.
118
+
119
+ This repeats until the verdict is `pass` or `ctx.retries` reaches `max_retries`.
120
+
121
+ ```text
122
+ planner → executor → reviewer ──pass──▶ (continue / end)
123
+
124
+ └──retry (and retries < max_retries)──▶ executor (rewind)
125
+ ```
126
+
127
+ ### Final status
128
+
129
+ The terminal status is derived from the final reviewer verdict and retry count:
130
+
131
+ | Condition | `status` |
132
+ | --- | --- |
133
+ | Final verdict `pass`, no retries | `ok` |
134
+ | Final verdict `pass`, after one or more retries | `retried_ok` |
135
+ | Final verdict not `pass` (retries exhausted) | `failed` |
136
+
137
+ ### `OrchestrationContext`
138
+
139
+ The mutable carrier threaded through every stage:
140
+
141
+ ```python
142
+ @dataclass
143
+ class OrchestrationContext:
144
+ goal: str
145
+ user_email: Optional[str] = None
146
+ workspace_id: Optional[str] = None
147
+ inputs: Dict[str, Any] = field(default_factory=dict)
148
+ plan: List[Dict[str, Any]] = field(default_factory=list)
149
+ research: List[str] = field(default_factory=list)
150
+ executed: List[Dict[str, Any]] = field(default_factory=list)
151
+ review: Dict[str, Any] = field(default_factory=dict)
152
+ timeline: List[Dict[str, Any]] = field(default_factory=list)
153
+ retries: int = 0
154
+ output: str = ""
155
+
156
+ def handoff(self, frm: str, to: str, note: str = "") -> None: ...
157
+ ```
158
+
159
+ ### `AgentRunResult`
160
+
161
+ `run` returns an `AgentRunResult`, which exposes `as_dict()` for serialization:
162
+
163
+ ```python
164
+ @dataclass
165
+ class AgentRunResult:
166
+ agent_id: str
167
+ status: str # ok | failed | retried_ok
168
+ output: str
169
+ timeline: List[Dict[str, Any]]
170
+ plan: List[Dict[str, Any]]
171
+ review: Dict[str, Any]
172
+ roles_run: List[str]
173
+ retries: int = 0
174
+ ```
175
+
176
+ ## The role runner is an injected port
177
+
178
+ Like the v1 runtime, the orchestrator is **pure logic over an injected `role_runner`
179
+ port**. It runs with no LLM and no server:
180
+
181
+ ```python
182
+ class MultiAgentOrchestrator:
183
+ def __init__(
184
+ self,
185
+ role_runner: Optional[Callable[[str, OrchestrationContext], Dict[str, Any]]] = None,
186
+ ):
187
+ self.role_runner = role_runner or default_role_runner()
188
+ ```
189
+
190
+ The runner is a single callable `(role: str, ctx: OrchestrationContext) -> Dict[str, Any]`.
191
+ The orchestration logic — pipeline walking, handoffs, retry rewind, timeline emission,
192
+ status derivation — does not depend on *how* a role does its work. This means a
193
+ **production deployment can swap in an LLM-backed runner without touching the
194
+ orchestration layer**: implement the same callable signature, pass it to the
195
+ constructor, and the pipeline behaves identically while individual roles gain
196
+ model-backed reasoning.
197
+
198
+ If a runner raises, `_run_role` captures the exception into the `role` event as
199
+ `status: "error"` with an `{"error": ...}` result, rather than crashing the run.
200
+
201
+ ## The default runner is deterministic and useful
202
+
203
+ `default_role_runner` builds a **dependency-free, deterministic** runner that
204
+ implements every built-in role with real (non-LLM) behavior. This is what makes
205
+ "agent runs can execute workflows / plugins" true in the community edition without
206
+ requiring a model.
207
+
208
+ ```python
209
+ def default_role_runner(
210
+ *,
211
+ workflow_runner: Optional[Callable[..., Any]] = None,
212
+ plugin_runner: Optional[Callable[..., Any]] = None,
213
+ context_provider: Optional[Callable[[str], List[str]]] = None,
214
+ ) -> Callable[[str, OrchestrationContext], Dict[str, Any]]:
215
+ ...
216
+ ```
217
+
218
+ Behavior by role:
219
+
220
+ - **`researcher`** — calls the injected `context_provider(goal)` (workspace memory) to
221
+ pull relevant context into `ctx.research`; returns the count and the first items.
222
+ - **`planner`** — decomposes the goal into ordered steps. If `inputs["steps"]` is a
223
+ non-empty list, each entry becomes a planned step; otherwise it produces a default
224
+ three-step plan (`Analyze` / `Execute` / `Verify the result`). Steps are written to
225
+ `ctx.plan`.
226
+ - **`executor`** — iterates the plan and marks each step `done`. A step may request a
227
+ workflow or plugin run; when the corresponding runner is injected, the executor
228
+ drives it (see below). Results are written to `ctx.executed` and a summary line to
229
+ `ctx.output`.
230
+ - **`reviewer`** — passes only if `ctx.executed` is non-empty and *every* executed step
231
+ has `status == "done"`; otherwise it returns `retry`. The verdict shape is:
232
+
233
+ ```json
234
+ {
235
+ "verdict": "pass",
236
+ "reason": "all steps completed",
237
+ "confidence": 0.9
238
+ }
239
+ ```
240
+
241
+ A failing review yields `{"verdict": "retry", "reason": "no steps executed", "confidence": 0.3}`.
242
+ - **`release`** — sets/keeps `ctx.output` and returns `{"released": true, "summary": ...}`.
243
+
244
+ An unrecognized role returns `{"role": role, "status": "noop"}`.
245
+
246
+ ### Agent → workflow and agent → plugin integration
247
+
248
+ The executor role is where Lattice's cross-feature integration happens. When the
249
+ `default_role_runner` is built with a `workflow_runner` and/or `plugin_runner`, a plan
250
+ step can drive them:
251
+
252
+ - A step's `workflow` (or `inputs["workflow"]`) is run via `workflow_runner(wf, ctx)`
253
+ on the first step (`index == 0`), capturing the result under `workflow_result` (or
254
+ `workflow_error` on failure).
255
+ - A step's `plugin` is run via `plugin_runner(pl, ctx)`, capturing `plugin_result` (or
256
+ `plugin_error`).
257
+
258
+ This is the **agent → workflow** and **agent → plugin** seam: an orchestrated agent run
259
+ can actually execute Workflows and Plugins, in the community edition, with no model
260
+ required.
261
+
262
+ ## Persistence and Knowledge Graph
263
+
264
+ After a run completes, the API persists it via
265
+ `WorkspaceOSStore.record_agent_run`, which both ingests a Knowledge Graph node and
266
+ records a Workspace timeline event:
267
+
268
+ ```python
269
+ def record_agent_run(
270
+ self,
271
+ *,
272
+ agent_id: str,
273
+ status: str,
274
+ input_text: str,
275
+ output_text: str,
276
+ user_email: Optional[str],
277
+ timeline: Optional[List[Dict[str, Any]]] = None,
278
+ relationships: Optional[List[str]] = None,
279
+ graph: Any = None,
280
+ workspace_id: Optional[str] = None,
281
+ ) -> Dict[str, Any]:
282
+ ...
283
+ ```
284
+
285
+ What it does:
286
+
287
+ - Builds a run record (`id`, `agent_id`, `status`, `input`, `output_preview` truncated
288
+ to 1000 chars, `user_email`, scoped `workspace_id`, `relationships`, `timeline`,
289
+ `created_at`).
290
+ - When a `graph` is supplied, calls `graph.ingest_event("AgentRun", ...)` and stores the
291
+ returned `graph_node_id` on the run (capturing `graph_error` if ingest fails).
292
+ - Appends the run to workspace state (retaining the most recent 300) and records an
293
+ `agent` / `agent_run` timeline event.
294
+
295
+ Runs are workspace-scoped; `list_agents(workspace_id=...)` returns the registered
296
+ `agents` plus the most recent runs for that scope.
297
+
298
+ ## HTTP API
299
+
300
+ The router is created by `create_agents_router(...)` in `latticeai/api/agents.py`. All
301
+ paths live under `/agents` (plural).
302
+
303
+ > **Compatibility.** `/agents` does **not** collide with the existing single-agent
304
+ > `/agent` endpoints — the trailing `s` keeps the v2 namespace fully separate, so v1
305
+ > clients continue to work unchanged.
306
+
307
+ ### `GET /agents` — UI page
308
+
309
+ Requires an authenticated user. Serves the `agents.html` Multi-Agent UI from the static
310
+ directory; returns `404` if the UI is not available or not found.
311
+
312
+ ### `GET /agents/api/roles`
313
+
314
+ Requires an authenticated user. Lists the built-in roles and the default pipeline.
315
+
316
+ ```json
317
+ {
318
+ "roles": [
319
+ {"role": "researcher", "agent_id": "agent:researcher"},
320
+ {"role": "planner", "agent_id": "agent:planner"},
321
+ {"role": "executor", "agent_id": "agent:executor"},
322
+ {"role": "reviewer", "agent_id": "agent:reviewer"},
323
+ {"role": "release", "agent_id": "agent:release"}
324
+ ],
325
+ "default_pipeline": ["planner", "executor", "reviewer"]
326
+ }
327
+ ```
328
+
329
+ ### `GET /agents/api/runs`
330
+
331
+ Requires an authenticated user; reads are scoped via `gate_read`. Returns the registered
332
+ agents plus recent runs for the resolved workspace scope (the result of
333
+ `store.list_agents(workspace_id=scope)`).
334
+
335
+ ```json
336
+ {
337
+ "agents": [ { "id": "agent:planner", "name": "Planner", "...": "..." } ],
338
+ "runs": [ { "id": "agent-run-...", "agent_id": "agent:executor", "status": "ok", "...": "..." } ]
339
+ }
340
+ ```
341
+
342
+ ### `POST /agents/api/run`
343
+
344
+ Requires an authenticated user; writes are scoped via `gate_write`. Runs the
345
+ orchestrator and persists the result.
346
+
347
+ **Request body** (`AgentRunRequest`):
348
+
349
+ ```json
350
+ {
351
+ "goal": "Summarize the open incidents and draft a status update",
352
+ "roles": ["researcher", "planner", "executor", "reviewer"],
353
+ "inputs": { "steps": ["Collect incidents", "Draft update"] },
354
+ "max_retries": 2
355
+ }
356
+ ```
357
+
358
+ | Field | Type | Default | Notes |
359
+ | --- | --- | --- | --- |
360
+ | `goal` | string | — | Required; a `400` is returned if blank. |
361
+ | `roles` | string[] | `[]` | Empty means the default `CORE_PIPELINE`. Unknown roles are filtered out. |
362
+ | `inputs` | object | `{}` | Passed through to the runner (e.g. `steps`, `workflow`). |
363
+ | `max_retries` | int | `2` | Clamped server-side to the range `0`–`5`. |
364
+
365
+ The endpoint resolves an orchestrator via the injected `orchestrator_factory(user, scope)`,
366
+ runs it, records the run with `store.record_agent_run(...)` (passing the
367
+ `workspace_graph()` for KG ingest and the run's `roles_run` as `relationships`), and
368
+ appends a `multi_agent_run` audit event.
369
+
370
+ **Response:**
371
+
372
+ ```json
373
+ {
374
+ "run": {
375
+ "id": "agent-run-…",
376
+ "agent_id": "agent:executor",
377
+ "status": "ok",
378
+ "input": "Summarize the open incidents and draft a status update",
379
+ "output_preview": "Completed 2 planned step(s) for: …",
380
+ "relationships": ["agent:researcher", "agent:planner", "agent:executor", "agent:reviewer"],
381
+ "timeline": [ { "event": "start", "...": "..." } ],
382
+ "graph_node_id": "…",
383
+ "created_at": "…"
384
+ },
385
+ "result": {
386
+ "agent_id": "agent:executor",
387
+ "status": "ok",
388
+ "output": "Completed 2 planned step(s) for: …",
389
+ "timeline": [ "…" ],
390
+ "plan": [ { "index": 0, "description": "Collect incidents", "status": "done" } ],
391
+ "review": { "verdict": "pass", "reason": "all steps completed", "confidence": 0.9 },
392
+ "roles_run": ["researcher", "planner", "executor", "reviewer"],
393
+ "retries": 0
394
+ }
395
+ }
396
+ ```
397
+
398
+ ## Timeline event reference
399
+
400
+ The orchestration timeline is a flat, ordered list of event objects. Event types:
401
+
402
+ | `event` | Emitted by | Key fields |
403
+ | --- | --- | --- |
404
+ | `start` | `run` (before the pipeline) | `goal`, `pipeline`, `timestamp` |
405
+ | `role` | `_run_role` (per stage) | `role`, `agent_id`, `status`, `result`, `started_at`, `timestamp` |
406
+ | `handoff` | `OrchestrationContext.handoff` | `from`, `to`, `note`, `timestamp` |
407
+ | `end` | `run` (after the pipeline) | `status`, `retries`, `timestamp` |
408
+
409
+ This timeline is returned on the run result and persisted with the run, so it drops
410
+ straight into the Workspace timeline and Knowledge Graph.