ltcai 4.3.1 → 4.4.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 (131) hide show
  1. package/README.md +191 -278
  2. package/docs/CHANGELOG.md +128 -0
  3. package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +174 -0
  4. package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +81 -0
  5. package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +75 -0
  6. package/docs/V4_3_2_GRAPH_UX_REPORT.md +48 -0
  7. package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +209 -0
  8. package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +57 -0
  9. package/docs/V4_3_2_SELF_AUDIT_REPORT.md +63 -0
  10. package/docs/V4_3_2_VALIDATION_REPORT.md +97 -0
  11. package/docs/V4_3_3_VALIDATION_REPORT.md +46 -0
  12. package/docs/V4_4_0_EXTRACTION_REPORT.md +239 -0
  13. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +18 -19
  14. package/frontend/openapi.json +1 -1
  15. package/frontend/src/components/primitives.tsx +92 -10
  16. package/frontend/src/pages/Act.tsx +11 -9
  17. package/frontend/src/pages/Ask.tsx +2 -2
  18. package/frontend/src/pages/Brain.tsx +607 -65
  19. package/frontend/src/pages/Capture.tsx +11 -7
  20. package/frontend/src/pages/Library.tsx +3 -3
  21. package/frontend/src/pages/System.tsx +186 -23
  22. package/lattice_brain/__init__.py +38 -23
  23. package/lattice_brain/_kg_common.py +11 -1
  24. package/lattice_brain/context.py +212 -2
  25. package/lattice_brain/conversations.py +234 -1
  26. package/lattice_brain/discovery.py +11 -1
  27. package/lattice_brain/documents.py +11 -1
  28. package/lattice_brain/graph/__init__.py +28 -0
  29. package/lattice_brain/graph/_kg_common.py +1123 -0
  30. package/lattice_brain/graph/curator.py +473 -0
  31. package/lattice_brain/graph/discovery.py +1455 -0
  32. package/lattice_brain/graph/documents.py +218 -0
  33. package/lattice_brain/graph/identity.py +175 -0
  34. package/lattice_brain/graph/ingest.py +644 -0
  35. package/lattice_brain/graph/network.py +205 -0
  36. package/lattice_brain/graph/projection.py +571 -0
  37. package/lattice_brain/graph/provenance.py +401 -0
  38. package/lattice_brain/graph/retrieval.py +1341 -0
  39. package/lattice_brain/graph/schema.py +640 -0
  40. package/lattice_brain/graph/store.py +237 -0
  41. package/lattice_brain/graph/write_master.py +225 -0
  42. package/lattice_brain/identity.py +11 -13
  43. package/lattice_brain/ingest.py +11 -1
  44. package/lattice_brain/ingestion.py +318 -0
  45. package/lattice_brain/memory.py +100 -1
  46. package/lattice_brain/network.py +11 -1
  47. package/lattice_brain/portability.py +431 -0
  48. package/lattice_brain/projection.py +11 -1
  49. package/lattice_brain/provenance.py +11 -1
  50. package/lattice_brain/retrieval.py +11 -1
  51. package/lattice_brain/runtime/__init__.py +32 -0
  52. package/lattice_brain/runtime/agent_runtime.py +569 -0
  53. package/lattice_brain/runtime/hooks.py +754 -0
  54. package/lattice_brain/runtime/multi_agent.py +795 -0
  55. package/lattice_brain/schema.py +11 -1
  56. package/lattice_brain/store.py +10 -2
  57. package/lattice_brain/workflow.py +461 -0
  58. package/lattice_brain/write_master.py +11 -1
  59. package/latticeai/__init__.py +1 -1
  60. package/latticeai/api/agents.py +2 -2
  61. package/latticeai/api/browser.py +1 -1
  62. package/latticeai/api/chat.py +1 -1
  63. package/latticeai/api/computer_use.py +1 -1
  64. package/latticeai/api/hooks.py +2 -2
  65. package/latticeai/api/mcp.py +1 -1
  66. package/latticeai/api/tools.py +1 -1
  67. package/latticeai/api/workflow_designer.py +2 -2
  68. package/latticeai/app_factory.py +4 -4
  69. package/latticeai/brain/__init__.py +24 -6
  70. package/latticeai/brain/_kg_common.py +11 -1117
  71. package/latticeai/brain/context.py +12 -208
  72. package/latticeai/brain/conversations.py +12 -231
  73. package/latticeai/brain/discovery.py +13 -1451
  74. package/latticeai/brain/documents.py +13 -214
  75. package/latticeai/brain/identity.py +11 -169
  76. package/latticeai/brain/ingest.py +13 -640
  77. package/latticeai/brain/memory.py +12 -97
  78. package/latticeai/brain/network.py +12 -200
  79. package/latticeai/brain/projection.py +13 -567
  80. package/latticeai/brain/provenance.py +13 -397
  81. package/latticeai/brain/retrieval.py +13 -1337
  82. package/latticeai/brain/schema.py +12 -635
  83. package/latticeai/brain/store.py +13 -233
  84. package/latticeai/brain/write_master.py +13 -221
  85. package/latticeai/core/agent.py +1 -1
  86. package/latticeai/core/agent_registry.py +2 -2
  87. package/latticeai/core/builtin_hooks.py +2 -2
  88. package/latticeai/core/graph_curator.py +6 -468
  89. package/latticeai/core/hooks.py +6 -749
  90. package/latticeai/core/marketplace.py +1 -1
  91. package/latticeai/core/multi_agent.py +6 -790
  92. package/latticeai/core/workflow_engine.py +6 -456
  93. package/latticeai/core/workspace_os.py +1 -1
  94. package/latticeai/services/agent_runtime.py +6 -564
  95. package/latticeai/services/ingestion.py +6 -313
  96. package/latticeai/services/kg_portability.py +6 -426
  97. package/latticeai/services/platform_runtime.py +3 -3
  98. package/latticeai/services/run_executor.py +1 -1
  99. package/latticeai/services/upload_service.py +1 -1
  100. package/p_reinforce.py +1 -1
  101. package/package.json +3 -6
  102. package/scripts/build_vercel_static.mjs +77 -0
  103. package/scripts/bump_version.py +1 -1
  104. package/scripts/check_markdown_links.mjs +75 -0
  105. package/scripts/wheel_smoke.py +7 -0
  106. package/src-tauri/Cargo.lock +1 -1
  107. package/src-tauri/Cargo.toml +1 -1
  108. package/src-tauri/src/main.rs +12 -2
  109. package/src-tauri/tauri.conf.json +1 -1
  110. package/static/app/asset-manifest.json +5 -5
  111. package/static/app/assets/index-CHHal8Zl.css +2 -0
  112. package/static/app/assets/index-pdzil9ac.js +333 -0
  113. package/static/app/assets/index-pdzil9ac.js.map +1 -0
  114. package/static/app/index.html +2 -2
  115. package/latticeai/api/deps.py +0 -15
  116. package/scripts/capture/README.md +0 -28
  117. package/scripts/capture/capture_enterprise.js +0 -8
  118. package/scripts/capture/capture_graph.js +0 -8
  119. package/scripts/capture/capture_onboarding.js +0 -8
  120. package/scripts/capture/capture_page.js +0 -43
  121. package/scripts/capture/capture_release_media.js +0 -125
  122. package/scripts/capture/capture_skills.js +0 -8
  123. package/scripts/capture/capture_v340.js +0 -88
  124. package/scripts/capture/capture_workspace.js +0 -8
  125. package/scripts/generate_diagrams.py +0 -512
  126. package/scripts/release-0.3.1.sh +0 -105
  127. package/scripts/take_screenshots.js +0 -69
  128. package/static/app/assets/index-BhPuj8rT.js +0 -333
  129. package/static/app/assets/index-BhPuj8rT.js.map +0 -1
  130. package/static/app/assets/index-yZswHE3d.css +0 -2
  131. package/static/css/tokens.3ba22e37.css +0 -260
@@ -1,795 +1,11 @@
1
- """Multi-Agent Runtime 2.1.
1
+ """Compatibility shim: physically moved to ``lattice_brain.runtime.multi_agent``.
2
2
 
3
- The runtime remains a small, dependency-injected orchestrator, but v2.1 makes
4
- the operational objects first-class: handoffs, context packets, review/retry
5
- history, replayable timeline events, and explicit planning records. The default
6
- runner is still deterministic and LLM-free so tests, local demos, and Community
7
- installations can exercise the full Planner -> Executor -> Reviewer loop.
3
+ Aliases itself to the physical module so identity, module-level state, and
4
+ monkeypatching keep working through the old import path.
8
5
  """
9
6
 
10
- from __future__ import annotations
7
+ import sys
11
8
 
12
- from dataclasses import dataclass, field
13
- from datetime import datetime
14
- from typing import Any, Callable, Dict, List, Optional
9
+ import lattice_brain.runtime.multi_agent as _impl
15
10
 
16
-
17
- MULTI_AGENT_VERSION = "4.3.1"
18
-
19
- AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
20
- CORE_PIPELINE = ("planner", "executor", "reviewer")
21
-
22
- ROLE_AGENT_IDS = {
23
- "researcher": "agent:researcher",
24
- "planner": "agent:planner",
25
- "executor": "agent:executor",
26
- "reviewer": "agent:reviewer",
27
- "release": "agent:release",
28
- }
29
-
30
- HANDOFF_STATUSES = (
31
- "created",
32
- "accepted",
33
- "running",
34
- "blocked",
35
- "completed",
36
- "rejected",
37
- "retry_requested",
38
- "cancelled",
39
- )
40
-
41
- REVIEW_OUTCOMES = ("approve", "reject", "retry")
42
-
43
- _SECRET_KEYS = ("secret", "token", "password", "api_key", "apikey", "credential")
44
-
45
-
46
- def _now() -> str:
47
- return datetime.now().isoformat(timespec="seconds")
48
-
49
-
50
- def _redact(value: Any) -> Any:
51
- """Return a JSON-safe value with obvious secret fields redacted."""
52
- if isinstance(value, dict):
53
- clean: Dict[str, Any] = {}
54
- for key, item in value.items():
55
- if any(part in str(key).lower() for part in _SECRET_KEYS):
56
- clean[key] = "[redacted]"
57
- else:
58
- clean[key] = _redact(item)
59
- return clean
60
- if isinstance(value, list):
61
- return [_redact(item) for item in value[:100]]
62
- if isinstance(value, tuple):
63
- return [_redact(item) for item in value[:100]]
64
- if isinstance(value, (str, int, float, bool)) or value is None:
65
- return value
66
- return str(value)
67
-
68
-
69
- def _review_outcome(review: Dict[str, Any]) -> str:
70
- raw = str(review.get("outcome") or review.get("verdict") or "").lower().strip()
71
- if raw in {"approve", "approved", "pass", "passed", "ok"}:
72
- return "approve"
73
- if raw in {"reject", "rejected", "fail", "failed"}:
74
- return "reject"
75
- if raw == "retry":
76
- return "retry"
77
- return "approve"
78
-
79
-
80
- @dataclass
81
- class AgentContextPacket:
82
- """Structured, replay-safe context transferred between agent roles."""
83
-
84
- packet_id: str
85
- objective: str
86
- task_summary: str
87
- workspace_context: Dict[str, Any] = field(default_factory=dict)
88
- graph_context: Dict[str, Any] = field(default_factory=dict)
89
- memory_context: List[Any] = field(default_factory=list)
90
- workflow_context: Dict[str, Any] = field(default_factory=dict)
91
- plugin_outputs: List[Any] = field(default_factory=list)
92
- constraints: List[str] = field(default_factory=list)
93
- reviewer_notes: List[str] = field(default_factory=list)
94
- retry_metadata: Dict[str, Any] = field(default_factory=dict)
95
- created_at: str = field(default_factory=_now)
96
-
97
- def as_dict(self) -> Dict[str, Any]:
98
- return _redact({
99
- "packet_id": self.packet_id,
100
- "objective": self.objective,
101
- "task_summary": self.task_summary,
102
- "workspace_context": self.workspace_context,
103
- "graph_context": self.graph_context,
104
- "memory_context": self.memory_context,
105
- "workflow_context": self.workflow_context,
106
- "plugin_outputs": self.plugin_outputs,
107
- "constraints": self.constraints,
108
- "reviewer_notes": self.reviewer_notes,
109
- "retry_metadata": self.retry_metadata,
110
- "created_at": self.created_at,
111
- })
112
-
113
-
114
- @dataclass
115
- class AgentHandoff:
116
- """Inspectable handoff between two agent roles."""
117
-
118
- handoff_id: str
119
- source_agent: str
120
- target_agent: str
121
- reason: str
122
- task_summary: str
123
- context_packet: Dict[str, Any]
124
- status: str = "created"
125
- created_at: str = field(default_factory=_now)
126
- accepted_at: Optional[str] = None
127
- started_at: Optional[str] = None
128
- completed_at: Optional[str] = None
129
-
130
- def as_dict(self) -> Dict[str, Any]:
131
- return {
132
- "handoff_id": self.handoff_id,
133
- "source_agent": self.source_agent,
134
- "target_agent": self.target_agent,
135
- "reason": self.reason,
136
- "task_summary": self.task_summary,
137
- "context_packet": self.context_packet,
138
- "status": self.status,
139
- "created_at": self.created_at,
140
- "accepted_at": self.accepted_at,
141
- "started_at": self.started_at,
142
- "completed_at": self.completed_at,
143
- }
144
-
145
-
146
- @dataclass
147
- class OrchestrationContext:
148
- """Mutable carrier threaded through every role stage."""
149
-
150
- goal: str
151
- user_email: Optional[str] = None
152
- workspace_id: Optional[str] = None
153
- inputs: Dict[str, Any] = field(default_factory=dict)
154
- plan: List[Dict[str, Any]] = field(default_factory=list)
155
- plan_id: str = ""
156
- plan_review: Dict[str, Any] = field(default_factory=dict)
157
- research: List[str] = field(default_factory=list)
158
- executed: List[Dict[str, Any]] = field(default_factory=list)
159
- plugin_outputs: List[Any] = field(default_factory=list)
160
- workflow_outputs: List[Any] = field(default_factory=list)
161
- review: Dict[str, Any] = field(default_factory=dict)
162
- review_history: List[Dict[str, Any]] = field(default_factory=list)
163
- retry_history: List[Dict[str, Any]] = field(default_factory=list)
164
- timeline: List[Dict[str, Any]] = field(default_factory=list)
165
- handoffs: List[Dict[str, Any]] = field(default_factory=list)
166
- context_packets: List[Dict[str, Any]] = field(default_factory=list)
167
- memory_snapshots: List[Dict[str, Any]] = field(default_factory=list)
168
- retries: int = 0
169
- output: str = ""
170
-
171
- def build_context_packet(
172
- self,
173
- *,
174
- target_agent: Optional[str] = None,
175
- reviewer_notes: Optional[List[str]] = None,
176
- retry_metadata: Optional[Dict[str, Any]] = None,
177
- ) -> Dict[str, Any]:
178
- packet = AgentContextPacket(
179
- packet_id=f"context-packet-{len(self.context_packets) + 1}",
180
- objective=self.goal,
181
- task_summary=(self.output or self.goal or "Agent task")[:500],
182
- workspace_context={
183
- "workspace_id": self.workspace_id,
184
- "user_email": self.user_email,
185
- "target_agent": target_agent,
186
- },
187
- graph_context=_redact(self.inputs.get("graph_context") or {}),
188
- memory_context=list(self.research[:20]),
189
- workflow_context={
190
- "requested_workflow": self.inputs.get("workflow"),
191
- "workflow_outputs": self.workflow_outputs[-10:],
192
- },
193
- plugin_outputs=self.plugin_outputs[-10:],
194
- constraints=list(self.inputs.get("constraints") or []),
195
- reviewer_notes=reviewer_notes or [],
196
- retry_metadata=retry_metadata or {"retry_count": self.retries},
197
- ).as_dict()
198
- self.context_packets.append(packet)
199
- return packet
200
-
201
- def handoff(self, frm: str, to: str, note: str = "", *, status: str = "completed") -> Dict[str, Any]:
202
- if status not in HANDOFF_STATUSES:
203
- status = "completed"
204
- handoff_id = f"handoff-{len(self.handoffs) + 1}"
205
- packet = self.build_context_packet(target_agent=to)
206
- now = _now()
207
- record = AgentHandoff(
208
- handoff_id=handoff_id,
209
- source_agent=ROLE_AGENT_IDS.get(frm, f"agent:{frm}"),
210
- target_agent=ROLE_AGENT_IDS.get(to, f"agent:{to}"),
211
- reason=note or f"{frm} completed work for {to}",
212
- task_summary=(self.output or self.goal or "Agent handoff")[:500],
213
- context_packet=packet,
214
- status=status,
215
- created_at=now,
216
- accepted_at=now if status in {"accepted", "running", "completed", "retry_requested"} else None,
217
- started_at=now if status in {"running", "completed", "retry_requested"} else None,
218
- completed_at=now if status in {"completed", "retry_requested"} else None,
219
- ).as_dict()
220
- self.handoffs.append(record)
221
-
222
- self.timeline.append({
223
- "event": "handoff_created",
224
- "handoff_id": handoff_id,
225
- "from": frm,
226
- "to": to,
227
- "source_agent": record["source_agent"],
228
- "target_agent": record["target_agent"],
229
- "reason": record["reason"],
230
- "context_packet": packet,
231
- "status": "created",
232
- "timestamp": now,
233
- })
234
- if record["accepted_at"]:
235
- self.timeline.append({
236
- "event": "handoff_accepted",
237
- "handoff_id": handoff_id,
238
- "from": frm,
239
- "to": to,
240
- "status": "accepted",
241
- "timestamp": record["accepted_at"],
242
- })
243
- if status in {"completed", "retry_requested"}:
244
- self.timeline.append({
245
- "event": "handoff_completed",
246
- "handoff_id": handoff_id,
247
- "from": frm,
248
- "to": to,
249
- "status": status,
250
- "timestamp": record["completed_at"],
251
- })
252
-
253
- # Backward-compatible compact event used by v2.0 UI/tests.
254
- self.timeline.append({
255
- "event": "handoff",
256
- "handoff_id": handoff_id,
257
- "from": frm,
258
- "to": to,
259
- "note": note,
260
- "status": status,
261
- "timestamp": now,
262
- })
263
- return record
264
-
265
-
266
- @dataclass
267
- class AgentRunResult:
268
- agent_id: str
269
- status: str # ok | failed | retried_ok
270
- output: str
271
- timeline: List[Dict[str, Any]]
272
- plan: List[Dict[str, Any]]
273
- review: Dict[str, Any]
274
- roles_run: List[str]
275
- retries: int = 0
276
- handoffs: List[Dict[str, Any]] = field(default_factory=list)
277
- context_packets: List[Dict[str, Any]] = field(default_factory=list)
278
- review_history: List[Dict[str, Any]] = field(default_factory=list)
279
- retry_history: List[Dict[str, Any]] = field(default_factory=list)
280
- plan_review: Dict[str, Any] = field(default_factory=dict)
281
- memory_snapshots: List[Dict[str, Any]] = field(default_factory=list)
282
- # "simulation" = deterministic LLM-free runner; "llm" = model-driven (v4 runtime).
283
- mode: str = "simulation"
284
-
285
- def as_dict(self) -> Dict[str, Any]:
286
- return {
287
- "agent_id": self.agent_id,
288
- "mode": self.mode,
289
- "status": self.status,
290
- "output": self.output,
291
- "timeline": self.timeline,
292
- "plan": self.plan,
293
- "review": self.review,
294
- "roles_run": self.roles_run,
295
- "retries": self.retries,
296
- "handoffs": self.handoffs,
297
- "context_packets": self.context_packets,
298
- "review_history": self.review_history,
299
- "retry_history": self.retry_history,
300
- "plan_review": self.plan_review,
301
- "memory_snapshots": self.memory_snapshots,
302
- }
303
-
304
-
305
- def default_role_runner(
306
- *,
307
- workflow_runner: Optional[Callable[..., Any]] = None,
308
- plugin_runner: Optional[Callable[..., Any]] = None,
309
- context_provider: Optional[Callable[[str], List[str]]] = None,
310
- ) -> Callable[[str, OrchestrationContext], Dict[str, Any]]:
311
- """Build a deterministic, dependency-free role runner."""
312
-
313
- def runner(role: str, ctx: OrchestrationContext) -> Dict[str, Any]:
314
- if role == "researcher":
315
- found = context_provider(ctx.goal) if context_provider else []
316
- ctx.research = list(found)
317
- snapshot = {
318
- "snapshot_id": f"memory-snapshot-{len(ctx.memory_snapshots) + 1}",
319
- "scope": "short_term",
320
- "items": ctx.research[:10],
321
- "created_at": _now(),
322
- }
323
- ctx.memory_snapshots.append(snapshot)
324
- return {"role": role, "context_items": len(ctx.research), "items": ctx.research[:10], "memory_snapshot": snapshot}
325
-
326
- if role == "planner":
327
- goal = ctx.goal.strip() or "Complete the requested task"
328
- requested = ctx.inputs.get("steps")
329
- steps: List[Dict[str, Any]]
330
- if isinstance(requested, list) and requested:
331
- steps = []
332
- for i, step in enumerate(requested):
333
- if isinstance(step, dict):
334
- item = dict(step)
335
- item.setdefault("index", i)
336
- item.setdefault("description", str(step.get("description") or step.get("name") or f"Step {i + 1}"))
337
- item.setdefault("status", "planned")
338
- else:
339
- item = {"index": i, "description": str(step), "status": "planned"}
340
- steps.append(item)
341
- else:
342
- steps = [
343
- {"index": 0, "description": f"Analyze: {goal}", "status": "planned"},
344
- {"index": 1, "description": f"Execute: {goal}", "status": "planned"},
345
- {"index": 2, "description": "Verify the result", "status": "planned"},
346
- ]
347
- if ctx.inputs.get("workflow") and steps:
348
- steps[0]["workflow"] = ctx.inputs.get("workflow")
349
- if ctx.inputs.get("plugin") and steps:
350
- steps[0]["plugin"] = ctx.inputs.get("plugin")
351
- ctx.plan = steps
352
- ctx.plan_id = f"plan-{abs(hash((ctx.goal, len(steps)))) % 10_000_000}"
353
- ctx.plan_review = {
354
- "plan_id": ctx.plan_id,
355
- "outcome": "approve",
356
- "reason": "deterministic plan is bounded and executable",
357
- "reviewed_at": _now(),
358
- }
359
- return {"role": role, "plan_id": ctx.plan_id, "steps": len(steps), "plan": steps, "plan_review": ctx.plan_review}
360
-
361
- if role == "executor":
362
- results = []
363
- for step in ctx.plan:
364
- outcome: Dict[str, Any] = {"index": step["index"], "description": step["description"]}
365
- wf = step.get("workflow") or (ctx.inputs.get("workflow") if step["index"] == 0 else None)
366
- pl = step.get("plugin") or (ctx.inputs.get("plugin") if step["index"] == 0 else None)
367
- if wf and workflow_runner is not None:
368
- try:
369
- workflow_result = workflow_runner(wf, ctx)
370
- outcome["workflow_result"] = workflow_result
371
- ctx.workflow_outputs.append(workflow_result)
372
- except Exception as exc:
373
- outcome["workflow_error"] = str(exc)
374
- if pl and plugin_runner is not None:
375
- try:
376
- plugin_result = plugin_runner(pl, ctx)
377
- outcome["plugin_result"] = plugin_result
378
- ctx.plugin_outputs.append(plugin_result)
379
- except Exception as exc:
380
- outcome["plugin_error"] = str(exc)
381
- if outcome.get("workflow_error") or outcome.get("plugin_error"):
382
- step["status"] = "failed"
383
- outcome["status"] = "error"
384
- else:
385
- step["status"] = "done"
386
- outcome["status"] = "done"
387
- results.append(outcome)
388
- ctx.executed = results
389
- done = sum(1 for item in results if item.get("status") == "done")
390
- ctx.output = f"Completed {done}/{len(results)} planned step(s) for: {ctx.goal}"
391
- return {"role": role, "executed": len(results), "results": results, "plugin_outputs": ctx.plugin_outputs[-10:]}
392
-
393
- if role == "reviewer":
394
- ok = bool(ctx.executed) and all(r.get("status") == "done" for r in ctx.executed)
395
- ctx.review = {
396
- "outcome": "approve" if ok else "retry",
397
- "verdict": "pass" if ok else "retry",
398
- "reason": "all steps completed" if ok else "one or more steps failed or no steps executed",
399
- "confidence": 0.9 if ok else 0.3,
400
- "notes": [] if ok else ["executor should retry with preserved context"],
401
- "reviewed_at": _now(),
402
- }
403
- return {"role": role, **ctx.review}
404
-
405
- if role == "release":
406
- ctx.output = ctx.output or f"Released outcome for: {ctx.goal}"
407
- return {"role": role, "released": True, "summary": ctx.output}
408
-
409
- return {
410
- "role": role,
411
- "status": "skipped",
412
- "reason": "this role has no deterministic behaviour (custom agents require a loaded model)",
413
- }
414
-
415
- return runner
416
-
417
-
418
- def _extract_json_object(raw: str) -> Dict[str, Any]:
419
- """Parse one JSON object out of an LLM response (fences/prose tolerated)."""
420
- import json as _json
421
- import re as _re
422
-
423
- text = str(raw or "").strip()
424
- fenced = _re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, flags=_re.DOTALL)
425
- if fenced:
426
- text = fenced.group(1)
427
- elif not text.startswith("{"):
428
- start, end = text.find("{"), text.rfind("}")
429
- if start >= 0 and end > start:
430
- text = text[start : end + 1]
431
- parsed = _json.loads(text)
432
- if not isinstance(parsed, dict):
433
- raise ValueError("model returned JSON that is not an object")
434
- return parsed
435
-
436
-
437
- def llm_role_runner(
438
- *,
439
- generate: Callable[..., str],
440
- planner_prompt: str,
441
- critic_prompt: str,
442
- context_provider: Optional[Callable[[str], List[str]]] = None,
443
- workflow_runner: Optional[Callable[..., Any]] = None,
444
- plugin_runner: Optional[Callable[..., Any]] = None,
445
- custom_agents: Optional[Dict[str, Dict[str, Any]]] = None,
446
- ) -> Callable[[str, OrchestrationContext], Dict[str, Any]]:
447
- """Model-driven role runner — the real Multi-Agent Runtime (T7b).
448
-
449
- ``generate(message, context, max_tokens, temperature) -> str`` is a
450
- synchronous bridge to the loaded model. Honesty contract (design-review
451
- amendment): when the model responds but its plan/critique cannot be
452
- parsed, the RUN FAILS with the raw output preserved in the records —
453
- it never silently falls back to fabricated deterministic artifacts.
454
- """
455
- base = default_role_runner(
456
- workflow_runner=workflow_runner,
457
- plugin_runner=plugin_runner,
458
- context_provider=context_provider,
459
- )
460
-
461
- def _fail(ctx: OrchestrationContext, role: str, reason: str, raw: str) -> Dict[str, Any]:
462
- ctx.inputs["__llm_failure__"] = {"role": role, "reason": reason, "raw": raw[:2000]}
463
- ctx.review = {
464
- "outcome": "reject",
465
- "verdict": "fail",
466
- "reason": f"{role}: {reason}",
467
- "raw_output": raw[:2000],
468
- "reviewed_at": _now(),
469
- }
470
- return {"role": role, "status": "error", "reason": reason, "raw": raw[:2000]}
471
-
472
- def runner(role: str, ctx: OrchestrationContext) -> Dict[str, Any]:
473
- failure = ctx.inputs.get("__llm_failure__")
474
-
475
- custom = (custom_agents or {}).get(role)
476
- if custom is not None:
477
- # Executable registry entry (T7e): the agent's persisted config
478
- # (system_prompt, max_tokens, temperature) is actually loaded.
479
- cfg = custom.get("config") or {}
480
- system = str(
481
- cfg.get("system_prompt")
482
- or custom.get("description")
483
- or f"You are {custom.get('name') or role}."
484
- )
485
- try:
486
- out = str(generate(
487
- ctx.output or ctx.goal,
488
- context=system,
489
- max_tokens=int(cfg.get("max_tokens") or 1024),
490
- temperature=float(cfg.get("temperature") or 0.2),
491
- ))
492
- except Exception as exc:
493
- return _fail(ctx, role, f"custom agent generation failed ({exc})", "")
494
- ctx.output = out
495
- return {"role": role, "agent": custom.get("name"), "status": "ok",
496
- "output": out[:2000]}
497
-
498
- if role == "planner":
499
- research = "\n".join(f"- {item}" for item in (ctx.research or [])[:8])
500
- raw = generate(
501
- "Produce a JSON execution plan for this goal. Respond with one JSON "
502
- 'object: {"goal": str, "steps": [{"description": str}, ...]} and nothing else.',
503
- context=f"{planner_prompt}\n\nGoal: {ctx.goal}\n\nKnown context:\n{research}",
504
- max_tokens=1024,
505
- temperature=0.1,
506
- )
507
- try:
508
- parsed = _extract_json_object(str(raw))
509
- except Exception as exc:
510
- return _fail(ctx, role, f"plan output unparseable ({exc})", str(raw))
511
- steps = []
512
- for i, step in enumerate(parsed.get("steps") or []):
513
- description = step.get("description") if isinstance(step, dict) else str(step)
514
- steps.append({"index": i, "description": str(description or f"Step {i + 1}"), "status": "planned"})
515
- if not steps:
516
- return _fail(ctx, role, "model returned a plan with no steps", str(raw))
517
- if ctx.inputs.get("workflow"):
518
- steps[0]["workflow"] = ctx.inputs.get("workflow")
519
- if ctx.inputs.get("plugin"):
520
- steps[0]["plugin"] = ctx.inputs.get("plugin")
521
- ctx.plan = steps
522
- ctx.plan_id = f"plan-{abs(hash((ctx.goal, len(steps)))) % 10_000_000}"
523
- ctx.plan_review = {
524
- "plan_id": ctx.plan_id,
525
- "outcome": "approve",
526
- "reason": "model-generated plan parsed and bounded",
527
- "reviewed_at": _now(),
528
- }
529
- return {"role": role, "plan_id": ctx.plan_id, "steps": len(steps), "plan": steps, "plan_review": ctx.plan_review}
530
-
531
- if role == "executor":
532
- if failure:
533
- return {"role": role, "status": "error", "reason": f"skipped — {failure['role']} failed"}
534
- results = []
535
- for step in ctx.plan:
536
- outcome: Dict[str, Any] = {"index": step["index"], "description": step["description"]}
537
- wf = step.get("workflow")
538
- pl = step.get("plugin")
539
- if wf and workflow_runner is not None:
540
- try:
541
- outcome["workflow_result"] = workflow_runner(wf, ctx)
542
- ctx.workflow_outputs.append(outcome["workflow_result"])
543
- except Exception as exc:
544
- outcome["workflow_error"] = str(exc)
545
- if pl and plugin_runner is not None:
546
- try:
547
- outcome["plugin_result"] = plugin_runner(pl, ctx)
548
- ctx.plugin_outputs.append(outcome["plugin_result"])
549
- except Exception as exc:
550
- outcome["plugin_error"] = str(exc)
551
- try:
552
- outcome["result"] = str(generate(
553
- f"Execute this step and return the concrete result only.\n"
554
- f"Goal: {ctx.goal}\nStep: {step['description']}",
555
- context="",
556
- max_tokens=1024,
557
- temperature=0.2,
558
- ))[:4000]
559
- except Exception as exc:
560
- outcome["error"] = str(exc)
561
- if outcome.get("workflow_error") or outcome.get("plugin_error") or outcome.get("error"):
562
- step["status"] = "failed"
563
- outcome["status"] = "error"
564
- else:
565
- step["status"] = "done"
566
- outcome["status"] = "done"
567
- results.append(outcome)
568
- ctx.executed = results
569
- done = [r for r in results if r.get("status") == "done"]
570
- ctx.output = "\n\n".join(str(r.get("result") or "") for r in done).strip() or (
571
- f"Completed {len(done)}/{len(results)} step(s) for: {ctx.goal}"
572
- )
573
- return {"role": role, "executed": len(results), "results": results}
574
-
575
- if role == "reviewer":
576
- if failure:
577
- # Fail-closed: an upstream unparseable model output means this
578
- # run is failed, with the raw output preserved — never rescued
579
- # by a rubber-stamp review.
580
- ctx.review = {
581
- "outcome": "reject",
582
- "verdict": "fail",
583
- "reason": f"{failure['role']} output unparseable",
584
- "raw_output": failure.get("raw"),
585
- "reviewed_at": _now(),
586
- }
587
- return {"role": role, **ctx.review}
588
- raw = generate(
589
- "Review this execution. Respond with one JSON object: "
590
- '{"approve": bool, "reason": str} and nothing else.',
591
- context=(
592
- f"{critic_prompt}\n\nGoal: {ctx.goal}\n\n"
593
- f"Steps: {[s.get('status') for s in ctx.plan]}\n\nOutput:\n{(ctx.output or '')[:3000]}"
594
- ),
595
- max_tokens=512,
596
- temperature=0.1,
597
- )
598
- try:
599
- parsed = _extract_json_object(str(raw))
600
- approve = bool(parsed.get("approve"))
601
- reason = str(parsed.get("reason") or "")
602
- except Exception as exc:
603
- ctx.review = {
604
- "outcome": "reject",
605
- "verdict": "fail",
606
- "reason": f"critic output unparseable ({exc})",
607
- "raw_output": str(raw)[:2000],
608
- "reviewed_at": _now(),
609
- }
610
- return {"role": role, **ctx.review}
611
- ctx.review = {
612
- "outcome": "approve" if approve else "retry",
613
- "verdict": "pass" if approve else "retry",
614
- "reason": reason or ("model approved the result" if approve else "model requested a retry"),
615
- "confidence": 0.9 if approve else 0.4,
616
- "notes": [],
617
- "reviewed_at": _now(),
618
- }
619
- return {"role": role, **ctx.review}
620
-
621
- # researcher / release / anything else: the deterministic behaviour is
622
- # real work (memory recall, bookkeeping) — reuse it.
623
- return base(role, ctx)
624
-
625
- return runner
626
-
627
-
628
- class MultiAgentOrchestrator:
629
- """Drives a role pipeline with handoff, planning, review, and retry."""
630
-
631
- def __init__(
632
- self,
633
- role_runner: Optional[Callable[[str, OrchestrationContext], Dict[str, Any]]] = None,
634
- mode: str = "simulation",
635
- custom_agents: Optional[Dict[str, Dict[str, Any]]] = None,
636
- ):
637
- self.role_runner = role_runner or default_role_runner()
638
- # Executable registry entries (T7e): a requested role may be a
639
- # registered custom agent id; its config (system_prompt, …) is
640
- # actually loaded at run time — registration is no longer a UI illusion.
641
- self.custom_agents = dict(custom_agents or {})
642
- # Honest execution-mode label persisted on every run record. The
643
- # built-in runner never calls a model, so the default is "simulation";
644
- # an LLM-backed runner must declare mode="llm" explicitly.
645
- self.mode = mode
646
-
647
- def _run_role(self, role: str, ctx: OrchestrationContext) -> Dict[str, Any]:
648
- started = _now()
649
- if role == "reviewer":
650
- ctx.timeline.append({
651
- "event": "review_requested",
652
- "role": role,
653
- "agent_id": ROLE_AGENT_IDS.get(role, f"agent:{role}"),
654
- "timestamp": started,
655
- })
656
- try:
657
- result = self.role_runner(role, ctx) or {}
658
- status = result.get("status", "ok")
659
- except Exception as exc:
660
- result = {"error": str(exc)}
661
- status = "error"
662
- if role == "reviewer":
663
- review = dict(ctx.review or result)
664
- outcome = _review_outcome(review)
665
- event = {
666
- "approve": "review_approved",
667
- "reject": "review_rejected",
668
- "retry": "retry_requested",
669
- }[outcome]
670
- ctx.timeline.append({
671
- "event": event,
672
- "role": role,
673
- "agent_id": ROLE_AGENT_IDS.get(role, f"agent:{role}"),
674
- "outcome": outcome,
675
- "reason": review.get("reason", ""),
676
- "review": review,
677
- "timestamp": _now(),
678
- })
679
- ctx.timeline.append({
680
- "event": "role",
681
- "role": role,
682
- "agent_id": ROLE_AGENT_IDS.get(role, f"agent:{role}"),
683
- "status": status,
684
- "result": result,
685
- "started_at": started,
686
- "timestamp": _now(),
687
- })
688
- return result
689
-
690
- def run(
691
- self,
692
- goal: str,
693
- *,
694
- user_email: Optional[str] = None,
695
- workspace_id: Optional[str] = None,
696
- inputs: Optional[Dict[str, Any]] = None,
697
- roles: Optional[List[str]] = None,
698
- max_retries: int = 2,
699
- ) -> AgentRunResult:
700
- ctx = OrchestrationContext(
701
- goal=goal or "",
702
- user_email=user_email,
703
- workspace_id=workspace_id,
704
- inputs=inputs or {},
705
- )
706
- pipeline = [
707
- r for r in (roles or list(CORE_PIPELINE))
708
- if r in AGENT_ROLES or r in self.custom_agents
709
- ]
710
- if not pipeline:
711
- pipeline = list(CORE_PIPELINE)
712
- max_retries = max(0, int(max_retries or 0))
713
-
714
- ctx.timeline.append({"event": "start", "goal": ctx.goal, "pipeline": pipeline, "timestamp": _now()})
715
- ctx.timeline.append({
716
- "event": "agent_started",
717
- "agent_id": ROLE_AGENT_IDS.get(pipeline[0], "agent:planner"),
718
- "goal": ctx.goal,
719
- "pipeline": pipeline,
720
- "workspace_id": workspace_id,
721
- "timestamp": _now(),
722
- })
723
-
724
- roles_run: List[str] = []
725
- previous: Optional[str] = None
726
- index = 0
727
- while index < len(pipeline):
728
- role = pipeline[index]
729
- if previous is not None:
730
- ctx.handoff(previous, role)
731
- self._run_role(role, ctx)
732
- roles_run.append(role)
733
-
734
- if role == "reviewer":
735
- review = dict(ctx.review or {})
736
- outcome = _review_outcome(review)
737
- review_entry = {
738
- "index": len(ctx.review_history),
739
- "outcome": outcome,
740
- "verdict": review.get("verdict") or ("pass" if outcome == "approve" else outcome),
741
- "reason": review.get("reason", ""),
742
- "notes": review.get("notes") or review.get("reviewer_notes") or [],
743
- "retry_count": ctx.retries,
744
- "timestamp": _now(),
745
- }
746
- ctx.review_history.append(review_entry)
747
- if outcome == "retry" and ctx.retries < max_retries:
748
- ctx.retries += 1
749
- retry_entry = {
750
- "retry": ctx.retries,
751
- "limit": max_retries,
752
- "reason": review_entry["reason"],
753
- "reviewer_notes": review_entry["notes"],
754
- "timestamp": _now(),
755
- }
756
- ctx.retry_history.append(retry_entry)
757
- exec_index = pipeline.index("executor") if "executor" in pipeline else None
758
- if exec_index is not None:
759
- ctx.handoff("reviewer", "executor", note=f"retry #{ctx.retries}: {review_entry['reason']}", status="retry_requested")
760
- index = exec_index
761
- previous = "reviewer"
762
- continue
763
- if outcome == "reject":
764
- ctx.timeline.append({"event": "execution_failed", "reason": review_entry["reason"], "timestamp": _now()})
765
- break
766
-
767
- previous = role
768
- index += 1
769
-
770
- final_outcome = _review_outcome(ctx.review or {})
771
- if final_outcome == "approve":
772
- status = "retried_ok" if ctx.retries else "ok"
773
- else:
774
- status = "failed"
775
- if status == "failed":
776
- ctx.timeline.append({"event": "execution_failed", "status": status, "retries": ctx.retries, "timestamp": _now()})
777
- ctx.timeline.append({"event": "end", "status": status, "retries": ctx.retries, "timestamp": _now()})
778
-
779
- return AgentRunResult(
780
- agent_id=ROLE_AGENT_IDS.get("executor", "agent:executor"),
781
- status=status,
782
- output=ctx.output or f"Processed goal: {ctx.goal}",
783
- timeline=ctx.timeline,
784
- plan=ctx.plan,
785
- review=ctx.review,
786
- roles_run=roles_run,
787
- retries=ctx.retries,
788
- handoffs=ctx.handoffs,
789
- context_packets=ctx.context_packets,
790
- review_history=ctx.review_history,
791
- retry_history=ctx.retry_history,
792
- plan_review=ctx.plan_review,
793
- memory_snapshots=ctx.memory_snapshots,
794
- mode=self.mode,
795
- )
11
+ sys.modules[__name__] = _impl