nexo-brain 2.7.0 → 3.0.1

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 (50) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +66 -12
  3. package/hooks/hooks.json +79 -0
  4. package/package.json +1 -1
  5. package/src/agent_runner.py +295 -7
  6. package/src/cli.py +111 -0
  7. package/src/client_preferences.py +99 -1
  8. package/src/client_sync.py +207 -3
  9. package/src/cognitive/__init__.py +1 -1
  10. package/src/cognitive/_search.py +39 -19
  11. package/src/dashboard/app.py +141 -1
  12. package/src/dashboard/templates/base.html +4 -0
  13. package/src/dashboard/templates/protocol.html +199 -0
  14. package/src/db/__init__.py +23 -1
  15. package/src/db/_learnings.py +31 -4
  16. package/src/db/_personal_scripts.py +12 -0
  17. package/src/db/_protocol.py +303 -0
  18. package/src/db/_schema.py +248 -0
  19. package/src/db/_watchers.py +173 -0
  20. package/src/db/_workflow.py +952 -0
  21. package/src/doctor/providers/boot.py +45 -19
  22. package/src/doctor/providers/runtime.py +923 -8
  23. package/src/evolution_cycle.py +62 -0
  24. package/src/hook_guardrails.py +308 -0
  25. package/src/hooks/protocol-guardrail.sh +10 -0
  26. package/src/nexo_sdk.py +103 -0
  27. package/src/plugins/cognitive_memory.py +18 -0
  28. package/src/plugins/cortex.py +55 -35
  29. package/src/plugins/guard.py +132 -16
  30. package/src/plugins/protocol.py +911 -0
  31. package/src/plugins/schedule.py +40 -6
  32. package/src/plugins/simple_api.py +103 -0
  33. package/src/plugins/skills.py +67 -0
  34. package/src/plugins/state_watchers.py +79 -0
  35. package/src/plugins/workflow.py +588 -0
  36. package/src/public_contribution.py +86 -12
  37. package/src/requirements.txt +1 -0
  38. package/src/script_registry.py +142 -0
  39. package/src/scripts/deep-sleep/apply_findings.py +204 -0
  40. package/src/scripts/deep-sleep/collect.py +49 -4
  41. package/src/scripts/nexo-agent-run.py +2 -0
  42. package/src/scripts/nexo-daily-self-audit.py +843 -5
  43. package/src/scripts/nexo-evolution-run.py +343 -1
  44. package/src/server.py +92 -6
  45. package/src/skills_runtime.py +151 -0
  46. package/src/state_watchers_runtime.py +334 -0
  47. package/src/tools_learnings.py +345 -7
  48. package/src/tools_sessions.py +183 -0
  49. package/templates/CLAUDE.md.template +9 -1
  50. package/templates/CODEX.AGENTS.md.template +10 -2
@@ -0,0 +1,952 @@
1
+ from __future__ import annotations
2
+ """NEXO DB — Durable workflow runtime."""
3
+
4
+ import json
5
+ import secrets
6
+ import time
7
+ from datetime import datetime, timezone
8
+
9
+ from db._core import get_db
10
+
11
+ RUN_STATUSES = {
12
+ "open",
13
+ "running",
14
+ "blocked",
15
+ "waiting_approval",
16
+ "failed",
17
+ "completed",
18
+ "cancelled",
19
+ }
20
+ STEP_STATUSES = {
21
+ "pending",
22
+ "running",
23
+ "blocked",
24
+ "waiting_approval",
25
+ "failed",
26
+ "completed",
27
+ "skipped",
28
+ "retrying",
29
+ }
30
+ GOAL_STATUSES = {
31
+ "active",
32
+ "blocked",
33
+ "abandoned",
34
+ "completed",
35
+ "cancelled",
36
+ }
37
+ PRIORITIES = {"low", "normal", "high", "critical"}
38
+ RUN_CLOSED_STATUSES = {"completed", "failed", "cancelled"}
39
+ GOAL_CLOSED_STATUSES = {"abandoned", "completed", "cancelled"}
40
+
41
+
42
+ def _workflow_run_id() -> str:
43
+ return f"WF-{int(time.time())}-{secrets.randbelow(100000)}"
44
+
45
+
46
+ def _workflow_goal_id() -> str:
47
+ return f"WG-{int(time.time())}-{secrets.randbelow(100000)}"
48
+
49
+
50
+ def _now_sql() -> str:
51
+ return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
52
+
53
+
54
+ def _as_json(value, default):
55
+ if value is None:
56
+ value = default
57
+ if isinstance(value, str):
58
+ return value
59
+ return json.dumps(value, ensure_ascii=False)
60
+
61
+
62
+ def _parse_json(value, default):
63
+ if value in (None, ""):
64
+ return default
65
+ if isinstance(value, (dict, list)):
66
+ return value
67
+ try:
68
+ return json.loads(value)
69
+ except Exception:
70
+ return default
71
+
72
+
73
+ def _row_to_step(row) -> dict:
74
+ step = dict(row)
75
+ step["human_gate"] = bool(step.get("human_gate"))
76
+ step["requires_approval"] = bool(step.get("requires_approval"))
77
+ step["last_state_patch"] = _parse_json(step.get("last_state_patch"), {})
78
+ return step
79
+
80
+
81
+ def _goal_closed_at(status: str) -> str | None:
82
+ return _now_sql() if status in GOAL_CLOSED_STATUSES else None
83
+
84
+
85
+ def _row_to_goal(row, *, include_runs: bool = False) -> dict:
86
+ goal = dict(row)
87
+ goal["shared_state"] = _parse_json(goal.get("shared_state"), {})
88
+ goal["open_run_count"] = int(goal.get("open_run_count") or 0)
89
+ goal["run_count"] = int(goal.get("run_count") or 0)
90
+ goal["child_count"] = int(goal.get("child_count") or 0)
91
+ if include_runs:
92
+ goal["runs"] = list_workflow_runs(goal_id=goal["goal_id"], include_closed=True, limit=50)
93
+ return goal
94
+
95
+
96
+ def get_workflow_goal(goal_id: str, *, include_runs: bool = False) -> dict | None:
97
+ conn = get_db()
98
+ row = conn.execute(
99
+ """SELECT g.*,
100
+ COALESCE((SELECT COUNT(*) FROM workflow_runs r WHERE r.goal_id = g.goal_id), 0) AS run_count,
101
+ COALESCE((SELECT COUNT(*) FROM workflow_runs r WHERE r.goal_id = g.goal_id
102
+ AND r.status NOT IN ('completed', 'failed', 'cancelled')), 0) AS open_run_count,
103
+ COALESCE((SELECT COUNT(*) FROM workflow_goals child WHERE child.parent_goal_id = g.goal_id), 0) AS child_count
104
+ FROM workflow_goals g
105
+ WHERE g.goal_id = ?""",
106
+ (goal_id.strip(),),
107
+ ).fetchone()
108
+ return _row_to_goal(row, include_runs=include_runs) if row else None
109
+
110
+
111
+ def list_workflow_steps(run_id: str) -> list[dict]:
112
+ conn = get_db()
113
+ rows = conn.execute(
114
+ """SELECT *
115
+ FROM workflow_steps
116
+ WHERE run_id = ?
117
+ ORDER BY step_index ASC, id ASC""",
118
+ (run_id,),
119
+ ).fetchall()
120
+ return [_row_to_step(row) for row in rows]
121
+
122
+
123
+ def _row_to_run(row, *, include_steps: bool = True) -> dict:
124
+ run = dict(row)
125
+ run["shared_state"] = _parse_json(run.get("shared_state"), {})
126
+ if include_steps:
127
+ run["steps"] = list_workflow_steps(run["run_id"])
128
+ return run
129
+
130
+
131
+ def get_workflow_run(run_id: str, *, include_steps: bool = True) -> dict | None:
132
+ conn = get_db()
133
+ row = conn.execute(
134
+ "SELECT * FROM workflow_runs WHERE run_id = ?",
135
+ (run_id.strip(),),
136
+ ).fetchone()
137
+ return _row_to_run(row, include_steps=include_steps) if row else None
138
+
139
+
140
+ def create_workflow_goal(
141
+ session_id: str,
142
+ title: str,
143
+ *,
144
+ objective: str = "",
145
+ parent_goal_id: str = "",
146
+ priority: str = "normal",
147
+ owner: str = "",
148
+ next_action: str = "",
149
+ success_signal: str = "",
150
+ shared_state=None,
151
+ ) -> dict:
152
+ conn = get_db()
153
+ clean_parent_goal_id = parent_goal_id.strip()
154
+ if clean_parent_goal_id:
155
+ parent = get_workflow_goal(clean_parent_goal_id)
156
+ if not parent:
157
+ raise ValueError(f"Unknown parent_goal_id: {clean_parent_goal_id}")
158
+
159
+ goal_id = _workflow_goal_id()
160
+ clean_priority = priority if priority in PRIORITIES else "normal"
161
+ conn.execute(
162
+ """INSERT INTO workflow_goals (
163
+ goal_id, session_id, title, objective, parent_goal_id, status,
164
+ priority, owner, next_action, success_signal, blocker_reason, shared_state
165
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
166
+ (
167
+ goal_id,
168
+ session_id.strip(),
169
+ title.strip(),
170
+ objective.strip(),
171
+ clean_parent_goal_id,
172
+ "active",
173
+ clean_priority,
174
+ owner.strip(),
175
+ next_action.strip(),
176
+ success_signal.strip(),
177
+ "",
178
+ _as_json(shared_state, {}),
179
+ ),
180
+ )
181
+ conn.commit()
182
+ return get_workflow_goal(goal_id) or {"goal_id": goal_id}
183
+
184
+
185
+ def update_workflow_goal(
186
+ goal_id: str,
187
+ *,
188
+ status: str = "",
189
+ title: str = "",
190
+ objective: str = "",
191
+ parent_goal_id: str = "",
192
+ owner: str = "",
193
+ next_action: str = "",
194
+ success_signal: str = "",
195
+ blocker_reason: str = "",
196
+ shared_state=None,
197
+ ) -> dict | None:
198
+ conn = get_db()
199
+ row = conn.execute("SELECT * FROM workflow_goals WHERE goal_id = ?", (goal_id.strip(),)).fetchone()
200
+ if not row:
201
+ return None
202
+ goal = dict(row)
203
+
204
+ clean_status = status.strip().lower() if status.strip().lower() in GOAL_STATUSES else goal["status"]
205
+ clean_parent_goal_id = parent_goal_id.strip() if parent_goal_id else goal.get("parent_goal_id", "")
206
+ if clean_parent_goal_id and clean_parent_goal_id != goal_id.strip():
207
+ parent = get_workflow_goal(clean_parent_goal_id)
208
+ if not parent:
209
+ raise ValueError(f"Unknown parent_goal_id: {clean_parent_goal_id}")
210
+ elif clean_parent_goal_id == goal_id.strip():
211
+ raise ValueError("parent_goal_id cannot equal goal_id")
212
+
213
+ effective_shared_state = goal["shared_state"] if shared_state is None else _as_json(shared_state, {})
214
+ effective_blocker = blocker_reason.strip() if blocker_reason else goal.get("blocker_reason", "")
215
+ if clean_status != "blocked" and blocker_reason.strip() == "":
216
+ effective_blocker = ""
217
+
218
+ conn.execute(
219
+ """UPDATE workflow_goals
220
+ SET title = ?,
221
+ objective = ?,
222
+ parent_goal_id = ?,
223
+ status = ?,
224
+ owner = ?,
225
+ next_action = ?,
226
+ success_signal = ?,
227
+ blocker_reason = ?,
228
+ shared_state = ?,
229
+ updated_at = datetime('now'),
230
+ closed_at = ?
231
+ WHERE goal_id = ?""",
232
+ (
233
+ title.strip() or goal["title"],
234
+ objective.strip() or goal.get("objective", ""),
235
+ clean_parent_goal_id,
236
+ clean_status,
237
+ owner.strip() or goal.get("owner", ""),
238
+ next_action.strip() or goal.get("next_action", ""),
239
+ success_signal.strip() or goal.get("success_signal", ""),
240
+ effective_blocker,
241
+ effective_shared_state,
242
+ _goal_closed_at(clean_status),
243
+ goal_id.strip(),
244
+ ),
245
+ )
246
+ conn.commit()
247
+ return get_workflow_goal(goal_id.strip())
248
+
249
+
250
+ def create_workflow_goal(
251
+ session_id: str,
252
+ title: str,
253
+ *,
254
+ objective: str = "",
255
+ parent_goal_id: str = "",
256
+ priority: str = "normal",
257
+ owner: str = "",
258
+ next_action: str = "",
259
+ success_signal: str = "",
260
+ shared_state=None,
261
+ ) -> dict:
262
+ conn = get_db()
263
+ clean_parent_goal_id = parent_goal_id.strip()
264
+ if clean_parent_goal_id and not get_workflow_goal(clean_parent_goal_id, include_runs=False):
265
+ raise ValueError(f"Unknown parent_goal_id: {clean_parent_goal_id}")
266
+
267
+ goal_id = _workflow_goal_id()
268
+ clean_priority = priority if priority in PRIORITIES else "normal"
269
+ conn.execute(
270
+ """INSERT INTO workflow_goals (
271
+ goal_id, session_id, title, objective, parent_goal_id,
272
+ status, priority, owner, next_action, success_signal, shared_state
273
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
274
+ (
275
+ goal_id,
276
+ session_id.strip(),
277
+ title.strip(),
278
+ objective.strip(),
279
+ clean_parent_goal_id,
280
+ "active",
281
+ clean_priority,
282
+ owner.strip(),
283
+ next_action.strip(),
284
+ success_signal.strip(),
285
+ _as_json(shared_state, {}),
286
+ ),
287
+ )
288
+ conn.commit()
289
+ goal = get_workflow_goal(goal_id)
290
+ return goal or {"goal_id": goal_id, "status": "active"}
291
+
292
+
293
+ def update_workflow_goal(
294
+ goal_id: str,
295
+ *,
296
+ status: str = "",
297
+ title: str = "",
298
+ objective: str = "",
299
+ parent_goal_id: str = "",
300
+ owner: str = "",
301
+ next_action: str = "",
302
+ success_signal: str = "",
303
+ blocker_reason: str = "",
304
+ shared_state=None,
305
+ ) -> dict | None:
306
+ conn = get_db()
307
+ row = conn.execute("SELECT * FROM workflow_goals WHERE goal_id = ?", (goal_id.strip(),)).fetchone()
308
+ if not row:
309
+ return None
310
+
311
+ goal = dict(row)
312
+ clean_status = status.strip().lower() if status.strip().lower() in GOAL_STATUSES else goal["status"]
313
+ clean_parent_goal_id = parent_goal_id.strip() if parent_goal_id.strip() else goal.get("parent_goal_id", "")
314
+ if clean_parent_goal_id and clean_parent_goal_id != goal_id.strip() and not get_workflow_goal(clean_parent_goal_id):
315
+ raise ValueError(f"Unknown parent_goal_id: {clean_parent_goal_id}")
316
+ if clean_parent_goal_id == goal_id.strip():
317
+ raise ValueError("goal_id cannot be its own parent")
318
+
319
+ effective_shared_state = goal["shared_state"]
320
+ if shared_state is not None:
321
+ effective_shared_state = _as_json(shared_state, {})
322
+
323
+ conn.execute(
324
+ """UPDATE workflow_goals
325
+ SET title = ?,
326
+ objective = ?,
327
+ parent_goal_id = ?,
328
+ status = ?,
329
+ owner = ?,
330
+ next_action = ?,
331
+ success_signal = ?,
332
+ blocker_reason = ?,
333
+ shared_state = ?,
334
+ updated_at = datetime('now'),
335
+ closed_at = ?
336
+ WHERE goal_id = ?""",
337
+ (
338
+ title.strip() or goal["title"],
339
+ objective.strip() or goal.get("objective", ""),
340
+ clean_parent_goal_id,
341
+ clean_status,
342
+ owner.strip() or goal.get("owner", ""),
343
+ next_action.strip() or goal.get("next_action", ""),
344
+ success_signal.strip() or goal.get("success_signal", ""),
345
+ blocker_reason.strip() or goal.get("blocker_reason", ""),
346
+ effective_shared_state,
347
+ _goal_closed_at(clean_status),
348
+ goal_id.strip(),
349
+ ),
350
+ )
351
+ conn.commit()
352
+ return get_workflow_goal(goal_id.strip())
353
+
354
+
355
+ def list_workflow_goals(*, status: str = "", include_closed: bool = False, limit: int = 20) -> list[dict]:
356
+ conn = get_db()
357
+ clauses = []
358
+ params: list[object] = []
359
+ if status.strip() in GOAL_STATUSES:
360
+ clauses.append("g.status = ?")
361
+ params.append(status.strip())
362
+ elif not include_closed:
363
+ clauses.append("g.status NOT IN ('abandoned', 'completed', 'cancelled')")
364
+ where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
365
+ rows = conn.execute(
366
+ f"""SELECT g.*,
367
+ COALESCE((SELECT COUNT(*) FROM workflow_runs r WHERE r.goal_id = g.goal_id), 0) AS run_count,
368
+ COALESCE((SELECT COUNT(*) FROM workflow_runs r WHERE r.goal_id = g.goal_id
369
+ AND r.status NOT IN ('completed', 'failed', 'cancelled')), 0) AS open_run_count,
370
+ COALESCE((SELECT COUNT(*) FROM workflow_goals child WHERE child.parent_goal_id = g.goal_id), 0) AS child_count
371
+ FROM workflow_goals g
372
+ {where}
373
+ ORDER BY g.updated_at DESC, g.opened_at DESC
374
+ LIMIT ?""",
375
+ params + [max(1, int(limit))],
376
+ ).fetchall()
377
+ return [_row_to_goal(row, include_runs=False) for row in rows]
378
+
379
+
380
+ def _find_reusable_run(session_id: str, idempotency_key: str, goal_id: str = "") -> dict | None:
381
+ if not session_id.strip() or not idempotency_key.strip():
382
+ return None
383
+ conn = get_db()
384
+ clauses = [
385
+ "session_id = ?",
386
+ "idempotency_key = ?",
387
+ "status NOT IN ('completed', 'failed', 'cancelled')",
388
+ ]
389
+ params: list[object] = [session_id.strip(), idempotency_key.strip()]
390
+ if goal_id.strip():
391
+ clauses.append("goal_id = ?")
392
+ params.append(goal_id.strip())
393
+ row = conn.execute(
394
+ f"""SELECT *
395
+ FROM workflow_runs
396
+ WHERE {' AND '.join(clauses)}
397
+ ORDER BY opened_at DESC
398
+ LIMIT 1""",
399
+ params,
400
+ ).fetchone()
401
+ return _row_to_run(row) if row else None
402
+
403
+
404
+ def list_workflow_goals(*, status: str = "", include_closed: bool = False, limit: int = 20) -> list[dict]:
405
+ conn = get_db()
406
+ clauses = []
407
+ params: list[object] = []
408
+ clean_status = status.strip().lower()
409
+ if clean_status in GOAL_STATUSES:
410
+ clauses.append("g.status = ?")
411
+ params.append(clean_status)
412
+ elif not include_closed:
413
+ clauses.append("g.status NOT IN ('abandoned', 'completed', 'cancelled')")
414
+ where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
415
+ rows = conn.execute(
416
+ f"""SELECT g.*,
417
+ COALESCE((SELECT COUNT(*) FROM workflow_runs r WHERE r.goal_id = g.goal_id), 0) AS run_count,
418
+ COALESCE((SELECT COUNT(*) FROM workflow_runs r WHERE r.goal_id = g.goal_id
419
+ AND r.status NOT IN ('completed', 'failed', 'cancelled')), 0) AS open_run_count,
420
+ COALESCE((SELECT COUNT(*) FROM workflow_goals child WHERE child.parent_goal_id = g.goal_id), 0) AS child_count
421
+ FROM workflow_goals g
422
+ {where}
423
+ ORDER BY g.updated_at DESC, g.opened_at DESC
424
+ LIMIT ?""",
425
+ params + [max(1, int(limit))],
426
+ ).fetchall()
427
+ return [_row_to_goal(row) for row in rows]
428
+
429
+
430
+ def _normalize_step(step, index: int) -> dict:
431
+ if isinstance(step, str):
432
+ title = step.strip()
433
+ key = title.lower().replace(" ", "-")[:80]
434
+ return {
435
+ "step_key": key or f"step-{index}",
436
+ "title": title or f"Step {index}",
437
+ "step_index": index,
438
+ "status": "pending",
439
+ "max_retries": 0,
440
+ "retry_policy": "",
441
+ "retry_after": "",
442
+ "human_gate": False,
443
+ "requires_approval": False,
444
+ "compensation": "",
445
+ }
446
+
447
+ step = dict(step or {})
448
+ title = str(step.get("title") or step.get("step_key") or f"Step {index}").strip()
449
+ key = str(step.get("step_key") or title.lower().replace(" ", "-")[:80] or f"step-{index}").strip()
450
+ status = str(step.get("status") or "pending").strip().lower()
451
+ if status not in STEP_STATUSES:
452
+ status = "pending"
453
+ return {
454
+ "step_key": key,
455
+ "title": title,
456
+ "step_index": int(step.get("step_index") or index),
457
+ "status": status,
458
+ "max_retries": max(0, int(step.get("max_retries") or 0)),
459
+ "retry_policy": str(step.get("retry_policy") or "").strip(),
460
+ "retry_after": str(step.get("retry_after") or "").strip(),
461
+ "human_gate": bool(step.get("human_gate")),
462
+ "requires_approval": bool(step.get("requires_approval")),
463
+ "compensation": str(step.get("compensation") or "").strip(),
464
+ }
465
+
466
+
467
+ def create_workflow_run(
468
+ session_id: str,
469
+ goal: str,
470
+ *,
471
+ goal_id: str = "",
472
+ workflow_kind: str = "general",
473
+ protocol_task_id: str = "",
474
+ idempotency_key: str = "",
475
+ priority: str = "normal",
476
+ shared_state=None,
477
+ next_action: str = "",
478
+ owner: str = "",
479
+ steps=None,
480
+ ) -> dict:
481
+ existing = _find_reusable_run(session_id, idempotency_key, goal_id=goal_id)
482
+ if existing:
483
+ existing["reused_existing"] = True
484
+ return existing
485
+
486
+ conn = get_db()
487
+ run_id = _workflow_run_id()
488
+ clean_priority = priority if priority in PRIORITIES else "normal"
489
+ clean_goal_id = goal_id.strip()
490
+ if clean_goal_id:
491
+ linked_goal = get_workflow_goal(clean_goal_id)
492
+ if not linked_goal:
493
+ raise ValueError(f"Unknown goal_id: {clean_goal_id}")
494
+ if linked_goal["status"] in GOAL_CLOSED_STATUSES:
495
+ raise ValueError(f"Goal {clean_goal_id} is closed with status '{linked_goal['status']}'")
496
+ steps = steps or []
497
+ first_step_key = ""
498
+ if steps:
499
+ first_step_key = _normalize_step(steps[0], 1)["step_key"]
500
+
501
+ conn.execute(
502
+ """INSERT INTO workflow_runs (
503
+ run_id, session_id, protocol_task_id, goal_id, goal, workflow_kind,
504
+ status, priority, idempotency_key, shared_state,
505
+ next_action, current_step_key, last_checkpoint_label, owner
506
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
507
+ (
508
+ run_id,
509
+ session_id.strip(),
510
+ protocol_task_id.strip(),
511
+ clean_goal_id,
512
+ goal.strip(),
513
+ workflow_kind.strip() or "general",
514
+ "open",
515
+ clean_priority,
516
+ idempotency_key.strip(),
517
+ _as_json(shared_state, {}),
518
+ next_action.strip(),
519
+ first_step_key,
520
+ "opened",
521
+ owner.strip(),
522
+ ),
523
+ )
524
+ for index, raw_step in enumerate(steps, 1):
525
+ step = _normalize_step(raw_step, index)
526
+ conn.execute(
527
+ """INSERT INTO workflow_steps (
528
+ run_id, step_key, title, step_index, status, max_retries,
529
+ retry_policy, retry_after, human_gate, requires_approval,
530
+ compensation
531
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
532
+ (
533
+ run_id,
534
+ step["step_key"],
535
+ step["title"],
536
+ step["step_index"],
537
+ step["status"],
538
+ step["max_retries"],
539
+ step["retry_policy"],
540
+ step["retry_after"],
541
+ 1 if step["human_gate"] else 0,
542
+ 1 if step["requires_approval"] else 0,
543
+ step["compensation"],
544
+ ),
545
+ )
546
+
547
+ conn.execute(
548
+ """INSERT INTO workflow_checkpoints (
549
+ run_id, step_key, checkpoint_label, run_status, step_status,
550
+ summary, shared_state, state_patch, next_action, actor
551
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
552
+ (
553
+ run_id,
554
+ first_step_key,
555
+ "opened",
556
+ "open",
557
+ "pending" if first_step_key else "",
558
+ "Workflow opened",
559
+ _as_json(shared_state, {}),
560
+ _as_json({}, {}),
561
+ next_action.strip(),
562
+ owner.strip() or "workflow-open",
563
+ ),
564
+ )
565
+ conn.commit()
566
+ run = get_workflow_run(run_id)
567
+ if run:
568
+ run["reused_existing"] = False
569
+ return run or {"run_id": run_id, "reused_existing": False}
570
+
571
+
572
+ def _next_pending_step(conn, run_id: str) -> dict | None:
573
+ row = conn.execute(
574
+ """SELECT *
575
+ FROM workflow_steps
576
+ WHERE run_id = ?
577
+ AND status IN ('pending', 'retrying')
578
+ ORDER BY step_index ASC, id ASC
579
+ LIMIT 1""",
580
+ (run_id,),
581
+ ).fetchone()
582
+ return _row_to_step(row) if row else None
583
+
584
+
585
+ def _upsert_workflow_step(
586
+ conn,
587
+ run_id: str,
588
+ *,
589
+ step_key: str,
590
+ step_title: str = "",
591
+ step_status: str = "",
592
+ max_retries: int | None = None,
593
+ retry_policy: str = "",
594
+ retry_after: str = "",
595
+ requires_approval: bool | None = None,
596
+ compensation: str = "",
597
+ state_patch=None,
598
+ summary: str = "",
599
+ evidence: str = "",
600
+ ) -> dict:
601
+ row = conn.execute(
602
+ "SELECT * FROM workflow_steps WHERE run_id = ? AND step_key = ?",
603
+ (run_id, step_key),
604
+ ).fetchone()
605
+ created = False
606
+ if not row:
607
+ created = True
608
+ conn.execute(
609
+ """INSERT INTO workflow_steps (
610
+ run_id, step_key, title, step_index, status
611
+ ) VALUES (?, ?, ?, ?, ?)""",
612
+ (
613
+ run_id,
614
+ step_key,
615
+ step_title or step_key,
616
+ 999,
617
+ "pending",
618
+ ),
619
+ )
620
+ row = conn.execute(
621
+ "SELECT * FROM workflow_steps WHERE run_id = ? AND step_key = ?",
622
+ (run_id, step_key),
623
+ ).fetchone()
624
+
625
+ step = dict(row)
626
+ clean_step_status = step_status if step_status in STEP_STATUSES else step["status"]
627
+ attempt_count = int(step.get("attempt_count") or 0)
628
+ if clean_step_status in {"running", "retrying"} and step.get("status") not in {"running", "retrying"}:
629
+ attempt_count += 1
630
+ elif created and clean_step_status in {"running", "retrying"}:
631
+ attempt_count = 1
632
+
633
+ started_at_value = step.get("started_at")
634
+ completed_at_value = step.get("completed_at")
635
+ if clean_step_status in {"running", "retrying"} and not started_at_value:
636
+ started_at_value = _now_sql()
637
+ if clean_step_status in {"completed", "skipped"}:
638
+ completed_at_value = _now_sql()
639
+ elif clean_step_status in {"running", "retrying", "blocked", "waiting_approval", "failed"}:
640
+ completed_at_value = None
641
+
642
+ effective_max_retries = int(max_retries) if max_retries is not None else int(step.get("max_retries") or 0)
643
+ effective_requires_approval = (
644
+ bool(requires_approval)
645
+ if requires_approval is not None
646
+ else bool(step.get("requires_approval"))
647
+ )
648
+ conn.execute(
649
+ """UPDATE workflow_steps
650
+ SET title = ?,
651
+ status = ?,
652
+ attempt_count = ?,
653
+ max_retries = ?,
654
+ retry_policy = ?,
655
+ retry_after = ?,
656
+ human_gate = ?,
657
+ requires_approval = ?,
658
+ compensation = ?,
659
+ last_summary = ?,
660
+ last_evidence = ?,
661
+ last_state_patch = ?,
662
+ started_at = ?,
663
+ completed_at = ?,
664
+ updated_at = datetime('now')
665
+ WHERE run_id = ? AND step_key = ?""",
666
+ (
667
+ step_title or step.get("title") or step_key,
668
+ clean_step_status,
669
+ attempt_count,
670
+ effective_max_retries,
671
+ retry_policy or step.get("retry_policy") or "",
672
+ retry_after or step.get("retry_after") or "",
673
+ 1 if (bool(step.get("human_gate")) or effective_requires_approval) else 0,
674
+ 1 if effective_requires_approval else 0,
675
+ compensation or step.get("compensation") or "",
676
+ summary[:2000],
677
+ evidence[:2000],
678
+ _as_json(state_patch, {}),
679
+ started_at_value,
680
+ completed_at_value,
681
+ run_id,
682
+ step_key,
683
+ ),
684
+ )
685
+ row = conn.execute(
686
+ "SELECT * FROM workflow_steps WHERE run_id = ? AND step_key = ?",
687
+ (run_id, step_key),
688
+ ).fetchone()
689
+ return _row_to_step(row)
690
+
691
+
692
+ def _derive_run_status(conn, run_id: str, *, requested: str = "", step_status: str = "") -> str:
693
+ if requested in RUN_STATUSES:
694
+ return requested
695
+ if step_status == "waiting_approval":
696
+ return "waiting_approval"
697
+ if step_status == "blocked":
698
+ return "blocked"
699
+ if step_status in {"running", "retrying"}:
700
+ return "running"
701
+ if step_status == "failed":
702
+ return "failed"
703
+ if step_status in {"completed", "skipped"}:
704
+ rows = conn.execute(
705
+ "SELECT status FROM workflow_steps WHERE run_id = ? ORDER BY step_index ASC, id ASC",
706
+ (run_id,),
707
+ ).fetchall()
708
+ statuses = [row["status"] for row in rows]
709
+ if statuses and all(status in {"completed", "skipped"} for status in statuses):
710
+ return "completed"
711
+ if statuses:
712
+ return "running"
713
+ row = conn.execute("SELECT status FROM workflow_runs WHERE run_id = ?", (run_id,)).fetchone()
714
+ return row["status"] if row else "open"
715
+
716
+
717
+ def record_workflow_transition(
718
+ run_id: str,
719
+ *,
720
+ step_key: str = "",
721
+ step_title: str = "",
722
+ step_status: str = "",
723
+ run_status: str = "",
724
+ checkpoint_label: str = "",
725
+ summary: str = "",
726
+ shared_state=None,
727
+ state_patch=None,
728
+ evidence: str = "",
729
+ next_action: str = "",
730
+ retry_after: str = "",
731
+ max_retries: int | None = None,
732
+ retry_policy: str = "",
733
+ requires_approval: bool | None = None,
734
+ compensation: str = "",
735
+ actor: str = "",
736
+ owner: str = "",
737
+ ) -> dict | None:
738
+ conn = get_db()
739
+ run_row = conn.execute("SELECT * FROM workflow_runs WHERE run_id = ?", (run_id.strip(),)).fetchone()
740
+ if not run_row:
741
+ return None
742
+
743
+ step = None
744
+ clean_step_status = step_status if step_status in STEP_STATUSES else ""
745
+ if step_key.strip():
746
+ step = _upsert_workflow_step(
747
+ conn,
748
+ run_id.strip(),
749
+ step_key=step_key.strip(),
750
+ step_title=step_title.strip(),
751
+ step_status=clean_step_status,
752
+ max_retries=max_retries,
753
+ retry_policy=retry_policy.strip(),
754
+ retry_after=retry_after.strip(),
755
+ requires_approval=requires_approval,
756
+ compensation=compensation.strip(),
757
+ state_patch=state_patch,
758
+ summary=summary.strip(),
759
+ evidence=evidence.strip(),
760
+ )
761
+
762
+ clean_run_status = _derive_run_status(
763
+ conn,
764
+ run_id.strip(),
765
+ requested=run_status.strip(),
766
+ step_status=clean_step_status,
767
+ )
768
+ current_step_key = run_row["current_step_key"] or ""
769
+ if step:
770
+ if step["status"] in {"completed", "skipped"}:
771
+ pending = _next_pending_step(conn, run_id.strip())
772
+ current_step_key = pending["step_key"] if pending else ""
773
+ else:
774
+ current_step_key = step["step_key"]
775
+ shared_state_json = run_row["shared_state"]
776
+ if shared_state is not None:
777
+ shared_state_json = _as_json(shared_state, {})
778
+ label = checkpoint_label.strip() or clean_step_status or clean_run_status or "checkpoint"
779
+ conn.execute(
780
+ """UPDATE workflow_runs
781
+ SET status = ?,
782
+ shared_state = ?,
783
+ next_action = ?,
784
+ current_step_key = ?,
785
+ last_checkpoint_label = ?,
786
+ owner = CASE
787
+ WHEN ? != '' THEN ?
788
+ ELSE owner
789
+ END,
790
+ updated_at = datetime('now'),
791
+ closed_at = CASE
792
+ WHEN ? IN ('completed', 'failed', 'cancelled') THEN datetime('now')
793
+ ELSE NULL
794
+ END
795
+ WHERE run_id = ?""",
796
+ (
797
+ clean_run_status,
798
+ shared_state_json,
799
+ next_action.strip() or run_row["next_action"] or "",
800
+ current_step_key,
801
+ label,
802
+ owner.strip(),
803
+ owner.strip(),
804
+ clean_run_status,
805
+ run_id.strip(),
806
+ ),
807
+ )
808
+ conn.execute(
809
+ """INSERT INTO workflow_checkpoints (
810
+ run_id, step_key, checkpoint_label, run_status, step_status, summary,
811
+ shared_state, state_patch, evidence, next_action, retry_after,
812
+ requires_approval, compensation_note, attempt, actor
813
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
814
+ (
815
+ run_id.strip(),
816
+ step_key.strip(),
817
+ label,
818
+ clean_run_status,
819
+ clean_step_status,
820
+ summary.strip()[:2000],
821
+ shared_state_json,
822
+ _as_json(state_patch, {}),
823
+ evidence.strip()[:2000],
824
+ next_action.strip()[:1000],
825
+ retry_after.strip(),
826
+ 1 if bool(requires_approval) else 0,
827
+ compensation.strip()[:1000],
828
+ int(step.get("attempt_count") or 0) if step else 0,
829
+ actor.strip() or "workflow-update",
830
+ ),
831
+ )
832
+ conn.commit()
833
+ return get_workflow_run(run_id.strip())
834
+
835
+
836
+ def list_workflow_runs(*, status: str = "", goal_id: str = "", include_closed: bool = False, limit: int = 20) -> list[dict]:
837
+ conn = get_db()
838
+ clauses = []
839
+ params: list[object] = []
840
+ clean_goal_id = goal_id.strip()
841
+ if clean_goal_id:
842
+ clauses.append("goal_id = ?")
843
+ params.append(clean_goal_id)
844
+ if status.strip() in RUN_STATUSES:
845
+ clauses.append("status = ?")
846
+ params.append(status.strip())
847
+ elif not include_closed:
848
+ clauses.append("status NOT IN ('completed', 'failed', 'cancelled')")
849
+ where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
850
+ rows = conn.execute(
851
+ f"""SELECT *
852
+ FROM workflow_runs
853
+ {where}
854
+ ORDER BY updated_at DESC, opened_at DESC
855
+ LIMIT ?""",
856
+ params + [max(1, int(limit))],
857
+ ).fetchall()
858
+ return [_row_to_run(row, include_steps=False) for row in rows]
859
+
860
+
861
+ def get_workflow_replay(run_id: str, *, limit: int = 50) -> list[dict]:
862
+ conn = get_db()
863
+ rows = conn.execute(
864
+ """SELECT *
865
+ FROM workflow_checkpoints
866
+ WHERE run_id = ?
867
+ ORDER BY id DESC
868
+ LIMIT ?""",
869
+ (run_id.strip(), max(1, int(limit))),
870
+ ).fetchall()
871
+ checkpoints = []
872
+ for row in rows:
873
+ item = dict(row)
874
+ item["shared_state"] = _parse_json(item.get("shared_state"), {})
875
+ item["state_patch"] = _parse_json(item.get("state_patch"), {})
876
+ item["requires_approval"] = bool(item.get("requires_approval"))
877
+ checkpoints.append(item)
878
+ return checkpoints
879
+
880
+
881
+ def get_workflow_resume_state(run_id: str) -> dict | None:
882
+ run = get_workflow_run(run_id.strip())
883
+ if not run:
884
+ return None
885
+
886
+ steps = run.get("steps") or []
887
+ current = None
888
+ if run.get("current_step_key"):
889
+ current = next((step for step in steps if step["step_key"] == run["current_step_key"]), None)
890
+
891
+ waiting = next((step for step in steps if step["status"] == "waiting_approval"), None)
892
+ if waiting:
893
+ return {
894
+ "run": run,
895
+ "resume_state": "waiting_approval",
896
+ "can_resume": False,
897
+ "requires_approval": True,
898
+ "next_step": waiting,
899
+ "message": f"Workflow is waiting for approval on step '{waiting['title']}'.",
900
+ }
901
+
902
+ retryable = next(
903
+ (
904
+ step
905
+ for step in steps
906
+ if step["status"] == "failed"
907
+ and int(step.get("max_retries") or 0) > int(step.get("attempt_count") or 0)
908
+ ),
909
+ None,
910
+ )
911
+ if retryable:
912
+ return {
913
+ "run": run,
914
+ "resume_state": "retry_available",
915
+ "can_resume": True,
916
+ "requires_approval": False,
917
+ "next_step": retryable,
918
+ "message": (
919
+ f"Retry step '{retryable['title']}' "
920
+ f"(attempt {int(retryable.get('attempt_count') or 0) + 1}/{int(retryable.get('max_retries') or 0)})."
921
+ ),
922
+ }
923
+
924
+ if current and current["status"] in {"running", "blocked", "retrying"}:
925
+ return {
926
+ "run": run,
927
+ "resume_state": current["status"],
928
+ "can_resume": current["status"] != "blocked",
929
+ "requires_approval": False,
930
+ "next_step": current,
931
+ "message": f"Resume current step '{current['title']}' from status '{current['status']}'.",
932
+ }
933
+
934
+ pending = next((step for step in steps if step["status"] in {"pending", "retrying"}), None)
935
+ if pending:
936
+ return {
937
+ "run": run,
938
+ "resume_state": "ready",
939
+ "can_resume": True,
940
+ "requires_approval": False,
941
+ "next_step": pending,
942
+ "message": f"Continue with step '{pending['title']}'.",
943
+ }
944
+
945
+ return {
946
+ "run": run,
947
+ "resume_state": run.get("status") or "open",
948
+ "can_resume": run.get("status") not in RUN_CLOSED_STATUSES,
949
+ "requires_approval": False,
950
+ "next_step": current,
951
+ "message": run.get("next_action") or "Workflow state available.",
952
+ }