From 9b54b39ddead71b2b805d8a1a6d8a3e380c33bc6 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Fri, 7 Aug 2026 17:33:20 -0700 Subject: [PATCH] [eric] workflows: a test that runs long hands back what it has instead of telling the agent to poll; memory stops re-deriving facts it already stored --- backend/apps/agents/schedule_mcp_server.py | 22 ++++++++++++++++++---- backend/apps/memory/distill.py | 13 ++++++++++++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/backend/apps/agents/schedule_mcp_server.py b/backend/apps/agents/schedule_mcp_server.py index fe74d732..bca169ef 100644 --- a/backend/apps/agents/schedule_mcp_server.py +++ b/backend/apps/agents/schedule_mcp_server.py @@ -546,7 +546,10 @@ def handle_delete_step(args: dict) -> dict: # How long a synchronous test may hold the turn. Long enough for a real multi-step workflow, short # enough that a wedged test returns an honest "still running" instead of hanging the conversation. -TEST_WAIT_S = 240 +# A real workflow test does several searches and page fetches per step. The first budget was 240s +# and a live three-step digest took 243, so it missed by three seconds and the agent had to make a +# second hop anyway, which is the exact re-pinging this tool exists to remove. +TEST_WAIT_S = 600 TEST_POLL_S = 3 @@ -574,10 +577,21 @@ def handle_test_workflow(args: dict) -> dict: continue transcript = t.get("transcript") or "(empty transcript)" return _ok(f"Test finished (status: {last_status}). Transcript:\n\n{transcript}") - return _ok( - f"Test Agent (session {sid[:8]}) is still running after {TEST_WAIT_S}s, so it is a long one. " - f"Last status: {last_status}. Call ReadTestTranscript to pick up the result." + # Hand back whatever the run has actually produced. Returning only "call ReadTestTranscript" made + # the model guess when to poll, which is the same dead end as not waiting at all; a partial + # transcript is something it can reason about right now. + partial = "" + try: + t = _call("GET", f"/{wid}/test-transcript") + if "_error" not in t: + partial = (t.get("transcript") or "").strip() + except Exception: + partial = "" + head = ( + f"Test Agent (session {sid[:8]}) has not finished after {TEST_WAIT_S}s (status: {last_status}). " + "Everything it has produced so far follows; call ReadTestTranscript for the rest once it lands." ) + return _ok(f"{head}\n\n{partial}" if partial else head) def handle_read_test_transcript(args: dict) -> dict: diff --git a/backend/apps/memory/distill.py b/backend/apps/memory/distill.py index e4f46e2f..4cb45db6 100644 --- a/backend/apps/memory/distill.py +++ b/backend/apps/memory/distill.py @@ -11,6 +11,7 @@ from backend.apps.agents.core.aux_llm import aux_max_tokens_for from backend.apps.agents.core.models import AgentSession from backend.apps.agents.manager.predict_followups import conversation_tail from backend.apps.agents.manager.session.history_compaction import get_branch_messages +from backend.apps.memory import store from backend.apps.memory.store import add_fact logger = logging.getLogger(__name__) @@ -65,7 +66,17 @@ async def distill_session_memory(session: AgentSession) -> List[str]: f"Return at most {MAX_FACTS_PER_DISTILL} facts, one per line, no numbering, no quotes. " "If the conversation reveals nothing durable, return the single word NOTHING." ) - user_turn = "Conversation:\n\n" + tail + "\n\n\nExtract the facts." + # Show it what is already known. Without this the model re-derives facts it recorded weeks + # ago, phrased differently every time, and the store's token-overlap guard cannot catch a + # paraphrase: the four duplicate pairs found in a real memory scored 0.11 to 0.33 against a + # 0.60 threshold. The generator is semantic, so the dedupe has to be too. + known = "\n".join(f"- {f.text}" for f in store.list_facts()) + already = ( + "Facts already stored (do NOT repeat these, in any wording):\n" + known + "\n\n" + "Return a fact ONLY if it is genuinely new, or is strictly more specific than one above " + "(in which case return the sharper version and it will replace the old one).\n\n" + ) if known else "" + user_turn = already + "Conversation:\n\n" + tail + "\n\n\nExtract the facts." chunks: List[str] = [] async with client.messages.stream( model=aux_model,