personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(schedule): hydrate facet enum at runtime + drop dead density/content_type defaults

Mirror the sense runtime-enum hydration pattern for schedule's `facet` and
add a drift detector for the activity enum against DEFAULT_ACTIVITIES.
Strip impossible degraded-payload fallbacks on density/content_type at the
sense consumer sites and the matching tests.

+46 -69
+3 -1
talent/schedule.md
··· 14 14 15 15 $daily_preamble 16 16 17 + $facets 18 + 17 19 # Future Schedule Extraction 18 20 19 21 **Input:** A markdown file containing chronologically ordered transcripts of a workday plus the screen agent's output for the same day. Calendar views, meeting invitations, scheduling UIs, and project-management interfaces are captured in the screen content; verbal mentions of future plans appear in transcripts. ··· 95 97 96 98 - **`participation_confidence`** — `0.0`–`1.0` — overall confidence in the participation list for this item. Lower when many attendees are inferred rather than confirmed. 97 99 98 - - **`facet`** — Facet ID from the configured facets context. Required. If no facet fits cleanly, skip the item rather than miscategorizing. 100 + - **`facet`** — Required. The facet ID slug; MUST be one of the configured facets listed in the input. Pick the closest configured facet for the scheduled item; if multiple plausibly fit, choose the one most central to the event's purpose. Do not invent slugs. 99 101 100 102 - **`cancelled`** — Boolean. `true` when the screen shows this item as cancelled (strikethrough, "Cancelled" label, declined, greyed out). `false` otherwise. 101 103
+1 -1
talent/schedule.schema.json
··· 93 93 }, 94 94 "facet": { 95 95 "type": "string", 96 - "minLength": 1 96 + "enum": ["__RUNTIME_FACETS__"] 97 97 }, 98 98 "cancelled": { 99 99 "type": "boolean"
+38 -17
tests/test_schedule_schema.py
··· 6 6 7 7 from jsonschema import Draft202012Validator 8 8 9 - from think.talent import get_talent 9 + from think.activities import DEFAULT_ACTIVITIES 10 + from think.talent import ( 11 + RUNTIME_FACETS_SENTINEL, 12 + get_talent, 13 + hydrate_runtime_enums, 14 + ) 10 15 11 16 TALENT_DIR = Path(__file__).resolve().parents[1] / "talent" 12 17 PARTICIPATION_ENTRY_SCHEMA_PATH = TALENT_DIR / "participation_entry.schema.json" 13 18 SCHEDULE_SCHEMA_PATH = TALENT_DIR / "schedule.schema.json" 14 - 15 - SCHEDULE_ACTIVITY_ENUM = { 16 - "meeting", 17 - "call", 18 - "deadline", 19 - "appointment", 20 - "event", 21 - "travel", 22 - "reminder", 23 - "errand", 24 - "celebration", 25 - "doctor_appointment", 26 - } 27 19 28 20 SCHEDULE_REQUIRED_FIELDS = { 29 21 "activity", ··· 50 42 return schema 51 43 52 44 45 + def _expected_schedule_activity_ids() -> set[str]: 46 + # Why: `meeting` is emitted by both the schedule talent and sense; the 47 + # other 9 are schedule-only (their instructions carry the marker). 48 + schedule_only = { 49 + a["id"] 50 + for a in DEFAULT_ACTIVITIES 51 + if "Scheduled events emitted by talent/schedule.md" 52 + in a.get("instructions", "") 53 + } 54 + return {"meeting"} | schedule_only 55 + 56 + 53 57 def _sample_schedule_payloads() -> list[list[dict]]: 54 58 return [ 55 59 [ ··· 145 149 assert get_talent("schedule")["json_schema"] == _load_schedule_schema() 146 150 147 151 152 + def test_schedule_schema_facet_uses_runtime_sentinel_constant(): 153 + schema = _load_schedule_schema() 154 + facet_schema = schema["items"]["properties"]["facet"] 155 + 156 + assert facet_schema["enum"] == [RUNTIME_FACETS_SENTINEL] 157 + 158 + 159 + def test_schedule_activity_enum_matches_default_activities_drift_detector(): 160 + schema = _load_schedule_schema() 161 + item_schema = schema["items"] 162 + 163 + assert set(item_schema["properties"]["activity"]["enum"]) == ( 164 + _expected_schedule_activity_ids() 165 + ) 166 + 167 + 148 168 def test_schedule_participation_entry_diverges_from_shared_fragment(): 149 169 """Schedule omits entity_id because the hook fills it via find_matching_entity.""" 150 170 schedule_schema = _load_schedule_schema() ··· 175 195 176 196 assert schedule_schema["type"] == "array" 177 197 assert set(item_schema["required"]) == SCHEDULE_REQUIRED_FIELDS 178 - assert set(properties["activity"]["enum"]) == SCHEDULE_ACTIVITY_ENUM 198 + assert set(properties["activity"]["enum"]) == _expected_schedule_activity_ids() 179 199 assert ( 180 200 participation_items["properties"]["role"]["enum"] 181 201 == fragment["properties"]["role"]["enum"] ··· 189 209 assert properties["cancelled"]["type"] == "boolean" 190 210 191 211 192 - def test_schedule_hook_fixtures_validate_against_schema(): 193 - validator = Draft202012Validator(_load_schedule_schema()) 212 + def test_schedule_hook_fixtures_validate_against_schema(monkeypatch): 213 + monkeypatch.setattr("think.talent.get_facets", lambda: {"work": {}}) 214 + validator = Draft202012Validator(hydrate_runtime_enums(_load_schedule_schema())) 194 215 195 216 for payload in _sample_schedule_payloads(): 196 217 assert list(validator.iter_errors(payload)) == []
-46
tests/test_sense_splitter.py
··· 171 171 172 172 173 173 class TestEdgeCases: 174 - def test_missing_optional_fields(self, tmp_path): 175 - from think.sense_splitter import write_sense_outputs 176 - 177 - seg_dir = Path(tmp_path) / "20260304" / "default" / "090000_300" 178 - 179 - # Advisory schema validation means the splitter must still tolerate degraded input. 180 - write_sense_outputs({}, seg_dir) 181 - 182 - agents_dir = seg_dir / "talents" 183 - assert (agents_dir / "activity.md").exists() 184 - assert (agents_dir / "facets.json").exists() 185 - assert (agents_dir / "density.json").exists() 186 - assert (agents_dir / "sense.json").exists() 187 - assert (agents_dir / "activity.md").read_text(encoding="utf-8") == "" 188 - assert ( 189 - json.loads((agents_dir / "facets.json").read_text(encoding="utf-8")) == [] 190 - ) 191 - density = json.loads((agents_dir / "density.json").read_text(encoding="utf-8")) 192 - assert density["classification"] == "active" 193 - 194 - def test_null_fields(self, tmp_path): 195 - from think.sense_splitter import write_sense_outputs 196 - 197 - seg_dir = Path(tmp_path) / "20260304" / "default" / "090000_300" 198 - sense_json = { 199 - "density": None, 200 - "content_type": None, 201 - "activity_summary": None, 202 - "entities": None, 203 - "facets": None, 204 - "meeting_detected": None, 205 - "speakers": None, 206 - } 207 - 208 - # Advisory schema validation means the splitter must still tolerate degraded input. 209 - write_sense_outputs(sense_json, seg_dir) 210 - 211 - agents_dir = seg_dir / "talents" 212 - assert (agents_dir / "activity.md").read_text(encoding="utf-8") == "" 213 - assert ( 214 - json.loads((agents_dir / "facets.json").read_text(encoding="utf-8")) == [] 215 - ) 216 - density = json.loads((agents_dir / "density.json").read_text(encoding="utf-8")) 217 - assert density["classification"] == "active" 218 - assert not (agents_dir / "speakers.json").exists() 219 - 220 174 def test_empty_activity_summary(self, tmp_path): 221 175 from think.sense_splitter import write_sense_outputs 222 176
+2 -2
think/activity_state_machine.py
··· 140 140 if self._should_reset(segment_key, day, previous_segment_key): 141 141 changes.extend(self._end_all(segment_key, "ended_gap")) 142 142 143 - density = sense_output.get("density") or "active" 144 - content_type = sense_output.get("content_type") or "idle" 143 + density = sense_output["density"] 144 + content_type = sense_output["content_type"] 145 145 activity_summary = sense_output.get("activity_summary") or "" 146 146 raw_entities = sense_output.get("entities") or [] 147 147 entity_names = [
+1 -1
think/sense_splitter.py
··· 30 30 """Write unified Sense output into per-agent files.""" 31 31 agents_dir = seg_dir / "talents" 32 32 33 - density = sense_json.get("density") or "active" 33 + density = sense_json["density"] 34 34 activity_summary = sense_json.get("activity_summary") or "" 35 35 entities = sense_json.get("entities") or [] 36 36 facets = sense_json.get("facets") or []
+1 -1
think/thinking.py
··· 645 645 return (total_success, total_failed + 1, failed_names) 646 646 647 647 write_sense_outputs(sense_json, seg_dir, stream=stream) 648 - density = sense_json.get("density") or "active" 648 + density = sense_json["density"] 649 649 _jsonl_log( 650 650 "sense.complete", 651 651 mode=target_schedule,