personal memory agent
0
fork

Configure Feed

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

talents/participation: migrate to external participation.schema.json + shared entry fragment

Next L3 structured-outputs migration: constrain the participation talent
with an external schema.

Follows sense (8c952dc4), story (50693752), daily_schedule (0e098e7b),
and detect_created (b58bd862).

Introduces talent/participation.schema.json plus a shared
participation_entry.schema.json fragment. The top-level schema inlines
that fragment shape (Option B) instead of using cross-file $ref because
think/talent.py::_load_talent_schema has no cross-file registry
plumbing; drift is guarded by
tests/test_participation_schema.py::test_participation_schema_items_match_fragment.

No post-hook changes: talent/participation.py is byte-identical, and the
prompt body in talent/participation.md is untouched.

Provider-side validation remains deferred per precedent; advisory local
validation continues through the existing dispatcher path.

Forward note: talent/schedule.md:55-63 is the logical next adoption site
for the shared fragment, but its current participation example does not
include entity_id, which the fragment requires, so that follow-up lode
will need to update the example or introduce a variant.

+124
+1
talent/participation.md
··· 8 8 "priority": 10, 9 9 "tier": 3, 10 10 "output": "json", 11 + "schema": "participation.schema.json", 11 12 "load": { 12 13 "transcripts": true, 13 14 "percepts": true,
+46
talent/participation.schema.json
··· 1 + { 2 + "$schema": "https://json-schema.org/draft/2020-12/schema", 3 + "type": "object", 4 + "additionalProperties": false, 5 + "required": ["participation"], 6 + "properties": { 7 + "participation": { 8 + "type": "array", 9 + "items": { 10 + "type": "object", 11 + "additionalProperties": false, 12 + "required": ["name", "role", "source", "confidence", "context", "entity_id"], 13 + "properties": { 14 + "name": { 15 + "type": "string", 16 + "minLength": 1 17 + }, 18 + "role": { 19 + "type": "string", 20 + "enum": ["attendee", "mentioned"] 21 + }, 22 + "source": { 23 + "type": "string", 24 + "enum": ["voice", "speaker_label", "transcript", "screen", "other"] 25 + }, 26 + "confidence": { 27 + "type": "number", 28 + "minimum": 0, 29 + "maximum": 1 30 + }, 31 + "context": { 32 + "type": "string" 33 + }, 34 + "entity_id": { 35 + "type": "null" 36 + } 37 + } 38 + } 39 + }, 40 + "participation_confidence": { 41 + "type": "number", 42 + "minimum": 0, 43 + "maximum": 1 44 + } 45 + } 46 + }
+31
talent/participation_entry.schema.json
··· 1 + { 2 + "$schema": "https://json-schema.org/draft/2020-12/schema", 3 + "type": "object", 4 + "additionalProperties": false, 5 + "required": ["name", "role", "source", "confidence", "context", "entity_id"], 6 + "properties": { 7 + "name": { 8 + "type": "string", 9 + "minLength": 1 10 + }, 11 + "role": { 12 + "type": "string", 13 + "enum": ["attendee", "mentioned"] 14 + }, 15 + "source": { 16 + "type": "string", 17 + "enum": ["voice", "speaker_label", "transcript", "screen", "other"] 18 + }, 19 + "confidence": { 20 + "type": "number", 21 + "minimum": 0, 22 + "maximum": 1 23 + }, 24 + "context": { 25 + "type": "string" 26 + }, 27 + "entity_id": { 28 + "type": "null" 29 + } 30 + } 31 + }
+1
tests/baselines/api/stats/stats.json
··· 172 172 "path": "<PROJECT>/talent/participation.md", 173 173 "priority": 10, 174 174 "schedule": "activity", 175 + "schema": "participation.schema.json", 175 176 "source": "system", 176 177 "tier": 3, 177 178 "title": "Participation",
+44
tests/test_participation_schema.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + import json 5 + from pathlib import Path 6 + 7 + from jsonschema import Draft202012Validator 8 + 9 + from think.talent import get_talent 10 + 11 + TALENT_DIR = Path(__file__).resolve().parents[1] / "talent" 12 + PARTICIPATION_ENTRY_SCHEMA_PATH = TALENT_DIR / "participation_entry.schema.json" 13 + PARTICIPATION_SCHEMA_PATH = TALENT_DIR / "participation.schema.json" 14 + 15 + 16 + def _load_json(path: Path) -> dict: 17 + return json.loads(path.read_text(encoding="utf-8")) 18 + 19 + 20 + def test_participation_entry_schema_is_valid_draft_2020_12(): 21 + schema = _load_json(PARTICIPATION_ENTRY_SCHEMA_PATH) 22 + 23 + Draft202012Validator.check_schema(schema) 24 + 25 + assert schema["$schema"] == "https://json-schema.org/draft/2020-12/schema" 26 + 27 + 28 + def test_participation_schema_is_valid_and_matches_loaded(): 29 + schema = _load_json(PARTICIPATION_SCHEMA_PATH) 30 + 31 + Draft202012Validator.check_schema(schema) 32 + 33 + assert get_talent("participation")["json_schema"] == schema 34 + 35 + 36 + def test_participation_schema_items_match_fragment(): 37 + schema = _load_json(PARTICIPATION_SCHEMA_PATH) 38 + fragment = _load_json(PARTICIPATION_ENTRY_SCHEMA_PATH) 39 + 40 + items = dict(schema["properties"]["participation"]["items"]) 41 + fragment_without_schema = dict(fragment) 42 + fragment_without_schema.pop("$schema") 43 + 44 + assert items == fragment_without_schema
+1
tests/test_participation_talent.py
··· 15 15 assert post.metadata["activities"] == ["*"] 16 16 assert post.metadata["tier"] == 3 17 17 assert post.metadata["output"] == "json" 18 + assert post.metadata["schema"] == "participation.schema.json" 18 19 assert post.metadata["priority"] == 10 19 20 assert post.metadata["load"]["talents"]["sense"] is True 20 21