From 000b52e77dfc77dc5884775207ccce74eebb8541 Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Sat, 7 Feb 2026 19:34:11 -0700 Subject: [PATCH] activity_state: add active_entities field to activity output Prompt now asks LLM to include an active_entities array of people, companies, projects, and tools noticeably active per activity. Post-hook passes through on active items, omits on ended. Bumped max_output_tokens to 1024 to accommodate. Co-Authored-By: Claude Opus 4.6 --- muse/activity_state.md | 13 ++--- muse/activity_state.py | 55 +++++++++++---------- tests/test_activity_state.py | 95 ++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 31 deletions(-) diff --git a/muse/activity_state.md b/muse/activity_state.md index e4b0cad15..cec5869f3 100644 --- a/muse/activity_state.md +++ b/muse/activity_state.md @@ -10,7 +10,7 @@ "hook": {"pre": "activity_state", "post": "activity_state"}, "tier": 3, "thinking_budget": 2048, - "max_output_tokens": 512, + "max_output_tokens": 1024, "instructions": { "sources": {"audio": true, "screen": true, "agents": false}, "facets": false @@ -40,8 +40,8 @@ Return a JSON array of activity objects: ```json [ - {"activity": "meeting", "state": "continuing", "description": "Design review with UX team, now discussing navigation", "level": "high"}, - {"activity": "messaging", "state": "new", "description": "Slack thread about deployment", "level": "low"}, + {"activity": "meeting", "state": "continuing", "description": "Design review with UX team, now discussing navigation", "level": "high", "active_entities": ["Sarah Chen", "UX Team"]}, + {"activity": "messaging", "state": "new", "description": "Slack thread about deployment", "level": "low", "active_entities": ["DevOps"]}, {"activity": "email", "state": "ended", "description": "Replied to deployment notification from ops team"} ] ``` @@ -52,6 +52,7 @@ Return a JSON array of activity objects: - `state`: One of `"continuing"`, `"new"`, or `"ended"` - `description`: Brief description of what this activity involves (update as context evolves) - `level`: Engagement level — `"high"` (primary focus), `"medium"` (secondary), `"low"` (background). Only for continuing/new, omit for ended. +- `active_entities`: Names of people, companies, projects, or tools that were noticeably active in this segment and associated with this activity. Only include entities with clear evidence of involvement (speaking, mentioned, visible on screen). Omit for ended. ## Rules @@ -66,19 +67,19 @@ Return a JSON array of activity objects: **New activity starts:** ```json -[{"activity": "coding", "state": "new", "description": "Implementing user auth flow", "level": "high"}] +[{"activity": "coding", "state": "new", "description": "Implementing user auth flow", "level": "high", "active_entities": ["Claude Code", "VS Code"]}] ``` **Activity continues from previous:** ```json -[{"activity": "meeting", "state": "continuing", "description": "Sprint planning - now discussing blockers", "level": "high"}] +[{"activity": "meeting", "state": "continuing", "description": "Sprint planning - now discussing blockers", "level": "high", "active_entities": ["Alice Johnson", "Bob Smith"]}] ``` **One meeting ends, another starts:** ```json [ {"activity": "meeting", "state": "ended", "description": "Sprint planning completed"}, - {"activity": "meeting", "state": "new", "description": "1:1 with manager", "level": "high"} + {"activity": "meeting", "state": "new", "description": "1:1 with manager", "level": "high", "active_entities": ["Manager Name"]} ] ``` diff --git a/muse/activity_state.py b/muse/activity_state.py index 4dd05b287..aad84bf6c 100644 --- a/muse/activity_state.py +++ b/muse/activity_state.py @@ -499,6 +499,8 @@ def post_process(result: str, context: dict) -> str | None: # Build unclaimed candidates with their original indices unclaimed = [(i, c) for i, c in enumerate(prev_active) if i not in claimed] + active_entities = item.get("active_entities", []) + if state == "continuing": result = _find_best_match(activity_id, description, unclaimed) if result: @@ -509,15 +511,16 @@ def post_process(result: str, context: dict) -> str | None: # No previous match — treat as new since = segment - resolved.append( - { - "activity": activity_id, - "state": "active", - "since": since, - "description": description, - "level": item.get("level", "medium"), - } - ) + entry = { + "activity": activity_id, + "state": "active", + "since": since, + "description": description, + "level": item.get("level", "medium"), + } + if active_entities: + entry["active_entities"] = active_entities + resolved.append(entry) elif state == "ended": result = _find_best_match(activity_id, description, unclaimed) @@ -537,27 +540,29 @@ def post_process(result: str, context: dict) -> str | None: ): # No active match but has a novel description — likely # a real activity the LLM mis-tagged as ended; treat as new - resolved.append( - { - "activity": activity_id, - "state": "active", - "since": segment, - "description": description, - "level": item.get("level", "medium"), - } - ) - # else: redundant re-report of already ended activity — drop - - else: - # "new" or any unrecognized state — stamp current segment - resolved.append( - { + entry = { "activity": activity_id, "state": "active", "since": segment, "description": description, "level": item.get("level", "medium"), } - ) + if active_entities: + entry["active_entities"] = active_entities + resolved.append(entry) + # else: redundant re-report of already ended activity — drop + + else: + # "new" or any unrecognized state — stamp current segment + entry = { + "activity": activity_id, + "state": "active", + "since": segment, + "description": description, + "level": item.get("level", "medium"), + } + if active_entities: + entry["active_entities"] = active_entities + resolved.append(entry) return json.dumps(resolved, ensure_ascii=False) diff --git a/tests/test_activity_state.py b/tests/test_activity_state.py index ca0e4082c..4a1b8a292 100644 --- a/tests/test_activity_state.py +++ b/tests/test_activity_state.py @@ -835,6 +835,101 @@ class TestPostProcess: items = json.loads(result) assert items[0]["level"] == "medium" + def test_active_entities_passthrough_on_new(self): + """active_entities array is passed through on new activities.""" + from muse.activity_state import post_process + + llm_output = json.dumps( + [ + { + "activity": "meeting", + "state": "new", + "description": "Standup with team", + "level": "high", + "active_entities": ["Alice", "Bob"], + } + ] + ) + + result = post_process(llm_output, {"segment": "143000_300"}) + items = json.loads(result) + assert items[0]["active_entities"] == ["Alice", "Bob"] + + def test_active_entities_omitted_when_empty(self): + """active_entities is omitted from output when not provided or empty.""" + from muse.activity_state import post_process + + llm_output = json.dumps( + [ + { + "activity": "coding", + "state": "new", + "description": "Writing code", + "level": "high", + "active_entities": [], + } + ] + ) + + result = post_process(llm_output, {"segment": "143000_300"}) + items = json.loads(result) + assert "active_entities" not in items[0] + + def test_active_entities_omitted_on_ended(self): + """active_entities is not included on ended activities.""" + from muse.activity_state import post_process + + with tempfile.TemporaryDirectory() as tmpdir: + original_path = os.environ.get("JOURNAL_PATH") + os.environ["JOURNAL_PATH"] = tmpdir + + try: + day_dir = Path(tmpdir) / "20260130" + day_dir.mkdir() + + prev_dir = day_dir / "100000_300" + prev_dir.mkdir() + prev_state = [ + { + "activity": "meeting", + "state": "active", + "since": "093000_300", + "description": "Sprint planning", + "level": "high", + } + ] + (prev_dir / "activity_state_work.json").write_text( + json.dumps(prev_state) + ) + + (day_dir / "100500_300").mkdir() + + llm_output = json.dumps( + [ + { + "activity": "meeting", + "state": "ended", + "description": "Sprint planning completed", + "active_entities": ["Alice"], + } + ] + ) + + context = { + "day": "20260130", + "segment": "100500_300", + "output_path": f"{tmpdir}/20260130/100500_300/activity_state_work.json", + } + + result = post_process(llm_output, context) + items = json.loads(result) + assert items[0]["state"] == "ended" + assert "active_entities" not in items[0] + + finally: + if original_path: + os.environ["JOURNAL_PATH"] = original_path + def test_fuzzy_match_disambiguates_same_type(self): """Multiple same-type previous activities matched by description.""" from muse.activity_state import post_process -- 2.51.2