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