ltcai 4.3.3 → 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.
- package/README.md +21 -16
- package/docs/CHANGELOG.md +37 -0
- package/docs/V4_4_0_EXTRACTION_REPORT.md +239 -0
- package/lattice_brain/__init__.py +38 -23
- package/lattice_brain/_kg_common.py +11 -1
- package/lattice_brain/context.py +212 -2
- package/lattice_brain/conversations.py +234 -1
- package/lattice_brain/discovery.py +11 -1
- package/lattice_brain/documents.py +11 -1
- package/lattice_brain/graph/__init__.py +28 -0
- package/lattice_brain/graph/_kg_common.py +1123 -0
- package/lattice_brain/graph/curator.py +473 -0
- package/lattice_brain/graph/discovery.py +1455 -0
- package/lattice_brain/graph/documents.py +218 -0
- package/lattice_brain/graph/identity.py +175 -0
- package/lattice_brain/graph/ingest.py +644 -0
- package/lattice_brain/graph/network.py +205 -0
- package/lattice_brain/graph/projection.py +571 -0
- package/lattice_brain/graph/provenance.py +401 -0
- package/lattice_brain/graph/retrieval.py +1341 -0
- package/lattice_brain/graph/schema.py +640 -0
- package/lattice_brain/graph/store.py +237 -0
- package/lattice_brain/graph/write_master.py +225 -0
- package/lattice_brain/identity.py +11 -13
- package/lattice_brain/ingest.py +11 -1
- package/lattice_brain/ingestion.py +318 -0
- package/lattice_brain/memory.py +100 -1
- package/lattice_brain/network.py +11 -1
- package/lattice_brain/portability.py +431 -0
- package/lattice_brain/projection.py +11 -1
- package/lattice_brain/provenance.py +11 -1
- package/lattice_brain/retrieval.py +11 -1
- package/lattice_brain/runtime/__init__.py +32 -0
- package/lattice_brain/runtime/agent_runtime.py +569 -0
- package/lattice_brain/runtime/hooks.py +754 -0
- package/lattice_brain/runtime/multi_agent.py +795 -0
- package/lattice_brain/schema.py +11 -1
- package/lattice_brain/store.py +10 -2
- package/lattice_brain/workflow.py +461 -0
- package/lattice_brain/write_master.py +11 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +2 -2
- package/latticeai/api/browser.py +1 -1
- package/latticeai/api/chat.py +1 -1
- package/latticeai/api/computer_use.py +1 -1
- package/latticeai/api/hooks.py +2 -2
- package/latticeai/api/mcp.py +1 -1
- package/latticeai/api/tools.py +1 -1
- package/latticeai/api/workflow_designer.py +2 -2
- package/latticeai/app_factory.py +4 -4
- package/latticeai/brain/__init__.py +24 -6
- package/latticeai/brain/_kg_common.py +11 -1117
- package/latticeai/brain/context.py +12 -208
- package/latticeai/brain/conversations.py +12 -231
- package/latticeai/brain/discovery.py +13 -1451
- package/latticeai/brain/documents.py +13 -214
- package/latticeai/brain/identity.py +11 -169
- package/latticeai/brain/ingest.py +13 -640
- package/latticeai/brain/memory.py +12 -97
- package/latticeai/brain/network.py +12 -200
- package/latticeai/brain/projection.py +13 -567
- package/latticeai/brain/provenance.py +13 -397
- package/latticeai/brain/retrieval.py +13 -1337
- package/latticeai/brain/schema.py +12 -635
- package/latticeai/brain/store.py +13 -233
- package/latticeai/brain/write_master.py +13 -221
- package/latticeai/core/agent.py +1 -1
- package/latticeai/core/agent_registry.py +2 -2
- package/latticeai/core/builtin_hooks.py +2 -2
- package/latticeai/core/graph_curator.py +6 -468
- package/latticeai/core/hooks.py +6 -749
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +6 -790
- package/latticeai/core/workflow_engine.py +6 -456
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/agent_runtime.py +6 -564
- package/latticeai/services/ingestion.py +6 -313
- package/latticeai/services/kg_portability.py +6 -426
- package/latticeai/services/platform_runtime.py +3 -3
- package/latticeai/services/run_executor.py +1 -1
- package/latticeai/services/upload_service.py +1 -1
- package/p_reinforce.py +1 -1
- package/package.json +1 -1
- package/scripts/bump_version.py +1 -1
- package/scripts/wheel_smoke.py +7 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +1 -1
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
"""Multi-Agent Runtime 2.1.
|
|
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.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
MULTI_AGENT_VERSION = "4.4.0"
|
|
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
|
+
)
|