fix: bound the waits that could freeze the learn loop (voyager_stack.py)
Browse files- voyager_stack.py +147 -0
voyager_stack.py
CHANGED
|
@@ -793,6 +793,153 @@ async function exploreUntil(bot, direction, maxTime = 60, callback) {
|
|
| 793 |
action_text = action_text.replace(marker, creative_facts + "\n" + marker, 1)
|
| 794 |
action_prompt.write_text(action_text, encoding="utf-8")
|
| 795 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 796 |
action_agent_path = SRC / "voyager" / "agents" / "action.py"
|
| 797 |
action_agent_text = action_agent_path.read_text(encoding="utf-8")
|
| 798 |
action_agent_text = action_agent_text.replace(
|
|
|
|
| 793 |
action_text = action_text.replace(marker, creative_facts + "\n" + marker, 1)
|
| 794 |
action_prompt.write_text(action_text, encoding="utf-8")
|
| 795 |
|
| 796 |
+
# SkillManager.__init__ built Chroma() with no deadline at all, and
|
| 797 |
+
# Chroma embeds through OpenAIEmbeddings(), which in langchain 0.0.350
|
| 798 |
+
# calls openai==0.28.1 with no request timeout. A single stalled
|
| 799 |
+
# /v1/embeddings response therefore parked the constructor forever:
|
| 800 |
+
# the log stopped at "Loading Skill Manager" and the loop never reached
|
| 801 |
+
# learn(), so nothing failed, nothing retried and no counter moved.
|
| 802 |
+
# The stall watchdog then fired every 900s and looked like the cause
|
| 803 |
+
# instead of the symptom.
|
| 804 |
+
#
|
| 805 |
+
# The same constructor ended with a hard assert. A vectordb that is
|
| 806 |
+
# one document out of sync is recoverable -- the skills.json on disk is
|
| 807 |
+
# the source of truth -- but the assert turned that into a dead process.
|
| 808 |
+
# Rebuild the index from skills.json instead, which is what the comment
|
| 809 |
+
# in the failure message already told the operator to do by hand.
|
| 810 |
+
skill_path = SRC / "voyager" / "agents" / "skill.py"
|
| 811 |
+
skill_text = skill_path.read_text(encoding="utf-8")
|
| 812 |
+
old_vectordb = ''' self.vectordb = Chroma(
|
| 813 |
+
collection_name="skill_vectordb",
|
| 814 |
+
embedding_function=OpenAIEmbeddings(),
|
| 815 |
+
persist_directory=f"{ckpt_dir}/skill/vectordb",
|
| 816 |
+
)'''
|
| 817 |
+
new_vectordb = ''' self.vectordb = self._open_vectordb(ckpt_dir)'''
|
| 818 |
+
if old_vectordb in skill_text:
|
| 819 |
+
skill_text = skill_text.replace(old_vectordb, new_vectordb, 1)
|
| 820 |
+
elif new_vectordb.strip() not in skill_text:
|
| 821 |
+
# The anchor is gone and the replacement is not already in place:
|
| 822 |
+
# the file changed underneath this patch and the unbounded
|
| 823 |
+
# constructor would survive. Refuse rather than boot a tree that
|
| 824 |
+
# can still hang.
|
| 825 |
+
raise RuntimeError("SkillManager patch anchor missing; refusing unbounded-Chroma patch")
|
| 826 |
+
|
| 827 |
+
old_assert = ''' assert self.vectordb._collection.count() == len(self.skills), (
|
| 828 |
+
f"Skill Manager's vectordb is not synced with skills.json.\\n"
|
| 829 |
+
f"There are {self.vectordb._collection.count()} skills in vectordb but {len(self.skills)} skills in skills.json.\\n"
|
| 830 |
+
f"Did you set resume=False when initializing the manager?\\n"
|
| 831 |
+
f"You may need to manually delete the vectordb directory for running from scratch."
|
| 832 |
+
)'''
|
| 833 |
+
new_resync = ''' self._resync_vectordb()'''
|
| 834 |
+
if old_assert in skill_text:
|
| 835 |
+
skill_text = skill_text.replace(old_assert, new_resync, 1)
|
| 836 |
+
elif "self._resync_vectordb()" not in skill_text:
|
| 837 |
+
raise RuntimeError("SkillManager sync-assert anchor missing; refusing patch")
|
| 838 |
+
|
| 839 |
+
skill_helpers = ''' # Every embedding this class makes goes through here, so one stalled
|
| 840 |
+
# response cannot freeze the constructor, add_new_skill or retrieve_skills.
|
| 841 |
+
EMBED_DEADLINE_SECONDS = 60
|
| 842 |
+
VECTORDB_OPEN_SECONDS = 180
|
| 843 |
+
|
| 844 |
+
def _embedding_function(self):
|
| 845 |
+
"""OpenAIEmbeddings with an explicit timeout.
|
| 846 |
+
|
| 847 |
+
langchain 0.0.350 builds the openai 0.28.1 client, whose
|
| 848 |
+
Embedding.create has no default request timeout. The base URL is the
|
| 849 |
+
remote brain Space, so a slow or wedged response is a real possibility
|
| 850 |
+
and must surface as an exception the runner can retry, not as a hang.
|
| 851 |
+
|
| 852 |
+
request_timeout is a declared pydantic field in this version, so pass it
|
| 853 |
+
to the constructor. Some versions renamed it to timeout, and a pydantic
|
| 854 |
+
model raises on an unknown keyword, so fall back rather than crash the
|
| 855 |
+
constructor over an observability nicety.
|
| 856 |
+
"""
|
| 857 |
+
try:
|
| 858 |
+
return OpenAIEmbeddings(request_timeout=self.EMBED_DEADLINE_SECONDS)
|
| 859 |
+
except Exception:
|
| 860 |
+
try:
|
| 861 |
+
return OpenAIEmbeddings(timeout=self.EMBED_DEADLINE_SECONDS)
|
| 862 |
+
except Exception:
|
| 863 |
+
return OpenAIEmbeddings()
|
| 864 |
+
|
| 865 |
+
def _open_vectordb(self, ckpt_dir):
|
| 866 |
+
"""Construct Chroma under a wall-clock deadline.
|
| 867 |
+
|
| 868 |
+
Chroma can touch the embedding function while opening (and will, when
|
| 869 |
+
it has to write a missing index). Running the open in a worker gives
|
| 870 |
+
the constructor a deadline it did not have; the caller follows up with
|
| 871 |
+
_resync_vectordb, which repairs any index that is out of date.
|
| 872 |
+
"""
|
| 873 |
+
import concurrent.futures as _cf
|
| 874 |
+
|
| 875 |
+
def _open():
|
| 876 |
+
return Chroma(
|
| 877 |
+
collection_name="skill_vectordb",
|
| 878 |
+
embedding_function=self._embedding_function(),
|
| 879 |
+
persist_directory=f"{ckpt_dir}/skill/vectordb",
|
| 880 |
+
)
|
| 881 |
+
|
| 882 |
+
with _cf.ThreadPoolExecutor(max_workers=1) as pool:
|
| 883 |
+
future = pool.submit(_open)
|
| 884 |
+
try:
|
| 885 |
+
return future.result(timeout=self.VECTORDB_OPEN_SECONDS)
|
| 886 |
+
except _cf.TimeoutError as exc:
|
| 887 |
+
raise RuntimeError(
|
| 888 |
+
"vector store did not open within "
|
| 889 |
+
f"{self.VECTORDB_OPEN_SECONDS}s; the embedding endpoint is "
|
| 890 |
+
"not answering"
|
| 891 |
+
) from exc
|
| 892 |
+
|
| 893 |
+
def _resync_vectordb(self):
|
| 894 |
+
"""Make the index match skills.json, which is the source of truth.
|
| 895 |
+
|
| 896 |
+
Replaces a hard assert that killed the process when the two counts
|
| 897 |
+
disagreed by one document. Rebuilding is safe: every skill is present
|
| 898 |
+
in skills.json with its code and description.
|
| 899 |
+
"""
|
| 900 |
+
expected = set(self.skills)
|
| 901 |
+
try:
|
| 902 |
+
stored = set(self.vectordb._collection.get()["ids"])
|
| 903 |
+
except Exception:
|
| 904 |
+
stored = set()
|
| 905 |
+
for stale in stored - expected:
|
| 906 |
+
try:
|
| 907 |
+
self.vectordb._collection.delete(ids=[stale])
|
| 908 |
+
except Exception:
|
| 909 |
+
pass
|
| 910 |
+
missing = expected - stored
|
| 911 |
+
if missing:
|
| 912 |
+
print(
|
| 913 |
+
f"\\033[33mSkill Manager resyncing {len(missing)} skill(s) into the vectordb\\033[0m",
|
| 914 |
+
flush=True,
|
| 915 |
+
)
|
| 916 |
+
for name in sorted(missing):
|
| 917 |
+
entry = self.skills.get(name) or {}
|
| 918 |
+
try:
|
| 919 |
+
self.vectordb.add_texts(
|
| 920 |
+
texts=[f"Skill: {name}\\n{entry.get('description', '')}"],
|
| 921 |
+
metadatas=[{"name": name}],
|
| 922 |
+
ids=[name],
|
| 923 |
+
)
|
| 924 |
+
except Exception as exc:
|
| 925 |
+
print(
|
| 926 |
+
f"\\033[33mSkill Manager could not index {name}: "
|
| 927 |
+
f"{type(exc).__name__}: {exc}\\033[0m",
|
| 928 |
+
flush=True,
|
| 929 |
+
)
|
| 930 |
+
try:
|
| 931 |
+
self.vectordb.persist()
|
| 932 |
+
except Exception:
|
| 933 |
+
pass
|
| 934 |
+
|
| 935 |
+
'''
|
| 936 |
+
if "def _open_vectordb" not in skill_text:
|
| 937 |
+
marker_after = " @property\n def programs(self):"
|
| 938 |
+
if marker_after not in skill_text:
|
| 939 |
+
raise RuntimeError("SkillManager property anchor missing; refusing patch")
|
| 940 |
+
skill_text = skill_text.replace(marker_after, skill_helpers + marker_after, 1)
|
| 941 |
+
skill_path.write_text(skill_text, encoding="utf-8")
|
| 942 |
+
|
| 943 |
action_agent_path = SRC / "voyager" / "agents" / "action.py"
|
| 944 |
action_agent_text = action_agent_path.read_text(encoding="utf-8")
|
| 945 |
action_agent_text = action_agent_text.replace(
|