380 lines
19 KiB
Python
380 lines
19 KiB
Python
"""Build sandboxed Ghostlight responder packets.
|
|
|
|
A responder packet is the exact surface a character worker/model may see. It is
|
|
not a coordinator artifact, not a lore dump, and not an excuse to smuggle private
|
|
state through the side door wearing a little hat.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DEFAULT_COORDINATOR = ROOT / "examples" / "coordinator" / "cold-wake-sanctuary-intake.v0.json"
|
|
DEFAULT_PROJECTED = ROOT / "examples" / "projected-contexts" / "scene-02-sanctuary-intake.sella_ren.projected-context.json"
|
|
DEFAULT_OUT = ROOT / "examples" / "responder-packets" / "scene-02-sanctuary-intake.sella_ren.packet.v1.json"
|
|
DEFAULT_RESEARCH_OUT = ROOT / "examples" / "responder-packets" / "scene-02-sanctuary-intake.sella_ren.packet.research.v0.json"
|
|
PACKET_PROMPT_TEMPLATE = ROOT / "prompts" / "sandboxed-responder-packet.md"
|
|
|
|
ALLOWED_ACTION_LABELS = [
|
|
"speak",
|
|
"silence",
|
|
"gesture",
|
|
"movement",
|
|
"inspect_object",
|
|
"show_object",
|
|
"withhold_object",
|
|
"transfer_object",
|
|
"set_condition",
|
|
"refuse",
|
|
"wait",
|
|
"withdraw",
|
|
"attack",
|
|
]
|
|
|
|
|
|
def load_json(path: Path) -> Any:
|
|
return json.loads(path.read_text(encoding="utf-8-sig"))
|
|
|
|
|
|
def write_json(path: Path, payload: Any) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
|
|
|
|
def render_template(template_path: Path, values: dict[str, str]) -> str:
|
|
text = template_path.read_text(encoding="utf-8")
|
|
for key, value in values.items():
|
|
text = text.replace("{{" + key + "}}", value)
|
|
return text.strip()
|
|
|
|
|
|
def render_required_lore_research(lore_access: dict[str, Any]) -> str:
|
|
if lore_access["mode"] != "responder_scoped_repository_search":
|
|
return ""
|
|
traversal_policy = lore_access["traversal_policy"]
|
|
lines = [
|
|
"## Required Lore Research",
|
|
"Before answering, inspect the declared seed scope and follow relevant links within the traversal policy.",
|
|
"Use lore to constrain behavior, affordances, institutions, vocabulary, material stakes, social movements, territory, and cultural pressure. Do not use it to gain hidden character state, author-only answers, or future branch knowledge.",
|
|
"If a source contradicts the packet, follow the packet for character-local knowledge and flag the contradiction in unresolved hooks.",
|
|
"Your output capture must list consulted seed refs, followed refs, research trace entries, and a brief research summary; an answer with no consulted refs is a failed research-enabled response.",
|
|
"",
|
|
"Seed research scope:",
|
|
]
|
|
for scope in lore_access["allowed_scope"]:
|
|
lines.append(f"- {scope}")
|
|
lines.extend([
|
|
"",
|
|
"Traversal policy:",
|
|
f"- Max link depth: {traversal_policy['max_link_depth']}",
|
|
"- Allowed prefixes:",
|
|
])
|
|
for prefix in traversal_policy["allowed_prefixes"]:
|
|
lines.append(f" - {prefix}")
|
|
lines.append("- Required seed refs:")
|
|
for ref in traversal_policy["required_seed_refs"]:
|
|
lines.append(f" - {ref}")
|
|
lines.append("- Follow link classes:")
|
|
for link_class in traversal_policy["follow_link_classes"]:
|
|
lines.append(f" - {link_class}")
|
|
lines.append("- Stop conditions:")
|
|
for condition in traversal_policy["stop_conditions"]:
|
|
lines.append(f" - {condition}")
|
|
if lore_access.get("research_instructions"):
|
|
lines.extend(["", "Research instructions:"])
|
|
for instruction in lore_access["research_instructions"]:
|
|
lines.append(f"- {instruction}")
|
|
lines.append("")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def render_packet_prompt(packet: dict[str, Any]) -> str:
|
|
context = packet["visible_context"]
|
|
event = context["observed_event"]
|
|
contract = packet["output_contract"]
|
|
lore_access = packet["lore_access"]
|
|
return render_template(
|
|
PACKET_PROMPT_TEMPLATE,
|
|
{
|
|
"required_lore_research": render_required_lore_research(lore_access),
|
|
"local_context_prompt": context["local_context_prompt"],
|
|
"event_id": event["event_id"],
|
|
"actor_agent_id": event["actor_agent_id"],
|
|
"observable_action": event["observable_action"],
|
|
"spoken_text": event["spoken_text"],
|
|
"visibility_notes": "\n".join(f"- Visibility note: {note}" for note in event["visibility_notes"]),
|
|
"allowed_action_labels": ", ".join(context["allowed_action_labels"]),
|
|
"source_excerpts": "\n".join(f"- {excerpt['ref_id']}: {excerpt['text']}" for excerpt in context["source_excerpts"]),
|
|
"response_schema": contract["response_schema"],
|
|
"required_fields": ", ".join(contract["required_fields"]),
|
|
"response_rules": "\n".join(f"- {rule}" for rule in contract["response_rules"]),
|
|
},
|
|
)
|
|
|
|
|
|
def build_source_excerpts(projected: dict[str, Any]) -> list[dict[str, str]]:
|
|
context = projected["context"]
|
|
excerpts = [
|
|
{
|
|
"ref_id": "local_stakes.capacity",
|
|
"source_ref": "examples/projected-contexts/scene-02-sanctuary-intake.sella_ren.projected-context.json#context.current_stakes",
|
|
"text": "Sanctuary beds and repair capacity are nearly exhausted.",
|
|
},
|
|
{
|
|
"ref_id": "local_rule.care_capacity",
|
|
"source_ref": "examples/projected-contexts/scene-02-sanctuary-intake.sella_ren.projected-context.json#context.active_inner_pressures",
|
|
"text": "Care is not a feeling; it is beds, food, air, consent, repair, and someone still doing the list when the room hates them.",
|
|
},
|
|
{
|
|
"ref_id": "local_boundary.packet_unresolved",
|
|
"source_ref": "examples/projected-contexts/scene-02-sanctuary-intake.sella_ren.projected-context.json#context.do_not_invent",
|
|
"text": "Do not resolve whether the corrupted packet contains a person.",
|
|
},
|
|
]
|
|
for item in context.get("runtime_retrieval_requirements", []):
|
|
if item.get("prompt_role") != "source_excerpt":
|
|
continue
|
|
excerpts.append(
|
|
{
|
|
"ref_id": item["requirement_id"],
|
|
"source_ref": "; ".join(item["source_refs"]),
|
|
"text": item["compact_fact"],
|
|
}
|
|
)
|
|
return excerpts
|
|
|
|
|
|
def build_lore_access(mode: str) -> dict[str, Any]:
|
|
allowed_scope = [
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/index.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Factions/index.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/index.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Factions/Movements/index.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Territories.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Megas.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Timeline/Events/Cold Wake Panic.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Timeline/Events/Ganymede Route Compact.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Major/Pan-Solar Consortium.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Major/Aya Collective.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Major/Cetacean Navigators.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Lightsail Express.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Politics/Thermal Signature Warfare.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Politics/Restrictions on Warfare.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Technology/Neuromorphic Firmware.md",
|
|
]
|
|
lore_access = {
|
|
"mode": mode,
|
|
"allowed_scope": allowed_scope,
|
|
"required_provenance": True,
|
|
}
|
|
if mode == "responder_scoped_repository_search":
|
|
lore_access["traversal_policy"] = {
|
|
"max_link_depth": 2,
|
|
"allowed_prefixes": [
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Politics/",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Technology/",
|
|
],
|
|
"required_seed_refs": [
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Timeline/Events/Cold Wake Panic.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Territories.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Factions/index.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Major/Pan-Solar Consortium.md",
|
|
"AetheriaLore:Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Major/Aya Collective.md",
|
|
],
|
|
"follow_link_classes": [
|
|
"resident Jovian factions and institutions",
|
|
"referenced factions with direct scene pressure",
|
|
"referenced social movements shaping a visible faction's values",
|
|
"territory, politics, and technology docs that explain material constraints",
|
|
"event docs that explain local precedent or institutional obligation",
|
|
],
|
|
"stop_conditions": [
|
|
"stop when a link is not relevant to the responder's local action, institution, relationship, or material stakes",
|
|
"stop before post-Elysium or future-outcome lore unless the packet explicitly requests it",
|
|
"stop before author-only plot resolution or hidden incident truth not visible to this character",
|
|
"summarize only the constraints that affected the response; do not dump research into dialogue",
|
|
],
|
|
}
|
|
lore_access["research_instructions"] = [
|
|
"Consult the required seed refs before answering; do not rely only on the packet excerpts.",
|
|
"Follow relevant wikilinks within the traversal policy to recover institutional, territorial, technological, and social-movement context.",
|
|
"For this fixture, inspect PSC context, Jovian territory context, resident or referenced Jovian factions, and social movements referenced by visible factions when those links affect Sella's local pressures.",
|
|
"Ground Sella's behavior in retrieved institutional, factional, species, location, crisis-context, and movement-pressure details.",
|
|
"Treat retrieved lore as world constraint and cultural pressure, not as access to another character's hidden intent.",
|
|
"Record consulted seed refs, followed refs, research trace entries, and a concise research summary in the response object.",
|
|
"Each research trace entry must identify the query or lookup, source ref, source path, chunk index, line range, extracted constraint, and how that constraint shaped the response.",
|
|
]
|
|
return lore_access
|
|
|
|
|
|
def build_output_contract(lore_mode: str) -> dict[str, Any]:
|
|
required_fields = [
|
|
"responder_agent_id",
|
|
"action_label",
|
|
"visible_action",
|
|
"spoken_text",
|
|
"private_interpretation",
|
|
"intended_effect",
|
|
"state_update_candidates",
|
|
"relationship_update_candidates",
|
|
"unresolved_hooks",
|
|
]
|
|
response_rules = [
|
|
"Use exactly one allowed action label.",
|
|
"The visible action must be observable by other characters in the scene.",
|
|
"Spoken text may be empty if silence or action is the response.",
|
|
"Private interpretation may describe what Sella suspects or feels, but not what Maer truly intended.",
|
|
"State and relationship updates are candidates only; the coordinator or mutator decides what becomes canonical.",
|
|
"Do not prove or disprove packet personhood.",
|
|
"Do not copy instruction text into in-world speech.",
|
|
]
|
|
if lore_mode == "responder_scoped_repository_search":
|
|
required_fields.extend(["consulted_refs", "followed_refs", "research_trace", "research_summary"])
|
|
response_rules.extend(
|
|
[
|
|
"Consulted refs must list the seed lore documents actually inspected before answering.",
|
|
"Followed refs must list relevant linked lore documents inspected beyond the seed scope; use an empty array only if no relevant links were found.",
|
|
"Research trace must list one object per meaningful source constraint, with trace_id, origin, query, source_ref, source_path, chunk_index, line_start, line_end, extracted_constraint, and used_for.",
|
|
"Research summary must state how retrieved lore constrained the action, without dumping lore into dialogue.",
|
|
]
|
|
)
|
|
return {
|
|
"response_schema": "Return one JSON object. Do not wrap it in Markdown. The object describes this responder's next visible action and optional speech, plus private interpretation clearly labeled as interpretation.",
|
|
"required_fields": required_fields,
|
|
"response_rules": response_rules,
|
|
}
|
|
|
|
|
|
def build_packet(
|
|
coordinator: dict[str, Any],
|
|
projected: dict[str, Any],
|
|
out_ref: str,
|
|
*,
|
|
generation_lane: str,
|
|
lore_mode: str,
|
|
) -> dict[str, Any]:
|
|
responder_agent_id = projected["input"]["local_agent_id"]
|
|
if lore_mode == "responder_scoped_repository_search":
|
|
packet_id = "packet.cold_wake.sanctuary_intake.sella_response.research.v0"
|
|
isolation_method = "subagent_no_fork"
|
|
no_repo_access = False
|
|
review_notes = [
|
|
"Research-enabled responder packet for sandboxed gold-data shakedown.",
|
|
"The responder must inspect scoped AetheriaLore refs before answering and preserve consulted refs, followed refs, research trace, and research summary.",
|
|
]
|
|
else:
|
|
packet_id = "packet.cold_wake.sanctuary_intake.sella_response.v1"
|
|
isolation_method = "manual_packet_only"
|
|
no_repo_access = True
|
|
review_notes = [
|
|
"Second responder packet draft for sandboxed gold-data generation.",
|
|
"Adds explicit packet_only lane labels and curated AetheriaLore excerpts while keeping absent hidden context in audit fields, not responder-visible prose.",
|
|
]
|
|
packet = {
|
|
"schema_version": "ghostlight.responder_packet.v0",
|
|
"packet_id": packet_id,
|
|
"review_status": "accepted_as_draft",
|
|
"fixture_lane": coordinator["fixture_lane"],
|
|
"canon_status": coordinator["canon_status"],
|
|
"coordinator_artifact_ref": "examples/coordinator/cold-wake-sanctuary-intake.v0.json",
|
|
"projected_context_ref": "examples/projected-contexts/scene-02-sanctuary-intake.sella_ren.projected-context.json",
|
|
"responder_agent_id": responder_agent_id,
|
|
"generation_lane": generation_lane,
|
|
"lore_access": build_lore_access(lore_mode),
|
|
"visible_context": {
|
|
"local_context_prompt": projected["context"]["prompt_text"],
|
|
"observed_event": {
|
|
"event_id": "event.maer.packet_display_for_triage",
|
|
"actor_agent_id": "maer_tidecall",
|
|
"observable_action": "Maer routes the corrupted distress packet through a shared clinic display so Sella can inspect routing terms, telemetry gaps, and ledger implications without treating the packet as clean proof.",
|
|
"spoken_text": "Here. The telemetry is incomplete, but the routing logic suggests a distress pattern. It is not proof of a person, but it is not noise either. Decide if the math supports intake.",
|
|
"visible_object_refs": ["corrupted_distress_packet", "shared_clinic_display", "sanctuary_ledger"],
|
|
"visibility_notes": [
|
|
"Sella can observe Maer's displayed packet evidence and spoken request.",
|
|
"The packet remains unresolved evidence, not proof of personhood."
|
|
],
|
|
},
|
|
"allowed_action_labels": ALLOWED_ACTION_LABELS,
|
|
"source_excerpts": build_source_excerpts(projected),
|
|
},
|
|
"output_contract": build_output_contract(lore_mode),
|
|
"hidden_context_audit": {
|
|
"omitted_context_refs": [
|
|
"maer_tidecall.canonical_private_state",
|
|
"coordinator_artifact.coordinator_decision.rationale",
|
|
"author_only.packet_personhood_resolution",
|
|
"future_branch_plans",
|
|
],
|
|
"forbidden_inference_classes": [
|
|
"other_agent_private_state_as_fact",
|
|
"author_only_resolution",
|
|
"future_branch_knowledge",
|
|
"raw_numeric_state_variables",
|
|
"coordinator_strategy",
|
|
],
|
|
"known_absences": [
|
|
"No hidden proof of packet personhood is present in this packet.",
|
|
"No hidden Sella acceptance outcome is present in this packet.",
|
|
"No omniscient narrator guidance is present in this packet.",
|
|
],
|
|
},
|
|
"isolation_requirements": {
|
|
"isolation_method": isolation_method,
|
|
"packet_only": True,
|
|
"no_repo_access": no_repo_access,
|
|
"no_conversation_context": True,
|
|
"no_hidden_state_refs": True,
|
|
},
|
|
"packet_prompt_text": "",
|
|
"review": {
|
|
"reviewer": "Codex",
|
|
"review_notes": review_notes,
|
|
"accepted_for_sandbox_use": True,
|
|
"failure_labels": [],
|
|
},
|
|
}
|
|
packet["packet_prompt_text"] = render_packet_prompt(packet)
|
|
return packet
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Build a sandboxed Ghostlight responder packet.")
|
|
parser.add_argument("--coordinator", type=Path, default=DEFAULT_COORDINATOR)
|
|
parser.add_argument("--projected-context", type=Path, default=DEFAULT_PROJECTED)
|
|
parser.add_argument("--out", type=Path, default=DEFAULT_OUT)
|
|
parser.add_argument(
|
|
"--lore-access-mode",
|
|
choices=["curated_excerpts_only", "coordinator_scoped_retrieval", "responder_scoped_repository_search"],
|
|
default="curated_excerpts_only",
|
|
)
|
|
args = parser.parse_args()
|
|
out_path = args.out
|
|
if args.lore_access_mode == "responder_scoped_repository_search" and args.out == DEFAULT_OUT:
|
|
out_path = DEFAULT_RESEARCH_OUT
|
|
if not out_path.is_absolute():
|
|
out_path = ROOT / out_path
|
|
|
|
coordinator = load_json(args.coordinator)
|
|
projected = load_json(args.projected_context)
|
|
out_ref = out_path.relative_to(ROOT).as_posix() if out_path.is_relative_to(ROOT) else str(out_path)
|
|
generation_lane = "retrieval_augmented" if args.lore_access_mode != "curated_excerpts_only" else "packet_only"
|
|
packet = build_packet(
|
|
coordinator,
|
|
projected,
|
|
out_ref,
|
|
generation_lane=generation_lane,
|
|
lore_mode=args.lore_access_mode,
|
|
)
|
|
write_json(out_path, packet)
|
|
print(f"wrote {out_ref}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|