|
47 | 47 | import yaml |
48 | 48 | from aliases import session_bucket # type: ignore[import-not-found] |
49 | 49 |
|
50 | | -from . import _response_parser |
| 50 | +from . import _response_parser, repair_queue |
51 | 51 | from ._schemas import ( |
52 | 52 | LibrarianProposal, |
53 | 53 | describe_action_types_markdown, |
@@ -822,6 +822,56 @@ def _slugify(s: str) -> str: |
822 | 822 | above. Use Glob (e.g. `Glob("**/*.md")`) to find anything you suspect \ |
823 | 823 | exists but don't see in the snapshot. |
824 | 824 |
|
| 825 | +═══════════════════════════════════════════════════════════════════ |
| 826 | +INTEGRITY-REPAIR TASKS (HIGHEST PRIORITY — fix these first) |
| 827 | +═══════════════════════════════════════════════════════════════════ |
| 828 | +
|
| 829 | +<repair_tasks> |
| 830 | +{{REPAIR_TASKS}} |
| 831 | +</repair_tasks> |
| 832 | +
|
| 833 | +If the block above is not empty, the daemon's deterministic post-write \ |
| 834 | +pass found library invariants it could NOT fix on its own and is handing \ |
| 835 | +them to you. These are NOT discretionary — propose an action to repair \ |
| 836 | +each one. They take priority over salience-driven proposals. |
| 837 | +
|
| 838 | +Each task names a ``file`` (relative to ``knowledge/``), a broken ``target`` \ |
| 839 | +(the wikilink that does not resolve), and a ``context`` snippet showing \ |
| 840 | +where it appears. For EACH broken-wikilink task, do this: |
| 841 | +
|
| 842 | + 1. **Research the intended target.** The broken target usually got the \ |
| 843 | +PATH wrong, not the concept. Run ``mcp__agent_mem_library__bm25_search`` \ |
| 844 | +AND ``mcp__agent_mem_library__embedding_search`` IN PARALLEL on the \ |
| 845 | +target's leaf name and the surrounding context, and ``Glob("**/<leaf>.md")`` \ |
| 846 | +for the filename. Read the top hits to confirm which existing entry the \ |
| 847 | +link was meant to point at. |
| 848 | +
|
| 849 | + 2. **Then propose exactly ONE of these EXISTING actions** (no new action \ |
| 850 | +type — the Scholar executes it as a normal proposal): |
| 851 | + - **Link points at the wrong path but the right entry EXISTS** → \ |
| 852 | +``update_entry`` on ``file`` whose ``new_body`` is the file's full body \ |
| 853 | +with the broken ``[[target]]`` rewritten to the correct \ |
| 854 | +``[[full/path/from/knowledge/root]]`` (no ``.md``; trailing ``/`` for a \ |
| 855 | +folder link). Read ``file`` first so you reproduce its body faithfully and \ |
| 856 | +change only the link. |
| 857 | + - **The intended target genuinely does NOT exist yet but SHOULD** \ |
| 858 | +(the link describes a real lesson worth having) → ``write_entry`` creating \ |
| 859 | +the missing entry at the path the link points to, with proper frontmatter \ |
| 860 | +and body. The link then resolves. |
| 861 | + - **The link is bogus / the concept isn't worth an entry** → \ |
| 862 | +``update_entry`` on ``file`` that removes the broken ``[[target]]`` (drop \ |
| 863 | +the link or replace it with plain descriptive text), preserving the rest \ |
| 864 | +of the prose. Explain in ``reasoning`` why no target should exist. |
| 865 | +
|
| 866 | + 3. In ``reasoning``, quote the task's ``file`` and ``target`` and state \ |
| 867 | +which of the three fixes you chose and why (cite the entry you found, or \ |
| 868 | +state that no entry exists). Set ``salience_signal: null`` for repair \ |
| 869 | +proposals — they are integrity fixes, not salience judgments. |
| 870 | +
|
| 871 | +Do the link research with the SAME parallel-search discipline as for \ |
| 872 | +dedup. A repair proposal that guesses the path without searching will \ |
| 873 | +likely be vetoed. |
| 874 | +
|
825 | 875 | ═══════════════════════════════════════════════════════════════════ |
826 | 876 | HIERARCHY INVARIANTS (the Scholar will veto violations) |
827 | 877 | ═══════════════════════════════════════════════════════════════════ |
@@ -980,24 +1030,50 @@ def load_prompt_template() -> str: |
980 | 1030 | return _PROMPT_TEMPLATE |
981 | 1031 |
|
982 | 1032 |
|
| 1033 | +_NO_REPAIR_TASKS = "(none — no integrity-repair tasks this run)" |
| 1034 | + |
| 1035 | + |
| 1036 | +def format_repair_tasks(tasks: Sequence[repair_queue.RepairTask]) -> str: |
| 1037 | + """Render drained integrity-repair tasks as the ``<repair_tasks>`` body. |
| 1038 | +
|
| 1039 | + One numbered block per task, listing kind/file/target/context so the |
| 1040 | + Librarian can research and repair each one. Returns a sentinel when |
| 1041 | + there are no tasks so the prompt block is never blank.""" |
| 1042 | + if not tasks: |
| 1043 | + return _NO_REPAIR_TASKS |
| 1044 | + lines: List[str] = [] |
| 1045 | + for i, t in enumerate(tasks, start=1): |
| 1046 | + lines.append(f"{i}. kind: {t.kind}") |
| 1047 | + lines.append(f" file: {t.file}") |
| 1048 | + lines.append(f" target: {t.target}") |
| 1049 | + if t.context: |
| 1050 | + lines.append(f" context: {t.context}") |
| 1051 | + return "\n".join(lines) |
| 1052 | + |
| 1053 | + |
983 | 1054 | def assemble_prompt( |
984 | 1055 | *, |
985 | 1056 | project_slug: str, |
986 | 1057 | rolling_buffer: str, |
987 | 1058 | library_snapshot: str, |
988 | 1059 | applies_when_table: str, |
| 1060 | + repair_tasks: str = _NO_REPAIR_TASKS, |
989 | 1061 | ) -> str: |
990 | 1062 | """Substitute placeholders into the prompt template. |
991 | 1063 |
|
992 | 1064 | ACTION_TYPES and RESPONSE_SHAPE are generated from ``_schemas.py`` |
993 | 1065 | at call time so the prompt instructions can never drift from the |
994 | | - Pydantic models the parser actually validates against. |
| 1066 | + Pydantic models the parser actually validates against. ``repair_tasks`` |
| 1067 | + is the pre-rendered ``<repair_tasks>`` body (see |
| 1068 | + :func:`format_repair_tasks`); it defaults to the empty sentinel so |
| 1069 | + callers that don't escalate anything need not pass it. |
995 | 1070 | """ |
996 | 1071 | out = load_prompt_template() |
997 | 1072 | for needle, value in ( |
998 | 1073 | ("{{PROJECT_SLUG}}", project_slug or "unknown"), |
999 | 1074 | ("{{ROLLING_BUFFER}}", rolling_buffer or "(empty)"), |
1000 | 1075 | ("{{LIBRARY_SNAPSHOT}}", library_snapshot or "(empty)"), |
| 1076 | + ("{{REPAIR_TASKS}}", repair_tasks or _NO_REPAIR_TASKS), |
1001 | 1077 | ("{{APPLIES_WHEN_TABLE}}", applies_when_table or "(empty)"), |
1002 | 1078 | ("{{ACTION_TYPES}}", describe_action_types_markdown()), |
1003 | 1079 | ("{{RESPONSE_SHAPE}}", describe_librarian_response_shape()), |
|
0 commit comments