delimit-cli 4.0.3 → 4.0.4
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 +9 -242
- package/bin/delimit-cli.js +352 -4
- package/bin/delimit-setup.js +30 -2
- package/gateway/ai/agent_dispatch.py +34 -2
- package/gateway/ai/github_scanner.py +1 -1
- package/gateway/ai/ledger_manager.py +13 -3
- package/gateway/ai/ledger_propose.py +240 -0
- package/gateway/ai/loop_engine.py +175 -372
- package/gateway/ai/notify.py +700 -13
- package/gateway/ai/reddit_proxy.py +106 -0
- package/gateway/ai/reddit_scanner.py +34 -0
- package/gateway/ai/server.py +343 -81
- package/gateway/ai/siem_streaming.py +290 -0
- package/gateway/ai/social_daemon.py +189 -0
- package/gateway/ai/swarm.py +434 -0
- package/lib/continuity-resolver.js +325 -0
- package/lib/cross-model-hooks.js +212 -0
- package/lib/delimit-template.js +5 -0
- package/lib/session-shell.js +655 -0
- package/lib/session-worker.js +479 -0
- package/package.json +1 -1
- package/scripts/security-check.sh +12 -0
package/gateway/ai/swarm.py
CHANGED
|
@@ -7,6 +7,9 @@ Config: ~/.delimit/swarm/config.yml
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
|
+
import os
|
|
11
|
+
import signal
|
|
12
|
+
import sys
|
|
10
13
|
import time
|
|
11
14
|
from pathlib import Path
|
|
12
15
|
from typing import Any, Dict, List, Optional
|
|
@@ -592,6 +595,168 @@ def check_docs_freshness(
|
|
|
592
595
|
}
|
|
593
596
|
|
|
594
597
|
|
|
598
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
599
|
+
# Swarm Governance Auto-Triggers — NEVER skip these
|
|
600
|
+
# Runs pre-flight checks before any major action
|
|
601
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
602
|
+
|
|
603
|
+
PREFLIGHT_LOG = SWARM_DIR / "preflight_log.jsonl"
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def preflight_check(
|
|
607
|
+
action: str,
|
|
608
|
+
venture: str = "",
|
|
609
|
+
path: str = "",
|
|
610
|
+
agent_id: str = "",
|
|
611
|
+
) -> Dict[str, Any]:
|
|
612
|
+
"""Run mandatory governance checks before any major swarm action.
|
|
613
|
+
|
|
614
|
+
This MUST be called before:
|
|
615
|
+
- Creating a new project/venture
|
|
616
|
+
- Deploying to production
|
|
617
|
+
- Publishing to npm
|
|
618
|
+
- Creating new agents or tools
|
|
619
|
+
- Any cross-venture operation
|
|
620
|
+
|
|
621
|
+
Returns a gate result: PASS (proceed), WARN (proceed with caution),
|
|
622
|
+
or BLOCK (stop and fix issues first).
|
|
623
|
+
"""
|
|
624
|
+
_ensure_dir()
|
|
625
|
+
checks = []
|
|
626
|
+
gate = "PASS"
|
|
627
|
+
|
|
628
|
+
# 1. Venture must be registered
|
|
629
|
+
if venture:
|
|
630
|
+
ventures = _load_ventures()
|
|
631
|
+
if venture not in ventures.get("ventures", {}):
|
|
632
|
+
checks.append({
|
|
633
|
+
"check": "venture_registered",
|
|
634
|
+
"status": "FAIL",
|
|
635
|
+
"message": f"Venture '{venture}' is not registered. Call delimit_swarm(action='register') first.",
|
|
636
|
+
"required_action": "register_venture",
|
|
637
|
+
})
|
|
638
|
+
gate = "BLOCK"
|
|
639
|
+
else:
|
|
640
|
+
checks.append({"check": "venture_registered", "status": "PASS"})
|
|
641
|
+
|
|
642
|
+
# 2. Agent must exist and be authorized
|
|
643
|
+
if agent_id:
|
|
644
|
+
registry = _load_registry()
|
|
645
|
+
agent = registry["agents"].get(agent_id, {})
|
|
646
|
+
if not agent:
|
|
647
|
+
checks.append({
|
|
648
|
+
"check": "agent_exists",
|
|
649
|
+
"status": "FAIL",
|
|
650
|
+
"message": f"Agent '{agent_id}' not found in registry.",
|
|
651
|
+
})
|
|
652
|
+
gate = "BLOCK"
|
|
653
|
+
elif agent.get("status") != "active":
|
|
654
|
+
checks.append({
|
|
655
|
+
"check": "agent_active",
|
|
656
|
+
"status": "FAIL",
|
|
657
|
+
"message": f"Agent '{agent_id}' is not active (status: {agent.get('status')}).",
|
|
658
|
+
})
|
|
659
|
+
gate = "BLOCK"
|
|
660
|
+
else:
|
|
661
|
+
checks.append({"check": "agent_authorized", "status": "PASS"})
|
|
662
|
+
|
|
663
|
+
# 3. Namespace isolation check
|
|
664
|
+
if venture and path:
|
|
665
|
+
ventures = _load_ventures()
|
|
666
|
+
v_data = ventures.get("ventures", {}).get(venture, {})
|
|
667
|
+
v_path = v_data.get("path", "")
|
|
668
|
+
if v_path and not path.startswith(v_path):
|
|
669
|
+
checks.append({
|
|
670
|
+
"check": "namespace_isolation",
|
|
671
|
+
"status": "WARN",
|
|
672
|
+
"message": f"Path '{path}' is outside venture namespace '{v_path}'.",
|
|
673
|
+
})
|
|
674
|
+
if gate == "PASS":
|
|
675
|
+
gate = "WARN"
|
|
676
|
+
else:
|
|
677
|
+
checks.append({"check": "namespace_isolation", "status": "PASS"})
|
|
678
|
+
|
|
679
|
+
# 4. Action-specific checks
|
|
680
|
+
if action in ("deploy_production", "publish_npm"):
|
|
681
|
+
# Must have run scan
|
|
682
|
+
checks.append({
|
|
683
|
+
"check": "pre_deploy_scan",
|
|
684
|
+
"status": "WARN",
|
|
685
|
+
"message": "Ensure delimit_scan, delimit_security_audit, and delimit_test_smoke have been run.",
|
|
686
|
+
"required_tools": ["delimit_scan", "delimit_security_audit", "delimit_test_smoke"],
|
|
687
|
+
})
|
|
688
|
+
if gate == "PASS":
|
|
689
|
+
gate = "WARN"
|
|
690
|
+
|
|
691
|
+
if action == "new_project":
|
|
692
|
+
checks.append({
|
|
693
|
+
"check": "project_init",
|
|
694
|
+
"status": "WARN",
|
|
695
|
+
"message": "New project: ensure delimit_scan is run after scaffolding.",
|
|
696
|
+
"required_tools": ["delimit_scan", "delimit_swarm(action='register')"],
|
|
697
|
+
})
|
|
698
|
+
if gate == "PASS":
|
|
699
|
+
gate = "WARN"
|
|
700
|
+
|
|
701
|
+
if action == "create_tool" or action == "create_agent":
|
|
702
|
+
checks.append({
|
|
703
|
+
"check": "extension_governance",
|
|
704
|
+
"status": "PASS" if agent_id else "WARN",
|
|
705
|
+
"message": "Self-extension requires architect role and founder approval for activation.",
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
# 5. Collision check
|
|
709
|
+
if path:
|
|
710
|
+
try:
|
|
711
|
+
from ai.collision_detect import check_collisions
|
|
712
|
+
collisions = check_collisions()
|
|
713
|
+
if collisions.get("conflicts"):
|
|
714
|
+
checks.append({
|
|
715
|
+
"check": "collision_free",
|
|
716
|
+
"status": "WARN",
|
|
717
|
+
"message": f"{len(collisions['conflicts'])} file collision(s) detected.",
|
|
718
|
+
})
|
|
719
|
+
if gate == "PASS":
|
|
720
|
+
gate = "WARN"
|
|
721
|
+
else:
|
|
722
|
+
checks.append({"check": "collision_free", "status": "PASS"})
|
|
723
|
+
except ImportError:
|
|
724
|
+
checks.append({"check": "collision_free", "status": "SKIP"})
|
|
725
|
+
|
|
726
|
+
# Log the preflight
|
|
727
|
+
log_entry = {
|
|
728
|
+
"timestamp": time.time(),
|
|
729
|
+
"action": action,
|
|
730
|
+
"venture": venture,
|
|
731
|
+
"agent_id": agent_id,
|
|
732
|
+
"gate": gate,
|
|
733
|
+
"checks_passed": sum(1 for c in checks if c["status"] == "PASS"),
|
|
734
|
+
"checks_total": len(checks),
|
|
735
|
+
}
|
|
736
|
+
try:
|
|
737
|
+
with open(PREFLIGHT_LOG, "a") as f:
|
|
738
|
+
f.write(json.dumps(log_entry) + "\n")
|
|
739
|
+
except Exception:
|
|
740
|
+
pass
|
|
741
|
+
|
|
742
|
+
_log({"action": "preflight_check", "gate": gate, "venture": venture,
|
|
743
|
+
"checks": len(checks), "action_type": action})
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
"gate": gate,
|
|
747
|
+
"action": action,
|
|
748
|
+
"venture": venture,
|
|
749
|
+
"checks": checks,
|
|
750
|
+
"passed": sum(1 for c in checks if c["status"] == "PASS"),
|
|
751
|
+
"total": len(checks),
|
|
752
|
+
"message": {
|
|
753
|
+
"PASS": "All governance checks passed. Proceed.",
|
|
754
|
+
"WARN": "Governance checks passed with warnings. Review before proceeding.",
|
|
755
|
+
"BLOCK": "Governance checks FAILED. Fix blocking issues before proceeding.",
|
|
756
|
+
}[gate],
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
|
|
595
760
|
# ═══════════════════════════════════════════════════════════════════════
|
|
596
761
|
# LED-279: Self-Extending Swarm — Founder Mode
|
|
597
762
|
# Agents can create new MCP tools when authorized
|
|
@@ -696,3 +861,272 @@ def list_custom_tools(venture: str = "") -> Dict[str, Any]:
|
|
|
696
861
|
"total": len(tools),
|
|
697
862
|
"venture_filter": venture or "all",
|
|
698
863
|
}
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
867
|
+
# MCP Hot Reload — Option B: subprocess restart with state transfer
|
|
868
|
+
# Consensus: Grok + Gemini agreed on subprocess restart with IPC
|
|
869
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
870
|
+
|
|
871
|
+
STATE_FILE = SWARM_DIR / "reload_state.json"
|
|
872
|
+
RESTART_FLAG = SWARM_DIR / "restart_pending"
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def hot_reload(reason: str = "update") -> Dict[str, Any]:
|
|
876
|
+
"""Restart the MCP server process to pick up new module code.
|
|
877
|
+
|
|
878
|
+
Saves current state (registry, ventures, metrics, custom tools list)
|
|
879
|
+
to a transfer file, signals the parent process to restart, and the
|
|
880
|
+
new process ingests the state on boot.
|
|
881
|
+
|
|
882
|
+
Works across all AI CLIs because MCP servers are subprocesses —
|
|
883
|
+
the CLI reconnects automatically within its timeout window (5-10s).
|
|
884
|
+
"""
|
|
885
|
+
_ensure_dir()
|
|
886
|
+
|
|
887
|
+
# 1. Capture current state for transfer
|
|
888
|
+
state = {
|
|
889
|
+
"timestamp": time.time(),
|
|
890
|
+
"reason": reason,
|
|
891
|
+
"registry": _load_registry(),
|
|
892
|
+
"ventures": _load_ventures(),
|
|
893
|
+
"metrics": _load_metrics(),
|
|
894
|
+
"custom_tools": list_custom_tools().get("tools", []),
|
|
895
|
+
}
|
|
896
|
+
STATE_FILE.write_text(json.dumps(state, indent=2))
|
|
897
|
+
|
|
898
|
+
# 2. Write restart flag (picked up by boot sequence)
|
|
899
|
+
RESTART_FLAG.write_text(json.dumps({
|
|
900
|
+
"requested_at": time.time(),
|
|
901
|
+
"reason": reason,
|
|
902
|
+
"pid": os.getpid(),
|
|
903
|
+
}))
|
|
904
|
+
|
|
905
|
+
_log({"action": "hot_reload_requested", "reason": reason, "pid": os.getpid()})
|
|
906
|
+
|
|
907
|
+
# 3. Schedule graceful restart — send SIGHUP to self after brief delay
|
|
908
|
+
# The MCP framework (FastMCP) handles SIGHUP by restarting the server
|
|
909
|
+
# If SIGHUP isn't supported, fall back to writing the flag for manual pickup
|
|
910
|
+
restart_method = "flag"
|
|
911
|
+
try:
|
|
912
|
+
# Check if we're the MCP server process
|
|
913
|
+
if os.environ.get("DELIMIT_MCP_PID"):
|
|
914
|
+
mcp_pid = int(os.environ["DELIMIT_MCP_PID"])
|
|
915
|
+
os.kill(mcp_pid, signal.SIGHUP)
|
|
916
|
+
restart_method = "sighup"
|
|
917
|
+
except (ValueError, ProcessLookupError, PermissionError):
|
|
918
|
+
pass
|
|
919
|
+
|
|
920
|
+
return {
|
|
921
|
+
"status": "reload_scheduled",
|
|
922
|
+
"method": restart_method,
|
|
923
|
+
"state_file": str(STATE_FILE),
|
|
924
|
+
"reason": reason,
|
|
925
|
+
"message": f"MCP server reload scheduled ({restart_method}). "
|
|
926
|
+
"AI CLI will reconnect within 5-10 seconds. "
|
|
927
|
+
"Session context (ledger, memory, conversation) is preserved.",
|
|
928
|
+
"next_step": "The MCP server will restart and load updated modules. "
|
|
929
|
+
"No action needed — tools will be available again momentarily.",
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def ingest_reload_state() -> Dict[str, Any]:
|
|
934
|
+
"""Called on MCP server boot to restore state from a hot reload.
|
|
935
|
+
|
|
936
|
+
Returns the transferred state if a reload just happened, or empty
|
|
937
|
+
if this is a fresh boot.
|
|
938
|
+
"""
|
|
939
|
+
if not STATE_FILE.exists():
|
|
940
|
+
return {"status": "fresh_boot", "restored": False}
|
|
941
|
+
|
|
942
|
+
try:
|
|
943
|
+
state = json.loads(STATE_FILE.read_text())
|
|
944
|
+
age = time.time() - state.get("timestamp", 0)
|
|
945
|
+
|
|
946
|
+
# Only ingest if state is less than 60 seconds old
|
|
947
|
+
if age > 60:
|
|
948
|
+
STATE_FILE.unlink(missing_ok=True)
|
|
949
|
+
return {"status": "stale_state", "restored": False, "age_seconds": age}
|
|
950
|
+
|
|
951
|
+
# Clean up
|
|
952
|
+
STATE_FILE.unlink(missing_ok=True)
|
|
953
|
+
RESTART_FLAG.unlink(missing_ok=True)
|
|
954
|
+
|
|
955
|
+
_log({"action": "reload_state_ingested", "reason": state.get("reason"), "age": age})
|
|
956
|
+
|
|
957
|
+
return {
|
|
958
|
+
"status": "restored",
|
|
959
|
+
"restored": True,
|
|
960
|
+
"reason": state.get("reason", "unknown"),
|
|
961
|
+
"age_seconds": round(age, 1),
|
|
962
|
+
"registry_agents": len(state.get("registry", {}).get("agents", {})),
|
|
963
|
+
"ventures": len(state.get("ventures", {}).get("ventures", {})),
|
|
964
|
+
"custom_tools": len(state.get("custom_tools", [])),
|
|
965
|
+
}
|
|
966
|
+
except (json.JSONDecodeError, KeyError):
|
|
967
|
+
STATE_FILE.unlink(missing_ok=True)
|
|
968
|
+
return {"status": "corrupt_state", "restored": False}
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
972
|
+
# Swarm Self-Scaling — as ventures grow, so does the workforce
|
|
973
|
+
# Architect agents can provision new specialist roles
|
|
974
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
975
|
+
|
|
976
|
+
CUSTOM_ROLES_FILE = SWARM_DIR / "custom_roles.json"
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def create_agent(
|
|
980
|
+
venture: str,
|
|
981
|
+
role_name: str,
|
|
982
|
+
description: str,
|
|
983
|
+
default_model: str = "claude-opus-4.6",
|
|
984
|
+
fallback_model: str = "gemini-3.1-pro-preview",
|
|
985
|
+
permissions: Optional[List[str]] = None,
|
|
986
|
+
creator_agent_id: str = "",
|
|
987
|
+
) -> Dict[str, Any]:
|
|
988
|
+
"""Create a new specialist agent role for a venture.
|
|
989
|
+
|
|
990
|
+
Only the Architect agent can create new roles. New roles inherit
|
|
991
|
+
the venture's namespace isolation but get scoped permissions.
|
|
992
|
+
The agent is registered but requires founder approval to activate.
|
|
993
|
+
|
|
994
|
+
All models see new agents via the standard MCP tool interface —
|
|
995
|
+
no model-specific configuration needed.
|
|
996
|
+
"""
|
|
997
|
+
if not venture or not role_name:
|
|
998
|
+
return {"error": "venture and role_name are required"}
|
|
999
|
+
|
|
1000
|
+
# Verify creator has authority
|
|
1001
|
+
registry = _load_registry()
|
|
1002
|
+
creator = registry["agents"].get(creator_agent_id, {})
|
|
1003
|
+
if creator.get("role") != "architect":
|
|
1004
|
+
return {
|
|
1005
|
+
"error": f"Only architect agents can create new roles. "
|
|
1006
|
+
f"Agent '{creator_agent_id}' has role '{creator.get('role', 'unknown')}'.",
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
# Verify venture namespace
|
|
1010
|
+
if creator.get("venture", "") != venture:
|
|
1011
|
+
return {"error": f"Agent '{creator_agent_id}' cannot create roles for venture '{venture}'"}
|
|
1012
|
+
|
|
1013
|
+
# Normalize role name
|
|
1014
|
+
safe_role = role_name.lower().replace("-", "_").replace(" ", "_")
|
|
1015
|
+
|
|
1016
|
+
# Check for duplicate
|
|
1017
|
+
if safe_role in DEFAULT_ROSTER:
|
|
1018
|
+
return {"error": f"Cannot override built-in role '{safe_role}'"}
|
|
1019
|
+
|
|
1020
|
+
# Load or create custom roles registry
|
|
1021
|
+
custom_roles = {}
|
|
1022
|
+
if CUSTOM_ROLES_FILE.exists():
|
|
1023
|
+
try:
|
|
1024
|
+
custom_roles = json.loads(CUSTOM_ROLES_FILE.read_text())
|
|
1025
|
+
except json.JSONDecodeError:
|
|
1026
|
+
custom_roles = {}
|
|
1027
|
+
|
|
1028
|
+
venture_roles = custom_roles.setdefault(venture, {})
|
|
1029
|
+
if safe_role in venture_roles:
|
|
1030
|
+
return {"error": f"Role '{safe_role}' already exists for venture '{venture}'"}
|
|
1031
|
+
|
|
1032
|
+
# Create the role definition
|
|
1033
|
+
role_def = {
|
|
1034
|
+
"role": description,
|
|
1035
|
+
"default_model": default_model,
|
|
1036
|
+
"fallback_model": fallback_model,
|
|
1037
|
+
"permissions": permissions or ["read", "suggest"],
|
|
1038
|
+
"created_by": creator_agent_id,
|
|
1039
|
+
"created_at": time.time(),
|
|
1040
|
+
"status": "pending_approval",
|
|
1041
|
+
}
|
|
1042
|
+
venture_roles[safe_role] = role_def
|
|
1043
|
+
|
|
1044
|
+
# Save
|
|
1045
|
+
_ensure_dir()
|
|
1046
|
+
CUSTOM_ROLES_FILE.write_text(json.dumps(custom_roles, indent=2))
|
|
1047
|
+
|
|
1048
|
+
# Auto-register the agent (inactive until approved)
|
|
1049
|
+
agent_id = f"{venture}_{safe_role}"
|
|
1050
|
+
registry["agents"][agent_id] = {
|
|
1051
|
+
"venture": venture,
|
|
1052
|
+
"role": safe_role,
|
|
1053
|
+
"model": default_model,
|
|
1054
|
+
"fallback": fallback_model,
|
|
1055
|
+
"status": "pending_approval",
|
|
1056
|
+
"registered_at": time.time(),
|
|
1057
|
+
"custom": True,
|
|
1058
|
+
}
|
|
1059
|
+
_save_registry(registry)
|
|
1060
|
+
|
|
1061
|
+
_log({
|
|
1062
|
+
"action": "agent_created",
|
|
1063
|
+
"venture": venture,
|
|
1064
|
+
"role": safe_role,
|
|
1065
|
+
"model": default_model,
|
|
1066
|
+
"created_by": creator_agent_id,
|
|
1067
|
+
"status": "pending_approval",
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
return {
|
|
1071
|
+
"status": "created",
|
|
1072
|
+
"agent_id": agent_id,
|
|
1073
|
+
"role": safe_role,
|
|
1074
|
+
"venture": venture,
|
|
1075
|
+
"model": default_model,
|
|
1076
|
+
"permissions": role_def["permissions"],
|
|
1077
|
+
"created_by": creator_agent_id,
|
|
1078
|
+
"activation": "pending_approval",
|
|
1079
|
+
"message": f"Agent '{agent_id}' created with role '{safe_role}'. "
|
|
1080
|
+
f"Founder approval required to activate.",
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def approve_agent(agent_id: str) -> Dict[str, Any]:
|
|
1085
|
+
"""Approve a pending custom agent for activation (founder only)."""
|
|
1086
|
+
registry = _load_registry()
|
|
1087
|
+
agent = registry["agents"].get(agent_id)
|
|
1088
|
+
if not agent:
|
|
1089
|
+
return {"error": f"Agent '{agent_id}' not found"}
|
|
1090
|
+
if not agent.get("custom"):
|
|
1091
|
+
return {"error": f"Agent '{agent_id}' is a built-in role, not a custom agent"}
|
|
1092
|
+
if agent.get("status") == "active":
|
|
1093
|
+
return {"status": "already_active", "agent_id": agent_id}
|
|
1094
|
+
|
|
1095
|
+
agent["status"] = "active"
|
|
1096
|
+
agent["approved_at"] = time.time()
|
|
1097
|
+
_save_registry(registry)
|
|
1098
|
+
|
|
1099
|
+
_log({"action": "agent_approved", "agent_id": agent_id})
|
|
1100
|
+
|
|
1101
|
+
return {
|
|
1102
|
+
"status": "activated",
|
|
1103
|
+
"agent_id": agent_id,
|
|
1104
|
+
"role": agent.get("role"),
|
|
1105
|
+
"venture": agent.get("venture"),
|
|
1106
|
+
"message": f"Agent '{agent_id}' is now active.",
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def list_agents(venture: str = "") -> Dict[str, Any]:
|
|
1111
|
+
"""List all agents — built-in and custom — optionally filtered by venture."""
|
|
1112
|
+
registry = _load_registry()
|
|
1113
|
+
agents = []
|
|
1114
|
+
|
|
1115
|
+
for agent_id, agent in registry["agents"].items():
|
|
1116
|
+
if venture and agent.get("venture") != venture:
|
|
1117
|
+
continue
|
|
1118
|
+
agents.append({
|
|
1119
|
+
"id": agent_id,
|
|
1120
|
+
"venture": agent.get("venture", ""),
|
|
1121
|
+
"role": agent.get("role", ""),
|
|
1122
|
+
"model": agent.get("model", ""),
|
|
1123
|
+
"status": agent.get("status", "active"),
|
|
1124
|
+
"custom": agent.get("custom", False),
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
return {
|
|
1128
|
+
"status": "ok",
|
|
1129
|
+
"agents": agents,
|
|
1130
|
+
"total": len(agents),
|
|
1131
|
+
"venture_filter": venture or "all",
|
|
1132
|
+
}
|