personal memory agent
0
fork

Configure Feed

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

talents/story: migrate conversation/work/event to shared story.schema.json

Second structured-outputs L3 migration after talents/sense (8c952dc4).
Introduces talent/story.schema.json — Draft 2020-12, provider-intersection
subset (type, enum, required, additionalProperties, properties, items,
minLength, minimum, maximum, maxItems) — shared by the three story
talents. Schema enforcement is advisory: schema_validation surfaces on
the finish event via existing dispatcher plumbing (think/talent.py and
think/talents.py), but never blocks generation or the story post-hook.

talent/story.py is unchanged — it remains the authoritative source for
normalization (topics lowercase/dedupe/cap, confidence clamp, resolution
enum filtering). The schema mirrors its required-field contract:
- top-level: body, topics, confidence, commitments, closures, decisions
- commitments: 5 required string fields
- closures: 5 required string fields + resolution enum
{sent, done, signed, dropped, deferred}
- decisions: 3 required string fields
- additionalProperties: false at every closed object level
- topics deliberately permissive (no minItems/uniqueItems/per-item
minLength) — the hook does its own normalization

Tests:
- test_story_schema_file_is_valid_draft_2020_12
- test_story_talents_load_shared_schema (all three talents resolve the
shared schema through get_talent)
- test_story_schema_mirrors_hook_requirements (asserts required-field
parity with talent/story.py and enum parity with ALLOWED_RESOLUTIONS)
- test_story_hook_fixtures_validate_against_schema (hook test fixtures
stay schema-clean)
- All 12 existing test_story_hook.py tests remain green.

Stats API baseline refresh: the stats API exposes raw generator
frontmatter, so the new `schema` field appears in three generator blocks
in tests/baselines/api/stats/stats.json — updated accordingly.

Live validation deferred — provider creds not available in this worktree
environment, matching the sense migration's disposition. End-to-end
wiring is verified by the dispatcher pass-through test plus existing
json_schema coverage in tests/test_generate_full.py and
tests/test_talent.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+141
+1
talent/conversation.md
··· 7 7 "activities": ["meeting", "call", "messaging", "email"], 8 8 "priority": 20, 9 9 "output": "json", 10 + "schema": "story.schema.json", 10 11 "hook": {"post": "story"}, 11 12 "load": { 12 13 "transcripts": true,
+1
talent/event.md
··· 7 7 "activities": ["appointment", "event", "travel", "errand", "celebration", "deadline", "reminder"], 8 8 "priority": 20, 9 9 "output": "json", 10 + "schema": "story.schema.json", 10 11 "hook": {"post": "story"}, 11 12 "load": { 12 13 "transcripts": true,
+61
talent/story.schema.json
··· 1 + { 2 + "$schema": "https://json-schema.org/draft/2020-12/schema", 3 + "type": "object", 4 + "additionalProperties": false, 5 + "required": ["body","topics","confidence","commitments","closures","decisions"], 6 + "properties": { 7 + "body": {"type": "string", "minLength": 1}, 8 + "topics": { 9 + "type": "array", 10 + "items": {"type": "string"}, 11 + "maxItems": 10 12 + }, 13 + "confidence": {"type": "number", "minimum": 0, "maximum": 1}, 14 + "commitments": { 15 + "type": "array", 16 + "items": { 17 + "type": "object", 18 + "additionalProperties": false, 19 + "required": ["owner","action","counterparty","when","context"], 20 + "properties": { 21 + "owner": {"type": "string", "minLength": 1}, 22 + "action": {"type": "string", "minLength": 1}, 23 + "counterparty": {"type": "string", "minLength": 1}, 24 + "when": {"type": "string", "minLength": 1}, 25 + "context": {"type": "string", "minLength": 1} 26 + } 27 + } 28 + }, 29 + "closures": { 30 + "type": "array", 31 + "items": { 32 + "type": "object", 33 + "additionalProperties": false, 34 + "required": ["owner","action","counterparty","resolution","context"], 35 + "properties": { 36 + "owner": {"type": "string", "minLength": 1}, 37 + "action": {"type": "string", "minLength": 1}, 38 + "counterparty": {"type": "string", "minLength": 1}, 39 + "resolution": { 40 + "type": "string", 41 + "enum": ["sent","done","signed","dropped","deferred"] 42 + }, 43 + "context": {"type": "string", "minLength": 1} 44 + } 45 + } 46 + }, 47 + "decisions": { 48 + "type": "array", 49 + "items": { 50 + "type": "object", 51 + "additionalProperties": false, 52 + "required": ["owner","action","context"], 53 + "properties": { 54 + "owner": {"type": "string", "minLength": 1}, 55 + "action": {"type": "string", "minLength": 1}, 56 + "context": {"type": "string", "minLength": 1} 57 + } 58 + } 59 + } 60 + } 61 + }
+1
talent/work.md
··· 7 7 "activities": ["coding", "browsing", "reading"], 8 8 "priority": 20, 9 9 "output": "json", 10 + "schema": "story.schema.json", 10 11 "hook": {"post": "story"}, 11 12 "load": { 12 13 "transcripts": true,
+3
tests/baselines/api/stats/stats.json
··· 22 22 "path": "<PROJECT>/talent/conversation.md", 23 23 "priority": 20, 24 24 "schedule": "activity", 25 + "schema": "story.schema.json", 25 26 "source": "system", 26 27 "title": "Conversation Story", 27 28 "type": "generate" ··· 144 145 "path": "<PROJECT>/talent/event.md", 145 146 "priority": 20, 146 147 "schedule": "activity", 148 + "schema": "story.schema.json", 147 149 "source": "system", 148 150 "title": "Event Story", 149 151 "type": "generate" ··· 301 303 "path": "<PROJECT>/talent/work.md", 302 304 "priority": 20, 303 305 "schedule": "activity", 306 + "schema": "story.schema.json", 304 307 "source": "system", 305 308 "title": "Work Story", 306 309 "type": "generate"
+74
tests/test_story_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 talent.story import ALLOWED_RESOLUTIONS 10 + from tests.test_story_hook import _valid_result 11 + from think.talent import get_talent 12 + 13 + STORY_SCHEMA_PATH = Path(__file__).resolve().parents[1] / "talent" / "story.schema.json" 14 + 15 + 16 + def _load_story_schema() -> dict: 17 + return json.loads(STORY_SCHEMA_PATH.read_text(encoding="utf-8")) 18 + 19 + 20 + def test_story_schema_file_is_valid_draft_2020_12(): 21 + Draft202012Validator.check_schema(_load_story_schema()) 22 + 23 + 24 + def test_story_talents_load_shared_schema(): 25 + on_disk = _load_story_schema() 26 + 27 + for talent_name in ["conversation", "work", "event"]: 28 + assert get_talent(talent_name)["json_schema"] == on_disk 29 + 30 + 31 + def test_story_schema_mirrors_hook_requirements(): 32 + schema = _load_story_schema() 33 + properties = schema["properties"] 34 + 35 + assert set(schema["required"]) == { 36 + "body", 37 + "topics", 38 + "confidence", 39 + "commitments", 40 + "closures", 41 + "decisions", 42 + } 43 + assert set(properties["commitments"]["items"]["required"]) == { 44 + "owner", 45 + "action", 46 + "counterparty", 47 + "when", 48 + "context", 49 + } 50 + assert set(properties["closures"]["items"]["required"]) == { 51 + "owner", 52 + "action", 53 + "counterparty", 54 + "resolution", 55 + "context", 56 + } 57 + assert ( 58 + set(properties["closures"]["items"]["properties"]["resolution"]["enum"]) 59 + == ALLOWED_RESOLUTIONS 60 + ) 61 + assert set(properties["decisions"]["items"]["required"]) == { 62 + "owner", 63 + "action", 64 + "context", 65 + } 66 + 67 + 68 + def test_story_hook_fixtures_validate_against_schema(): 69 + schema = _load_story_schema() 70 + payload = json.loads(_valid_result()) 71 + 72 + errors = list(Draft202012Validator(schema).iter_errors(payload)) 73 + 74 + assert errors == []