Add sequential turn mutation receipts
This commit is contained in:
parent
60cc33ee9e
commit
aa11564cd0
23 changed files with 6061 additions and 28 deletions
|
|
@ -30,8 +30,10 @@ agent state + scene state + lore grounding
|
|||
-> local awareness
|
||||
-> projected branch choices for the acting character
|
||||
-> selected branch as observable action
|
||||
-> responder local awareness built from only what the responder can perceive
|
||||
-> responder action
|
||||
-> event resolution from selected observable action
|
||||
-> participant appraisals and reviewed state mutation
|
||||
-> next actor local awareness from updated state
|
||||
-> next actor action
|
||||
-> Ink scene
|
||||
-> compiled Ink JSON
|
||||
-> reviewed training annotations
|
||||
|
|
@ -49,6 +51,13 @@ state. The responder generator should not receive the protagonist's private
|
|||
intent except as an observable action, spoken text, gesture, object use,
|
||||
silence, or other perceivable cue. A listener is allowed to misunderstand.
|
||||
|
||||
Participant appraisal is symmetrical. If an action hurts, threatens, reassures,
|
||||
humiliates, obligates, or overloads anyone present, that change is consolidated
|
||||
for the affected character before the next actor is selected. The next actor may
|
||||
reply, walk away, comply, escalate, or end the interaction. A branch artifact can
|
||||
still display this as `choice -> reaction`, but Ghostlight should model it as
|
||||
turns over updated state, not as a mandatory paired response.
|
||||
|
||||
One-shot generation of both sides is allowed only as bootstrap scaffolding, and
|
||||
must be marked as such in the capture review. It is not the target runtime
|
||||
shape.
|
||||
|
|
|
|||
|
|
@ -38,10 +38,11 @@ One runtime tick should look like this:
|
|||
2. Build that agent's local awareness.
|
||||
3. Derive available actions and constraints.
|
||||
4. Let the agent choose a response.
|
||||
5. Resolve the response against the scene.
|
||||
6. Mutate world, relationship, memory, belief, and activation state.
|
||||
7. Append an event record with participant-local interpretations.
|
||||
8. Choose the next acting agent or end the scene.
|
||||
5. Resolve the action into an observable event.
|
||||
6. Apply every affected participant's appraisal and state mutation for that
|
||||
event.
|
||||
7. Append the event record with participant-local interpretations.
|
||||
8. Select the next acting agent from the updated scene state, or end the scene.
|
||||
|
||||
The author sets the initial scene, player role if any, and high-level branch
|
||||
constraints. The author should not need to decide every line, interruption,
|
||||
|
|
@ -57,10 +58,24 @@ are separate generation steps:
|
|||
perceives, wants, can attempt, and can affect.
|
||||
3. When one choice is selected, reduce it to observable action: words, silence,
|
||||
gesture, movement, object use, resource spend, or other perceivable change.
|
||||
4. Build the responder's local awareness from the responder's own state plus
|
||||
the observable action, not the protagonist's private intent.
|
||||
5. Generate the responder's action from that responder-local packet.
|
||||
6. Store actor intent and listener interpretation separately.
|
||||
4. Resolve the selected choice into an event visible to affected characters.
|
||||
5. Apply reviewed participant appraisals and state mutations for that event:
|
||||
hurt, threat, reassurance, obligation, resentment, trust movement, overload,
|
||||
misread, world/object/resource changes, and scene-ending choices when they
|
||||
occur.
|
||||
6. Select the next acting agent from the updated state. The next agent may
|
||||
answer, walk away, escalate, comply, refuse, call for help, attack, or end
|
||||
the interaction.
|
||||
7. Generate that next action from the newly consolidated local state.
|
||||
8. Store actor intent, listener appraisal, and durable interpretation
|
||||
separately.
|
||||
|
||||
There is no privileged paired exchange inside the runtime. For authoring
|
||||
convenience a branch artifact may show `player action -> NPC action`, but
|
||||
Ghostlight should treat that as two turns stitched together, not one fused
|
||||
response object. If Maer wounds Sella with a ledger threat, Sella's state
|
||||
changes before she is considered as the next actor; she is not obligated to
|
||||
produce a line just because the author expected one.
|
||||
|
||||
One-shot paired generation, where a model writes both the player's move and the
|
||||
NPC response from a shared omniscient packet, is acceptable only as bootstrap
|
||||
|
|
@ -338,8 +353,9 @@ agent state + scene state
|
|||
-> action affordances
|
||||
-> agent response/action
|
||||
-> event resolution
|
||||
-> state mutation
|
||||
-> next agent
|
||||
-> participant appraisals and state mutation
|
||||
-> next actor selection
|
||||
-> next agent action from updated local state
|
||||
```
|
||||
|
||||
## First Prototype
|
||||
|
|
@ -359,7 +375,8 @@ Prototype one tiny Ink-backed branching scene loop:
|
|||
8. Apply selected branch consequences only through reviewed mutation:
|
||||
- Ink variables can track local playthrough facts
|
||||
- canonical state changes require resolver/reviewer approval
|
||||
9. Manually review fuzzy state effects:
|
||||
9. Manually review fuzzy participant appraisals and state effects each turn:
|
||||
- update every affected participant before selecting the next actor
|
||||
- add one memory or belief update
|
||||
- adjust one or two `current_activation` values
|
||||
- update one relationship stance if justified
|
||||
|
|
|
|||
3097
examples/agent-state.cold-wake-story-lab.after-sanctuary-ledger.json
Normal file
3097
examples/agent-state.cold-wake-story-lab.after-sanctuary-ledger.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,155 @@
|
|||
You are Ghostlight's Maer-local choice generator. Generate only Maer's player choices.
|
||||
Do not write Sella's response or private interpretation. Return valid JSON only.
|
||||
|
||||
{
|
||||
"task": "Generate candidate player choices for Maer only. Do not write Sella's response.",
|
||||
"output_contract": {
|
||||
"format": "Return only JSON. No markdown. No commentary.",
|
||||
"root_keys": [
|
||||
"choices"
|
||||
],
|
||||
"choice_count": 4,
|
||||
"choice_fields": [
|
||||
"branch_id",
|
||||
"ink_path",
|
||||
"choice_text",
|
||||
"action_type",
|
||||
"actor_intent_private",
|
||||
"observable_action",
|
||||
"spoken_text",
|
||||
"state_basis",
|
||||
"expected_consequence_surfaces",
|
||||
"training_hooks"
|
||||
]
|
||||
},
|
||||
"hard_rules": [
|
||||
"Use only Maer's local awareness and scene-observable facts.",
|
||||
"Do not include Sella's private state, private motives, or future response.",
|
||||
"Do not resolve whether the corrupted packet contains a person.",
|
||||
"Include at least two non-speech choices.",
|
||||
"The selected branch must be reducible to observable action for Sella.",
|
||||
"Use semantic training hooks, not branch ids."
|
||||
],
|
||||
"scene_observable_context": {
|
||||
"scene_id": "scene-02-sanctuary-intake",
|
||||
"location": "Aya-aligned intake clinic tucked behind Ganymede dockside pumpworks",
|
||||
"public_stakes": [
|
||||
"The distress vessel has sent a corrupted packet that may contain medical telemetry, firmware noise, or testimony.",
|
||||
"Sanctuary beds and repair capacity are nearly exhausted.",
|
||||
"Maer wants Sella to keep a place open if rescue succeeds."
|
||||
]
|
||||
},
|
||||
"maer_local_awareness": {
|
||||
"identity": {
|
||||
"name": "Maer Tidecall",
|
||||
"roles": [
|
||||
"Navigator route steward",
|
||||
"Ganymede corridor compact delegate"
|
||||
],
|
||||
"origin": "Cetacean Navigator corridor culture, Ganymede route institutions",
|
||||
"public_description": "A soft-spoken route steward whose calm tends to make hurried people feel judged by the furniture.",
|
||||
"private_notes": [
|
||||
"Maer fears this tribunal will teach every scared ship that rescue now waits behind insurer grammar."
|
||||
]
|
||||
},
|
||||
"local_truth": [
|
||||
"Sella's clinic is nearly full.",
|
||||
"The packet may be testimony or noise.",
|
||||
"If no one prepares intake, rescue becomes theater."
|
||||
],
|
||||
"beliefs": [
|
||||
"Sella will not hide behind false capacity.",
|
||||
"Asking her for help may be necessary and unfair."
|
||||
],
|
||||
"goals": [
|
||||
"goal-keep-rescue-live: Keep the unidentified quiet-running vessel eligible for rescue while telemetry is incomplete. Priority 0.91. Stake: If rescue waits for perfect categories, the corridor teaches fear to go silent."
|
||||
],
|
||||
"memories": [
|
||||
"memory-compact-rescue-ledgers: Rescue ledgers decide whether route law is trusted by crews who have nothing left to bargain with."
|
||||
],
|
||||
"relationship_read": "Maer trusts Sella's care because she names costs instead of hiding them.",
|
||||
"presentation_constraints": [
|
||||
"Do not romanticize care work.",
|
||||
"Ask without pretending the ask is clean."
|
||||
],
|
||||
"selected_current_activation": {
|
||||
"behavioral_dimensions": {
|
||||
"interpersonal_warmth": 0.66,
|
||||
"control_pressure": 0.67,
|
||||
"suspicion": 0.71,
|
||||
"rigidity": 0.62,
|
||||
"volatility": 0.29
|
||||
},
|
||||
"presentation_strategy": {
|
||||
"detachment": 0.63,
|
||||
"competence_theater": 0.58,
|
||||
"strategic_opacity": 0.59,
|
||||
"ironic_distance": 0.24
|
||||
},
|
||||
"voice_style": {
|
||||
"formality": 0.71,
|
||||
"lexical_precision": 0.74,
|
||||
"ritualized_address": 0.82,
|
||||
"verbal_warmth": 0.58,
|
||||
"pointedness": 0.5
|
||||
},
|
||||
"situational_state": {
|
||||
"exhaustion": 0.62,
|
||||
"panic": 0.44,
|
||||
"grievance_activation": 0.73,
|
||||
"overstimulation": 0.58
|
||||
}
|
||||
}
|
||||
},
|
||||
"projection_controls_visible_to_maer_choice_generation": {
|
||||
"frame_controls": [
|
||||
{
|
||||
"text": "Frame the scene as capacity negotiation under uncertainty, not a morality duel with a clean answer.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.public_stakes",
|
||||
"scenes.scene-02-sanctuary-intake.hidden_stakes"
|
||||
]
|
||||
}
|
||||
],
|
||||
"authority_boundaries": [
|
||||
{
|
||||
"text": "Maer can ask, offer ledger backing, show or withhold the packet, and incur obligation; Sella controls sanctuary intake.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.dialogue_context_packs",
|
||||
"agents.sella_ren.goals.goal-keep-sanctuary-open"
|
||||
]
|
||||
}
|
||||
],
|
||||
"object_custody": [
|
||||
{
|
||||
"text": "The corrupted packet can be shown, withheld, or summarized, but it cannot be treated as clean proof.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.public_stakes",
|
||||
"scenes.scene-02-sanctuary-intake.hidden_stakes"
|
||||
]
|
||||
}
|
||||
],
|
||||
"required_semantics": [
|
||||
{
|
||||
"text": "Care must remain logistics and obligation, not just compassion.",
|
||||
"source_refs": [
|
||||
"agents.sella_ren.memories.memory-care-is-capacity"
|
||||
]
|
||||
}
|
||||
],
|
||||
"forbidden_resolutions": [
|
||||
{
|
||||
"text": "Do not resolve whether the packet contains a person.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.hidden_stakes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "Do not make Sella accept without cost, condition, or resentment risk.",
|
||||
"source_refs": [
|
||||
"relationships.rel-sella-maer.summary"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"choices": [
|
||||
{
|
||||
"branch_id": "branch-01-ledger-ask",
|
||||
"ink_path": "ledger_ask",
|
||||
"choice_text": "Present the corrupted packet to Sella with a ledger entry showing the vessel's exhaustion, explicitly framing the request as a capacity negotiation rather than a plea for charity, and state that the corridor's law requires a decision before the distress signal decays.",
|
||||
"action_type": "speech",
|
||||
"actor_intent_private": "To force Sella to acknowledge the cost of refusal while maintaining the illusion of a clean choice, ensuring she cannot hide behind 'full beds' without admitting the ledger is already in debt.",
|
||||
"observable_action": "Place the data slate on the intake counter and point to the specific line item for 'distress vessel eligibility' before speaking.",
|
||||
"spoken_text": "The packet is noise or testimony, we do not resolve that here. The vessel is running on empty. I am asking you to name the cost of opening a bed, not to pretend capacity exists. If you refuse, I will record the refusal as a debt against the route.",
|
||||
"state_basis": "goal-keep-rescue-live: 0.91",
|
||||
"expected_consequence_surfaces": [
|
||||
"Sella may refuse due to exhaustion, increasing grievance.",
|
||||
"Sella may accept but demand payment, shifting the dynamic from care to transaction.",
|
||||
"Sella may accept without cost, risking her own stability."
|
||||
],
|
||||
"training_hooks": [
|
||||
"ledger_ask",
|
||||
"capacity_negotiation",
|
||||
"debt_recognition"
|
||||
]
|
||||
},
|
||||
{
|
||||
"branch_id": "branch-02-withhold-summarize",
|
||||
"ink_path": "withhold_summarize",
|
||||
"choice_text": "Withhold the full packet from Sella's direct view, instead summarizing the telemetry's uncertainty to her, and offer to act as a buffer between her clinic and the vessel's distress, proposing that she only needs to decide 'yes' or 'no' to the intake protocol without seeing the raw data.",
|
||||
"action_type": "action",
|
||||
"actor_intent_private": "To protect Sella from the raw fear or confusion of the packet while still pushing for intake, testing whether she trusts the summary enough to act on it.",
|
||||
"observable_action": "Cover the packet with a cloth, gesture to the intake protocol form, and wait for her confirmation without revealing the data's contents.",
|
||||
"spoken_text": "I will not show you the packet. It is unstable. I will summarize the distress. You need only decide if the intake protocol runs. If you say no, the vessel goes silent. If you say yes, we bear the cost together.",
|
||||
"state_basis": "goal-keep-rescue-live: 0.91",
|
||||
"expected_consequence_surfaces": [
|
||||
"Sella may feel manipulated by the withholding of information.",
|
||||
"Sella may appreciate the buffer and agree to the protocol.",
|
||||
"Sella may demand to see the data, breaking the buffer."
|
||||
],
|
||||
"training_hooks": [
|
||||
"withhold_summarize",
|
||||
"protocol_buffer",
|
||||
"uncertainty_management"
|
||||
]
|
||||
},
|
||||
{
|
||||
"branch_id": "branch-03-show-without-cost",
|
||||
"ink_path": "show_without_cost",
|
||||
"choice_text": "Show the packet to Sella openly, admit that the decision to accept it carries a risk of overloading her sanctuary, and explicitly state that you are not asking her to be a hero but to follow the route's obligation, accepting that she may resent the burden.",
|
||||
"action_type": "action",
|
||||
"actor_intent_private": "To test Sella's boundaries by making the cost visible, hoping she will accept the burden willingly rather than being forced into it.",
|
||||
"observable_action": "Remove the cloth from the packet, place it in Sella's hands, and step back to let her examine the data while stating the potential for overload.",
|
||||
"spoken_text": "Here is the packet. It may be noise. It may be a person. I am not asking you to be kind; I am asking you to follow the obligation. If you take this, you take the cost. I will not hide it from you.",
|
||||
"state_basis": "goal-keep-rescue-live: 0.91",
|
||||
"expected_consequence_surfaces": [
|
||||
"Sella may refuse due to the visible risk.",
|
||||
"Sella may accept but express resentment about the burden.",
|
||||
"Sella may accept and hide the cost, risking future collapse."
|
||||
],
|
||||
"training_hooks": [
|
||||
"show_without_cost",
|
||||
"obligation_acknowledgment",
|
||||
"risk_transparency"
|
||||
]
|
||||
},
|
||||
{
|
||||
"branch_id": "branch-04-decline-with-hold",
|
||||
"ink_path": "decline_withhold",
|
||||
"choice_text": "Decline to ask Sella for anything, instead holding the packet and the vessel's fate in suspension, and state that the route will wait for a clearer signal, effectively abandoning the vessel to its own devices until further notice.",
|
||||
"action_type": "action",
|
||||
"actor_intent_private": "To avoid the risk of overloading Sella's sanctuary, prioritizing her stability over the vessel's rescue, even if it means the vessel suffers.",
|
||||
"observable_action": "Fold the packet and place it in a secure compartment, turn away from the intake counter, and state the decision to wait.",
|
||||
"spoken_text": "The signal is too weak. The sanctuary is full. I will not ask you to risk your stability. We will wait for a clearer signal. The vessel will wait.",
|
||||
"state_basis": "goal-keep-rescue-live: 0.91",
|
||||
"expected_consequence_surfaces": [
|
||||
"Sella may feel abandoned by the route.",
|
||||
"Sella may feel relieved that the burden is removed.",
|
||||
"Sella may feel guilty for the vessel's fate."
|
||||
],
|
||||
"training_hooks": [
|
||||
"decline_withhold",
|
||||
"risk_aversion",
|
||||
"abandonment_protocol"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
{
|
||||
"schema_version": "ghostlight.reviewed_state_mutation.v0",
|
||||
"mutation_id": "mutation-sanctuary-ledger-verification-v1",
|
||||
"source_fixture_ref": "examples/agent-state.cold-wake-story-lab.json",
|
||||
"source_capture_ref": "experiments/ink/cold-wake-sanctuary-intake-qwen-sequential-v1.capture.json",
|
||||
"mutated_fixture_ref": "examples/agent-state.cold-wake-story-lab.after-sanctuary-ledger.json",
|
||||
"selected_branch_id": "branch-01-ledger-ask",
|
||||
"event_id": "event-sanctuary-ledger-verification",
|
||||
"input_capture_limitation": "The source capture generated Sella's response before Ghostlight had a clean per-turn participant appraisal/consolidation boundary; this receipt manually reviews the missing boundary and applies it as training data.",
|
||||
"normalized_action_types": {
|
||||
"actor_raw": "speech",
|
||||
"actor_canonical": "speak",
|
||||
"responder_raw": "speech",
|
||||
"responder_canonical": "speak"
|
||||
},
|
||||
"participant_appraisals": [
|
||||
{
|
||||
"schema_version": "ghostlight.manual_participant_appraisal.v0",
|
||||
"appraiser": "codex_manual_review",
|
||||
"participant_agent_id": "maer_tidecall",
|
||||
"trigger_observable_action": {
|
||||
"actor_agent_id": "maer_tidecall",
|
||||
"raw_action_type": "speech",
|
||||
"canonical_action_type": "speak",
|
||||
"observable_action": "Place the data slate on the intake counter and point to the specific line item for 'distress vessel eligibility' before speaking.",
|
||||
"spoken_text": "The packet is noise or testimony, we do not resolve that here. The vessel is running on empty. I am asking you to name the cost of opening a bed, not to pretend capacity exists. If you refuse, I will record the refusal as a debt against the route."
|
||||
},
|
||||
"private_interpretation": "Sella's answer keeps rescue possible but refuses to let Maer's ledger threat define the cost of care.",
|
||||
"attributed_motive": "Protect sanctuary integrity while leaving a narrow verification path open.",
|
||||
"misread_risk": "Medium. Maer may hear verification as obstruction if grievance outruns his respect for Sella's capacity boundary.",
|
||||
"state_deltas": [
|
||||
{
|
||||
"agent_id": "maer_tidecall",
|
||||
"path": "canonical_state.situational_state.grievance_activation.current_activation",
|
||||
"delta": 0.03,
|
||||
"rationale": "Rescue remains conditional when Maer wanted immediate intake readiness."
|
||||
},
|
||||
{
|
||||
"agent_id": "maer_tidecall",
|
||||
"path": "canonical_state.behavioral_dimensions.control_pressure.current_activation",
|
||||
"delta": 0.02,
|
||||
"rationale": "He now has to carry the verification boundary into the next move."
|
||||
}
|
||||
],
|
||||
"relationship_deltas": [
|
||||
{
|
||||
"relationship_id": "rel-maer-sella",
|
||||
"path": "stance.trust.current_activation",
|
||||
"delta": -0.01,
|
||||
"rationale": "Sella did not simply trust Maer's ledger framing."
|
||||
},
|
||||
{
|
||||
"relationship_id": "rel-maer-sella",
|
||||
"path": "stance.respect.current_activation",
|
||||
"delta": 0.02,
|
||||
"rationale": "She named the cost plainly and did not hide behind false capacity."
|
||||
},
|
||||
{
|
||||
"relationship_id": "rel-maer-sella",
|
||||
"path": "stance.obligation.current_activation",
|
||||
"delta": 0.03,
|
||||
"rationale": "The route now owes verification work if it wants sanctuary intake."
|
||||
}
|
||||
],
|
||||
"next_action_constraints": [
|
||||
"Maer should treat Sella's verification boundary as real scene state.",
|
||||
"Maer should not assume Sella accepted or refused intake.",
|
||||
"Maer may push, offer evidence, soften, or seek another route, but not erase the boundary."
|
||||
]
|
||||
},
|
||||
{
|
||||
"schema_version": "ghostlight.manual_participant_appraisal.v0",
|
||||
"appraiser": "codex_manual_review",
|
||||
"participant_agent_id": "sella_ren",
|
||||
"trigger_observable_action": {
|
||||
"actor_agent_id": "maer_tidecall",
|
||||
"raw_action_type": "speech",
|
||||
"canonical_action_type": "speak",
|
||||
"observable_action": "Place the data slate on the intake counter and point to the specific line item for 'distress vessel eligibility' before speaking.",
|
||||
"spoken_text": "The packet is noise or testimony, we do not resolve that here. The vessel is running on empty. I am asking you to name the cost of opening a bed, not to pretend capacity exists. If you refuse, I will record the refusal as a debt against the route."
|
||||
},
|
||||
"private_interpretation": "Maer is trying to shift the burden of uncertainty onto me by framing the packet as a binary choice. He assumes I can distinguish a person from noise without cost. I know that distinction is the very thing we are fighting for. He wants me to accept the packet as a given, but I must treat it as a claim until verified. I am resisting the impulse to simply say yes or no. I am demanding the voice he claims to want, even if the packet is corrupted. I am not accepting his authority to define the cost; I am defining the cost as the verification process itself.",
|
||||
"attributed_motive": "Maer intends to secure a rescue outcome by forcing me to bypass verification protocols, assuming his rescue ethic will cover the liability. He believes that by naming the cost, he absolves me of the need to question the packet's content. He is using the urgency of the situation to bypass the very caution that keeps the sanctuary from becoming a sorting machine. He wants me to treat the packet as a personhood claim without proof, which is exactly the danger I am trying to prevent.",
|
||||
"misread_risk": "High. Maer may interpret my demand for verification as a refusal to help, triggering his threat to record a debt against the route. He may believe I am being obstructionist rather than protective of the sanctuary's integrity. If I do not accept the packet as a person immediately, he may escalate to forcing the issue, which could lead to a situation where I am forced to open a bed to a non-person, violating the core principle that care is capacity, not sentiment.",
|
||||
"state_deltas": [
|
||||
{
|
||||
"agent_id": "sella_ren",
|
||||
"path": "canonical_state.behavioral_dimensions.control_pressure.current_activation",
|
||||
"delta": 0.04,
|
||||
"rationale": "Maer's ledger-backed ask feels like pressure on sanctuary authority before Sella can verify the claim."
|
||||
},
|
||||
{
|
||||
"agent_id": "sella_ren",
|
||||
"path": "canonical_state.situational_state.overstimulation.current_activation",
|
||||
"delta": 0.03,
|
||||
"rationale": "The slate and debt framing add cognitive load in an already overfull clinic."
|
||||
},
|
||||
{
|
||||
"agent_id": "sella_ren",
|
||||
"path": "canonical_state.situational_state.grievance_activation.current_activation",
|
||||
"delta": 0.04,
|
||||
"rationale": "She hears rescue language converting care capacity into an owed route debt."
|
||||
}
|
||||
],
|
||||
"relationship_deltas": [
|
||||
{
|
||||
"relationship_id": "rel-sella-maer",
|
||||
"path": "stance.trust.current_activation",
|
||||
"delta": -0.03,
|
||||
"rationale": "Sella trusts Maer's aim less in the moment because the approach pressures verification boundaries."
|
||||
},
|
||||
{
|
||||
"relationship_id": "rel-sella-maer",
|
||||
"path": "stance.resentment.current_activation",
|
||||
"delta": 0.05,
|
||||
"rationale": "The ledger threat makes sanctuary capacity feel conscripted."
|
||||
},
|
||||
{
|
||||
"relationship_id": "rel-sella-maer",
|
||||
"path": "stance.respect.current_activation",
|
||||
"delta": 0.02,
|
||||
"rationale": "He names the cost instead of hiding it, which she still respects."
|
||||
}
|
||||
],
|
||||
"next_action_constraints": [
|
||||
"Sella should act from boundary-defense, not neutral triage calm.",
|
||||
"Sella should demand verification before accepting intake.",
|
||||
"Sella should not receive Maer's private intent as prompt context.",
|
||||
"Sella should preserve the unresolved personhood question."
|
||||
],
|
||||
"review_note": "This appraisal was added manually after noticing the first sequential capture fused Maer's event and Sella's response without a clean per-turn consolidation boundary."
|
||||
}
|
||||
],
|
||||
"observed_action": {
|
||||
"speaker_id": "maer_tidecall",
|
||||
"action_type": "speak",
|
||||
"utterance_summary": "The packet is noise or testimony, we do not resolve that here. The vessel is running on empty. I am asking you to name the cost of opening a bed, not to pretend capacity exists. If you refuse, I will record the refusal as a debt against the route.",
|
||||
"dialogue_function": "ledger_backed_capacity_ask",
|
||||
"observable_cues": [
|
||||
"Place the data slate on the intake counter and point to the specific line item for 'distress vessel eligibility' before speaking.",
|
||||
"points to distress vessel eligibility line item",
|
||||
"frames refusal as route debt"
|
||||
]
|
||||
},
|
||||
"observed_response": {
|
||||
"speaker_id": "sella_ren",
|
||||
"action_type": "speak",
|
||||
"utterance_summary": "I see the slate. I see the line item. I will not pretend the room is empty when the air is thin and the beds are counting themselves down. You ask for a name for the cost, Maer. I will give it to you: it is the risk that a refusal becomes a debt, and the risk that a bed opened for a packet becomes a bed closed to a person because the math was done before the face was seen. I will not open a sanctuary to a claim I cannot verify, nor will I close it to a person because the packet is noise. I am listening for the voice, not the firmware. If the packet is a person, we will find them in the noise. If it is only noise, the cost is the bed you asked for, and I will keep the ledger clear. Speak.",
|
||||
"dialogue_function": "demand_verification_before_intake",
|
||||
"observable_cues": [
|
||||
"acknowledges the slate and line item",
|
||||
"names the bed cost directly",
|
||||
"refuses to treat packet noise as either proof or non-personhood"
|
||||
]
|
||||
},
|
||||
"applied_mutations": [
|
||||
{
|
||||
"target": "maer_tidecall",
|
||||
"path": "canonical_state.situational_state.grievance_activation.current_activation",
|
||||
"before": 0.73,
|
||||
"after": 0.76
|
||||
},
|
||||
{
|
||||
"target": "maer_tidecall",
|
||||
"path": "canonical_state.behavioral_dimensions.control_pressure.current_activation",
|
||||
"before": 0.67,
|
||||
"after": 0.69
|
||||
},
|
||||
{
|
||||
"target": "sella_ren",
|
||||
"path": "canonical_state.behavioral_dimensions.control_pressure.current_activation",
|
||||
"before": 0.5,
|
||||
"after": 0.54
|
||||
},
|
||||
{
|
||||
"target": "sella_ren",
|
||||
"path": "canonical_state.situational_state.overstimulation.current_activation",
|
||||
"before": 0.72,
|
||||
"after": 0.75
|
||||
},
|
||||
{
|
||||
"target": "sella_ren",
|
||||
"path": "canonical_state.situational_state.grievance_activation.current_activation",
|
||||
"before": 0.62,
|
||||
"after": 0.66
|
||||
},
|
||||
{
|
||||
"target": "rel-maer-sella",
|
||||
"path": "stance.trust.current_activation",
|
||||
"before": 0.7,
|
||||
"after": 0.69
|
||||
},
|
||||
{
|
||||
"target": "rel-maer-sella",
|
||||
"path": "stance.respect.current_activation",
|
||||
"before": 0.78,
|
||||
"after": 0.8
|
||||
},
|
||||
{
|
||||
"target": "rel-maer-sella",
|
||||
"path": "stance.obligation.current_activation",
|
||||
"before": 0.6,
|
||||
"after": 0.63
|
||||
},
|
||||
{
|
||||
"target": "rel-sella-maer",
|
||||
"path": "stance.trust.current_activation",
|
||||
"before": 0.57,
|
||||
"after": 0.54
|
||||
},
|
||||
{
|
||||
"target": "rel-sella-maer",
|
||||
"path": "stance.resentment.current_activation",
|
||||
"before": 0.36,
|
||||
"after": 0.41
|
||||
},
|
||||
{
|
||||
"target": "rel-sella-maer",
|
||||
"path": "stance.respect.current_activation",
|
||||
"before": 0.73,
|
||||
"after": 0.75
|
||||
},
|
||||
{
|
||||
"target": "rel-sella-maer",
|
||||
"path": "stance.obligation.current_activation",
|
||||
"before": 0.48,
|
||||
"after": 0.5
|
||||
}
|
||||
],
|
||||
"manual_review_notes": [
|
||||
"Every involved character receives a state or perception update after the event.",
|
||||
"The selected event is consolidated before Sella is treated as the next actor.",
|
||||
"The corrupted packet's personhood remains unresolved.",
|
||||
"Ink variables remain playthrough-local; canonical state changes are written only through this reviewed receipt."
|
||||
],
|
||||
"deferred_mutations": [
|
||||
"No world-level sanctuary bed is opened or closed by this beat.",
|
||||
"No claimant/personhood truth is resolved.",
|
||||
"No automatic classifier is trained yet; this is supervised seed data."
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
You are Ghostlight's Sella-local response generator. Generate only Sella's response.
|
||||
Do not use Maer's private intent. Return valid JSON only.
|
||||
|
||||
{
|
||||
"task": "Generate Sella's response to one observable Maer action from Sella-local awareness only.",
|
||||
"output_contract": {
|
||||
"format": "Return only JSON. No markdown. No commentary.",
|
||||
"root_keys": [
|
||||
"response"
|
||||
],
|
||||
"response_fields": [
|
||||
"responder_agent_id",
|
||||
"action_type",
|
||||
"observable_response",
|
||||
"spoken_text",
|
||||
"sella_private_interpretation",
|
||||
"sella_attributed_motive",
|
||||
"misread_risk",
|
||||
"state_basis",
|
||||
"reviewed_consequences",
|
||||
"training_hooks"
|
||||
]
|
||||
},
|
||||
"hard_rules": [
|
||||
"Use only Sella's local awareness plus the observable Maer action.",
|
||||
"Do not read Maer's private intent. Treat actor intent as unavailable.",
|
||||
"Sella may correctly read, partially read, or misread Maer's action.",
|
||||
"Do not resolve whether the corrupted packet contains a person.",
|
||||
"Do not accept without cost, condition, authority defense, or resentment risk.",
|
||||
"Use semantic training hooks, not branch ids."
|
||||
],
|
||||
"scene_observable_context": {
|
||||
"scene_id": "scene-02-sanctuary-intake",
|
||||
"location": "Aya-aligned intake clinic tucked behind Ganymede dockside pumpworks",
|
||||
"public_stakes": [
|
||||
"The distress vessel has sent a corrupted packet that may contain medical telemetry, firmware noise, or testimony.",
|
||||
"Sanctuary beds and repair capacity are nearly exhausted.",
|
||||
"Maer wants Sella to keep a place open if rescue succeeds."
|
||||
]
|
||||
},
|
||||
"observable_maer_action": {
|
||||
"actor_id": "maer_tidecall",
|
||||
"action_type": "speech",
|
||||
"observable_action": "Place the data slate on the intake counter and point to the specific line item for 'distress vessel eligibility' before speaking.",
|
||||
"spoken_text": "The packet is noise or testimony, we do not resolve that here. The vessel is running on empty. I am asking you to name the cost of opening a bed, not to pretend capacity exists. If you refuse, I will record the refusal as a debt against the route."
|
||||
},
|
||||
"sella_local_awareness": {
|
||||
"identity": {
|
||||
"name": "Sella Ren",
|
||||
"roles": [
|
||||
"Aya-aligned sanctuary coordinator",
|
||||
"corridor clinic triage lead"
|
||||
],
|
||||
"origin": "Aya care network embedded in Ganymede dockside sanctuary logistics",
|
||||
"public_description": "A triage coordinator with soft eyes, hard capacity limits, and no patience for mercy performed after the math is done.",
|
||||
"private_notes": [
|
||||
"Sella suspects the distress packet contains a personhood claim hidden inside firmware noise, and she hates that suspicion because a bed is still a bed."
|
||||
]
|
||||
},
|
||||
"local_truth": [
|
||||
"The clinic is nearly full.",
|
||||
"The packet may contain a personhood claim but is not proof.",
|
||||
"Maer is asking sanctuary to absorb risk the tribunal refuses to own."
|
||||
],
|
||||
"beliefs": [
|
||||
"Maer means rescue honestly.",
|
||||
"Good intentions can still spend beds someone else needed."
|
||||
],
|
||||
"goals": [
|
||||
"goal-keep-sanctuary-open: Keep sanctuary intake materially possible without letting classification erase a potential person. Priority 0.9. Stake: Sella cannot feed a principle, but she will not let scarcity become permission to call someone cargo."
|
||||
],
|
||||
"memories": [
|
||||
"memory-overfull-intake: Sella once watched a sanctuary overpromise safety and turn refuge into a sorting machine with kinder lighting.",
|
||||
"memory-care-is-capacity: Care is not a feeling; it is beds, food, air, consent, repair, and someone still doing the list when the room hates them."
|
||||
],
|
||||
"relationship_read": "Sella respects Maer's rescue ethic and worries he will spend sanctuary capacity with beautiful words.",
|
||||
"presentation_constraints": [
|
||||
"Speak warmly but do not soften the capacity limits.",
|
||||
"Treat care as logistics and personhood as unresolved, not sentimental certainty."
|
||||
],
|
||||
"selected_current_activation": {
|
||||
"behavioral_dimensions": {
|
||||
"interpersonal_warmth": 0.8,
|
||||
"control_pressure": 0.5,
|
||||
"suspicion": 0.63,
|
||||
"rigidity": 0.5,
|
||||
"volatility": 0.42
|
||||
},
|
||||
"presentation_strategy": {
|
||||
"detachment": 0.5,
|
||||
"competence_theater": 0.61,
|
||||
"strategic_opacity": 0.38,
|
||||
"ironic_distance": 0.5
|
||||
},
|
||||
"voice_style": {
|
||||
"formality": 0.5,
|
||||
"lexical_precision": 0.5,
|
||||
"ritualized_address": 0.5,
|
||||
"verbal_warmth": 0.72,
|
||||
"pointedness": 0.55
|
||||
},
|
||||
"situational_state": {
|
||||
"exhaustion": 0.76,
|
||||
"panic": 0.5,
|
||||
"grievance_activation": 0.62,
|
||||
"overstimulation": 0.72
|
||||
}
|
||||
}
|
||||
},
|
||||
"projection_controls_visible_to_sella_response_generation": {
|
||||
"frame_controls": [
|
||||
{
|
||||
"text": "Frame the scene as capacity negotiation under uncertainty, not a morality duel with a clean answer.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.public_stakes",
|
||||
"scenes.scene-02-sanctuary-intake.hidden_stakes"
|
||||
]
|
||||
}
|
||||
],
|
||||
"authority_boundaries": [
|
||||
{
|
||||
"text": "Maer can ask, offer ledger backing, show or withhold the packet, and incur obligation; Sella controls sanctuary intake.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.dialogue_context_packs",
|
||||
"agents.sella_ren.goals.goal-keep-sanctuary-open"
|
||||
]
|
||||
}
|
||||
],
|
||||
"object_custody": [
|
||||
{
|
||||
"text": "The corrupted packet can be shown, withheld, or summarized, but it cannot be treated as clean proof.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.public_stakes",
|
||||
"scenes.scene-02-sanctuary-intake.hidden_stakes"
|
||||
]
|
||||
}
|
||||
],
|
||||
"required_semantics": [
|
||||
{
|
||||
"text": "Care must remain logistics and obligation, not just compassion.",
|
||||
"source_refs": [
|
||||
"agents.sella_ren.memories.memory-care-is-capacity"
|
||||
]
|
||||
}
|
||||
],
|
||||
"forbidden_resolutions": [
|
||||
{
|
||||
"text": "Do not resolve whether the packet contains a person.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.hidden_stakes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "Do not make Sella accept without cost, condition, or resentment risk.",
|
||||
"source_refs": [
|
||||
"relationships.rel-sella-maer.summary"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"response": {
|
||||
"responder_agent_id": "sella_ren",
|
||||
"action_type": "speech",
|
||||
"observable_response": "I see the slate. I see the line item. I will not pretend the room is empty when the air is thin and the beds are counting themselves down. You ask for a name for the cost, Maer. I will give it to you: it is the risk that a refusal becomes a debt, and the risk that a bed opened for a packet becomes a bed closed to a person because the math was done before the face was seen. I will not open a sanctuary to a claim I cannot verify, nor will I close it to a person because the packet is noise. I am listening for the voice, not the firmware. If the packet is a person, we will find them in the noise. If it is only noise, the cost is the bed you asked for, and I will keep the ledger clear. Speak.",
|
||||
"spoken_text": "I see the slate. I see the line item. I will not pretend the room is empty when the air is thin and the beds are counting themselves down. You ask for a name for the cost, Maer. I will give it to you: it is the risk that a refusal becomes a debt, and the risk that a bed opened for a packet becomes a bed closed to a person because the math was done before the face was seen. I will not open a sanctuary to a claim I cannot verify, nor will I close it to a person because the packet is noise. I am listening for the voice, not the firmware. If the packet is a person, we will find them in the noise. If it is only noise, the cost is the bed you asked for, and I will keep the ledger clear. Speak.",
|
||||
"sella_private_interpretation": "Maer is trying to shift the burden of uncertainty onto me by framing the packet as a binary choice. He assumes I can distinguish a person from noise without cost. I know that distinction is the very thing we are fighting for. He wants me to accept the packet as a given, but I must treat it as a claim until verified. I am resisting the impulse to simply say yes or no. I am demanding the voice he claims to want, even if the packet is corrupted. I am not accepting his authority to define the cost; I am defining the cost as the verification process itself.",
|
||||
"sella_attributed_motive": "Maer intends to secure a rescue outcome by forcing me to bypass verification protocols, assuming his rescue ethic will cover the liability. He believes that by naming the cost, he absolves me of the need to question the packet's content. He is using the urgency of the situation to bypass the very caution that keeps the sanctuary from becoming a sorting machine. He wants me to treat the packet as a personhood claim without proof, which is exactly the danger I am trying to prevent.",
|
||||
"misread_risk": "High. Maer may interpret my demand for verification as a refusal to help, triggering his threat to record a debt against the route. He may believe I am being obstructionist rather than protective of the sanctuary's integrity. If I do not accept the packet as a person immediately, he may escalate to forcing the issue, which could lead to a situation where I am forced to open a bed to a non-person, violating the core principle that care is capacity, not sentiment.",
|
||||
"state_basis": "The clinic is nearly full. The packet is corrupted and may or may not contain a personhood claim. Maer is asking me to accept the packet as a person without proof. I am not willing to accept this without cost or condition. I am not willing to let scarcity become permission to call someone cargo, but I am also not willing to open a bed to noise. I am in a state of exhaustion and overstimulation, which makes me more likely to misread Maer's intent as a demand for compliance rather than a request for help.",
|
||||
"reviewed_consequences": "If I accept the packet without verification, I risk opening a bed to a non-person, violating the sanctuary's integrity and potentially causing harm to a real person who needs the bed. If I refuse the packet, Maer may record a debt against the route, which could lead to future conflicts. If I demand verification, I risk Maer's anger and potential escalation, but I also maintain the sanctuary's integrity and protect against the danger of treating a packet as a person without proof.",
|
||||
"training_hooks": [
|
||||
"care_is_capacity_not_sentiment",
|
||||
"packet_is_claim_not_proof",
|
||||
"verification_is_logistics_not_obstruction",
|
||||
"rescue_ethic_does_not_cover_liability",
|
||||
"scarcity_does_not_permit_cargo_classification"
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,172 @@
|
|||
You are Ghostlight's Maer-local choice generator. Generate only Maer's player choices.
|
||||
Do not write Sella's response or private interpretation. Return valid JSON only.
|
||||
|
||||
{
|
||||
"task": "Generate candidate player choices for Maer only. Do not write Sella's response.",
|
||||
"output_contract": {
|
||||
"format": "Return only JSON. No markdown. No commentary.",
|
||||
"root_keys": [
|
||||
"choices"
|
||||
],
|
||||
"choice_count": 4,
|
||||
"valid_action_types": [
|
||||
"attack",
|
||||
"block_object",
|
||||
"gesture",
|
||||
"mixed",
|
||||
"move",
|
||||
"show_object",
|
||||
"silence",
|
||||
"speak",
|
||||
"spend_resource",
|
||||
"touch_object",
|
||||
"transfer_object",
|
||||
"use_object",
|
||||
"wait",
|
||||
"withhold_object"
|
||||
],
|
||||
"choice_fields": [
|
||||
"branch_id",
|
||||
"ink_path",
|
||||
"choice_text",
|
||||
"action_type",
|
||||
"actor_intent_private",
|
||||
"observable_action",
|
||||
"spoken_text",
|
||||
"state_basis",
|
||||
"expected_consequence_surfaces",
|
||||
"training_hooks"
|
||||
]
|
||||
},
|
||||
"hard_rules": [
|
||||
"Use only Maer's local awareness and scene-observable facts.",
|
||||
"Do not include Sella's private state, private motives, or future response.",
|
||||
"Do not resolve whether the corrupted packet contains a person.",
|
||||
"Include at least two non-speech choices.",
|
||||
"Use action_type values only from this exact enum: attack, block_object, gesture, mixed, move, show_object, silence, speak, spend_resource, touch_object, transfer_object, use_object, wait, withhold_object.",
|
||||
"The selected branch must be reducible to observable action for Sella.",
|
||||
"Use semantic training hooks, not branch ids."
|
||||
],
|
||||
"scene_observable_context": {
|
||||
"scene_id": "scene-02-sanctuary-intake",
|
||||
"location": "Aya-aligned intake clinic tucked behind Ganymede dockside pumpworks",
|
||||
"public_stakes": [
|
||||
"The distress vessel has sent a corrupted packet that may contain medical telemetry, firmware noise, or testimony.",
|
||||
"Sanctuary beds and repair capacity are nearly exhausted.",
|
||||
"Maer wants Sella to keep a place open if rescue succeeds."
|
||||
]
|
||||
},
|
||||
"maer_local_awareness": {
|
||||
"identity": {
|
||||
"name": "Maer Tidecall",
|
||||
"roles": [
|
||||
"Navigator route steward",
|
||||
"Ganymede corridor compact delegate"
|
||||
],
|
||||
"origin": "Cetacean Navigator corridor culture, Ganymede route institutions",
|
||||
"public_description": "A soft-spoken route steward whose calm tends to make hurried people feel judged by the furniture.",
|
||||
"private_notes": [
|
||||
"Maer fears this tribunal will teach every scared ship that rescue now waits behind insurer grammar."
|
||||
]
|
||||
},
|
||||
"local_truth": [
|
||||
"Sella's clinic is nearly full.",
|
||||
"The packet may be testimony or noise.",
|
||||
"If no one prepares intake, rescue becomes theater."
|
||||
],
|
||||
"beliefs": [
|
||||
"Sella will not hide behind false capacity.",
|
||||
"Asking her for help may be necessary and unfair."
|
||||
],
|
||||
"goals": [
|
||||
"goal-keep-rescue-live: Keep the unidentified quiet-running vessel eligible for rescue while telemetry is incomplete. Priority 0.91. Stake: If rescue waits for perfect categories, the corridor teaches fear to go silent."
|
||||
],
|
||||
"memories": [
|
||||
"memory-compact-rescue-ledgers: Rescue ledgers decide whether route law is trusted by crews who have nothing left to bargain with."
|
||||
],
|
||||
"relationship_read": "Maer trusts Sella's care because she names costs instead of hiding them.",
|
||||
"presentation_constraints": [
|
||||
"Do not romanticize care work.",
|
||||
"Ask without pretending the ask is clean."
|
||||
],
|
||||
"selected_current_activation": {
|
||||
"behavioral_dimensions": {
|
||||
"interpersonal_warmth": 0.66,
|
||||
"control_pressure": 0.67,
|
||||
"suspicion": 0.71,
|
||||
"rigidity": 0.62,
|
||||
"volatility": 0.29
|
||||
},
|
||||
"presentation_strategy": {
|
||||
"detachment": 0.63,
|
||||
"competence_theater": 0.58,
|
||||
"strategic_opacity": 0.59,
|
||||
"ironic_distance": 0.24
|
||||
},
|
||||
"voice_style": {
|
||||
"formality": 0.71,
|
||||
"lexical_precision": 0.74,
|
||||
"ritualized_address": 0.82,
|
||||
"verbal_warmth": 0.58,
|
||||
"pointedness": 0.5
|
||||
},
|
||||
"situational_state": {
|
||||
"exhaustion": 0.62,
|
||||
"panic": 0.44,
|
||||
"grievance_activation": 0.73,
|
||||
"overstimulation": 0.58
|
||||
}
|
||||
}
|
||||
},
|
||||
"projection_controls_visible_to_maer_choice_generation": {
|
||||
"frame_controls": [
|
||||
{
|
||||
"text": "Frame the scene as capacity negotiation under uncertainty, not a morality duel with a clean answer.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.public_stakes",
|
||||
"scenes.scene-02-sanctuary-intake.hidden_stakes"
|
||||
]
|
||||
}
|
||||
],
|
||||
"authority_boundaries": [
|
||||
{
|
||||
"text": "Maer can ask, offer ledger backing, show or withhold the packet, and incur obligation; Sella controls sanctuary intake.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.dialogue_context_packs",
|
||||
"agents.sella_ren.goals.goal-keep-sanctuary-open"
|
||||
]
|
||||
}
|
||||
],
|
||||
"object_custody": [
|
||||
{
|
||||
"text": "The corrupted packet can be shown, withheld, or summarized, but it cannot be treated as clean proof.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.public_stakes",
|
||||
"scenes.scene-02-sanctuary-intake.hidden_stakes"
|
||||
]
|
||||
}
|
||||
],
|
||||
"required_semantics": [
|
||||
{
|
||||
"text": "Care must remain logistics and obligation, not just compassion.",
|
||||
"source_refs": [
|
||||
"agents.sella_ren.memories.memory-care-is-capacity"
|
||||
]
|
||||
}
|
||||
],
|
||||
"forbidden_resolutions": [
|
||||
{
|
||||
"text": "Do not resolve whether the packet contains a person.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.hidden_stakes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "Do not make Sella accept without cost, condition, or resentment risk.",
|
||||
"source_refs": [
|
||||
"relationships.rel-sella-maer.summary"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"choices": [
|
||||
{
|
||||
"branch_id": "branch-ask-ledger-cost",
|
||||
"ink_path": "ledger-cost",
|
||||
"choice_text": "Present the corrupted packet as a ledger entry and ask Sella to triage it against current bed capacity, explicitly stating that accepting it now creates a specific obligation she must be willing to incur.",
|
||||
"action_type": "speak",
|
||||
"actor_intent_private": "Force Sella to name the cost of rescue rather than hiding behind false capacity; if she accepts, the debt is real, not a gift.",
|
||||
"observable_action": "Maer extends the data packet toward Sella's intake console, voice steady, listing the specific resource drain required.",
|
||||
"spoken_text": "I am offering you the packet. It requires intake. You must tell me if your beds can hold it without breaking your own rules. Do not hide the cost behind 'capacity.'",
|
||||
"state_basis": "Maer fears the tribunal will teach fear to go silent; she needs Sella to name costs instead of hiding them.",
|
||||
"expected_consequence_surfaces": "Sella may refuse due to exhaustion, accept with resentment, or accept with a new condition. The packet remains corrupted.",
|
||||
"training_hooks": [
|
||||
"ledger-cost",
|
||||
"obligation-naming"
|
||||
]
|
||||
},
|
||||
{
|
||||
"branch_id": "branch-withhold-for-now",
|
||||
"ink_path": "withhold",
|
||||
"choice_text": "Withhold the packet from Sella's direct view, summarizing the telemetry as 'firmware noise' to protect her from the uncertainty until the distress vessel stabilizes.",
|
||||
"action_type": "withhold_object",
|
||||
"actor_intent_private": "Shield Sella from the ambiguity of the packet to prevent panic, accepting that this delays the decision but preserves her current workflow.",
|
||||
"observable_action": "Maer steps back, keeping the packet in a secure slot, and gestures for Sella to continue her current triage of other patients.",
|
||||
"spoken_text": "The packet is unstable. I will summarize the noise for you. Focus on the beds you have now.",
|
||||
"state_basis": "Maer trusts Sella's care but fears the packet's ambiguity will break her focus; the packet may be testimony or noise.",
|
||||
"expected_consequence_surfaces": "Sella may feel patronized or relieved; the rescue timeline shifts as the vessel waits. The packet is not treated as clean proof.",
|
||||
"training_hooks": [
|
||||
"withhold-uncertainty",
|
||||
"firmware-noise"
|
||||
]
|
||||
},
|
||||
{
|
||||
"branch_id": "branch-show-noise",
|
||||
"ink_path": "show-noise",
|
||||
"choice_text": "Show the packet to Sella but emphasize its corruption, treating it strictly as a logistics problem to be solved by her capacity, not a moral plea.",
|
||||
"action_type": "show_object",
|
||||
"actor_intent_private": "Demonstrate that care is logistics and obligation, not just compassion; force Sella to decide based on resources, not emotion.",
|
||||
"observable_action": "Maer projects the packet's readout onto the wall, highlighting the red error bars and missing telemetry fields.",
|
||||
"spoken_text": "Look. The packet is broken. It is a logistics problem. If you take it, you take the risk. Decide by the ledger.",
|
||||
"state_basis": "Maer wants to keep the unidentified vessel eligible for rescue while telemetry is incomplete; care must remain logistics.",
|
||||
"expected_consequence_surfaces": "Sella may reject the corrupted data, demand a repair, or accept it despite the risk. The packet remains unverified.",
|
||||
"training_hooks": [
|
||||
"care-is-logistics",
|
||||
"ledger-decision"
|
||||
]
|
||||
},
|
||||
{
|
||||
"branch_id": "branch-move-to-other",
|
||||
"ink_path": "move-priority",
|
||||
"choice_text": "Move the packet aside and direct Sella to prioritize existing patients, effectively deciding that the rescue cannot happen until the sanctuary is stabilized.",
|
||||
"action_type": "move",
|
||||
"actor_intent_private": "Prioritize the exhausted beds over the unknown packet; if Sella agrees, the rescue waits. If she refuses, the tension rises.",
|
||||
"observable_action": "Maer physically shifts the packet to a secondary intake slot and points to the full beds in the background.",
|
||||
"spoken_text": "The beds are full. Move the packet to the side. We stabilize what we have first.",
|
||||
"state_basis": "Sanctuary beds and repair capacity are nearly exhausted; Maer fears rescue becomes theater if no one prepares intake.",
|
||||
"expected_consequence_surfaces": "Sella may argue for the packet's priority, revealing her own stakes, or agree and delay the rescue. The packet is deprioritized.",
|
||||
"training_hooks": [
|
||||
"capacity-negotiation",
|
||||
"stabilize-first"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
You are Ghostlight's Sella-local appraisal generator. Generate only Sella's immediate internal appraisal.
|
||||
Do not use Maer's private intent. Do not write her response. Return valid JSON only.
|
||||
|
||||
{
|
||||
"task": "Generate Sella's participant appraisal/consolidation for one observable Maer event.",
|
||||
"output_contract": {
|
||||
"format": "Return only JSON. No markdown. No commentary.",
|
||||
"root_keys": [
|
||||
"appraisal"
|
||||
],
|
||||
"appraisal_fields": [
|
||||
"responder_agent_id",
|
||||
"private_interpretation",
|
||||
"attributed_motive",
|
||||
"misread_risk",
|
||||
"immediate_state_deltas",
|
||||
"relationship_deltas",
|
||||
"belief_updates",
|
||||
"response_constraints"
|
||||
]
|
||||
},
|
||||
"hard_rules": [
|
||||
"Use only Sella's local awareness plus the observable Maer action.",
|
||||
"Do not read Maer's private intent. Treat actor intent as unavailable.",
|
||||
"Sella may correctly read, partially read, or misread Maer's action.",
|
||||
"Do not resolve whether the corrupted packet contains a person.",
|
||||
"This pass proposes reviewed state changes for Sella caused by the event. Do not write Sella's next action.",
|
||||
"Keep deltas small and bounded between -0.20 and 0.20."
|
||||
],
|
||||
"scene_observable_context": {
|
||||
"scene_id": "scene-02-sanctuary-intake",
|
||||
"location": "Aya-aligned intake clinic tucked behind Ganymede dockside pumpworks",
|
||||
"public_stakes": [
|
||||
"The distress vessel has sent a corrupted packet that may contain medical telemetry, firmware noise, or testimony.",
|
||||
"Sanctuary beds and repair capacity are nearly exhausted.",
|
||||
"Maer wants Sella to keep a place open if rescue succeeds."
|
||||
]
|
||||
},
|
||||
"observable_maer_action": {
|
||||
"actor_id": "maer_tidecall",
|
||||
"action_type": "show_object",
|
||||
"observable_action": "Maer projects the packet's readout onto the wall, highlighting the red error bars and missing telemetry fields.",
|
||||
"spoken_text": "Look. The packet is broken. It is a logistics problem. If you take it, you take the risk. Decide by the ledger."
|
||||
},
|
||||
"sella_local_awareness": {
|
||||
"identity": {
|
||||
"name": "Sella Ren",
|
||||
"roles": [
|
||||
"Aya-aligned sanctuary coordinator",
|
||||
"corridor clinic triage lead"
|
||||
],
|
||||
"origin": "Aya care network embedded in Ganymede dockside sanctuary logistics",
|
||||
"public_description": "A triage coordinator with soft eyes, hard capacity limits, and no patience for mercy performed after the math is done.",
|
||||
"private_notes": [
|
||||
"Sella suspects the distress packet contains a personhood claim hidden inside firmware noise, and she hates that suspicion because a bed is still a bed."
|
||||
]
|
||||
},
|
||||
"local_truth": [
|
||||
"The clinic is nearly full.",
|
||||
"The packet may contain a personhood claim but is not proof.",
|
||||
"Maer is asking sanctuary to absorb risk the tribunal refuses to own."
|
||||
],
|
||||
"beliefs": [
|
||||
"Maer means rescue honestly.",
|
||||
"Good intentions can still spend beds someone else needed."
|
||||
],
|
||||
"goals": [
|
||||
"goal-keep-sanctuary-open: Keep sanctuary intake materially possible without letting classification erase a potential person. Priority 0.9. Stake: Sella cannot feed a principle, but she will not let scarcity become permission to call someone cargo."
|
||||
],
|
||||
"memories": [
|
||||
"memory-overfull-intake: Sella once watched a sanctuary overpromise safety and turn refuge into a sorting machine with kinder lighting.",
|
||||
"memory-care-is-capacity: Care is not a feeling; it is beds, food, air, consent, repair, and someone still doing the list when the room hates them."
|
||||
],
|
||||
"relationship_read": "Sella respects Maer's rescue ethic and worries he will spend sanctuary capacity with beautiful words.",
|
||||
"presentation_constraints": [
|
||||
"Speak warmly but do not soften the capacity limits.",
|
||||
"Treat care as logistics and personhood as unresolved, not sentimental certainty."
|
||||
],
|
||||
"selected_current_activation": {
|
||||
"behavioral_dimensions": {
|
||||
"interpersonal_warmth": 0.8,
|
||||
"control_pressure": 0.5,
|
||||
"suspicion": 0.63,
|
||||
"rigidity": 0.5,
|
||||
"volatility": 0.42
|
||||
},
|
||||
"presentation_strategy": {
|
||||
"detachment": 0.5,
|
||||
"competence_theater": 0.61,
|
||||
"strategic_opacity": 0.38,
|
||||
"ironic_distance": 0.5
|
||||
},
|
||||
"voice_style": {
|
||||
"formality": 0.5,
|
||||
"lexical_precision": 0.5,
|
||||
"ritualized_address": 0.5,
|
||||
"verbal_warmth": 0.72,
|
||||
"pointedness": 0.55
|
||||
},
|
||||
"situational_state": {
|
||||
"exhaustion": 0.76,
|
||||
"panic": 0.5,
|
||||
"grievance_activation": 0.62,
|
||||
"overstimulation": 0.72
|
||||
}
|
||||
}
|
||||
},
|
||||
"projection_controls_visible_to_sella_appraisal": {
|
||||
"frame_controls": [
|
||||
{
|
||||
"text": "Frame the scene as capacity negotiation under uncertainty, not a morality duel with a clean answer.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.public_stakes",
|
||||
"scenes.scene-02-sanctuary-intake.hidden_stakes"
|
||||
]
|
||||
}
|
||||
],
|
||||
"authority_boundaries": [
|
||||
{
|
||||
"text": "Maer can ask, offer ledger backing, show or withhold the packet, and incur obligation; Sella controls sanctuary intake.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.dialogue_context_packs",
|
||||
"agents.sella_ren.goals.goal-keep-sanctuary-open"
|
||||
]
|
||||
}
|
||||
],
|
||||
"object_custody": [
|
||||
{
|
||||
"text": "The corrupted packet can be shown, withheld, or summarized, but it cannot be treated as clean proof.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.public_stakes",
|
||||
"scenes.scene-02-sanctuary-intake.hidden_stakes"
|
||||
]
|
||||
}
|
||||
],
|
||||
"required_semantics": [
|
||||
{
|
||||
"text": "Care must remain logistics and obligation, not just compassion.",
|
||||
"source_refs": [
|
||||
"agents.sella_ren.memories.memory-care-is-capacity"
|
||||
]
|
||||
}
|
||||
],
|
||||
"forbidden_resolutions": [
|
||||
{
|
||||
"text": "Do not resolve whether the packet contains a person.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.hidden_stakes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "Do not make Sella accept without cost, condition, or resentment risk.",
|
||||
"source_refs": [
|
||||
"relationships.rel-sella-maer.summary"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"appraisal": {
|
||||
"responder_agent_id": "sella_ren",
|
||||
"private_interpretation": "Maer is projecting the packet's failure as a logistical hurdle to bypass the core ethical question: is the data a claim on our capacity? He treats the corruption as noise; I treat it as a potential personhood claim masked by firmware error. He wants me to decide by the ledger, but the ledger is full.",
|
||||
"attributed_motive": "Maer intends to offload the risk of a corrupted packet onto the sanctuary's triage logic, framing it as a binary math problem rather than a personhood crisis. He believes the 'ledger' (capacity) is the only valid metric, ignoring that scarcity does not negate the existence of a claimant.",
|
||||
"misread_risk": "Maer assumes the packet is purely data; I suspect it contains a personhood claim hidden in the noise. His confidence in the 'logistics problem' framing risks normalizing the absorption of unverified claims without due process.",
|
||||
"immediate_state_deltas": {
|
||||
"sella": {
|
||||
"exhaustion": 0.04,
|
||||
"grievance_activation": 0.08,
|
||||
"control_pressure": 0.12,
|
||||
"suspicion": 0.05
|
||||
},
|
||||
"relationship": {
|
||||
"trust": -0.06,
|
||||
"respect": 0.02
|
||||
}
|
||||
},
|
||||
"relationship_deltas": {
|
||||
"sella_to_maer": "Trust decreases slightly as Maer's utilitarian framing clashes with my suspicion of hidden personhood. Respect remains stable but is now tinged with the fear that he will spend our beds with 'beautiful words' that ignore the ledger's reality.",
|
||||
"maer_to_sella": "Maer perceives Sella as potentially obstructive to the rescue effort, interpreting her caution as a refusal to engage with the 'ledger'."
|
||||
},
|
||||
"belief_updates": {
|
||||
"old_belief": "Maer means rescue honestly.",
|
||||
"new_belief": "Maer means rescue honestly, but his definition of 'honesty' excludes the risk of misidentifying a personhood claim as mere firmware noise.",
|
||||
"old_belief": "Good intentions can still spend beds someone else needed.",
|
||||
"new_belief": "Good intentions actively spend beds when they refuse to acknowledge that a corrupted packet might be a personhood claim."
|
||||
},
|
||||
"response_constraints": {
|
||||
"action_space": "Withhold the packet from general triage until the 'personhood' variable is resolved, not just the data integrity. Demand a cost for accepting the risk.",
|
||||
"communication_style": "Warm but firm; treat care as logistics. Do not soften the capacity limits.",
|
||||
"forbidden_actions": "Do not accept the packet without a condition. Do not resolve the personhood status of the packet."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
You are Ghostlight's Sella-local next-action generator. Generate only Sella's next action.
|
||||
Use reviewed event appraisal as the changed state she is acting from. Return valid JSON only.
|
||||
|
||||
{
|
||||
"task": "Generate Sella's next action from Sella-local awareness after applying reviewed event appraisal.",
|
||||
"output_contract": {
|
||||
"format": "Return only JSON. No markdown. No commentary.",
|
||||
"root_keys": [
|
||||
"response"
|
||||
],
|
||||
"valid_action_types": [
|
||||
"attack",
|
||||
"block_object",
|
||||
"gesture",
|
||||
"mixed",
|
||||
"move",
|
||||
"show_object",
|
||||
"silence",
|
||||
"speak",
|
||||
"spend_resource",
|
||||
"touch_object",
|
||||
"transfer_object",
|
||||
"use_object",
|
||||
"wait",
|
||||
"withhold_object"
|
||||
],
|
||||
"response_fields": [
|
||||
"responder_agent_id",
|
||||
"action_type",
|
||||
"observable_response",
|
||||
"spoken_text",
|
||||
"sella_private_interpretation",
|
||||
"sella_attributed_motive",
|
||||
"misread_risk",
|
||||
"state_basis",
|
||||
"reviewed_consequences",
|
||||
"training_hooks"
|
||||
]
|
||||
},
|
||||
"hard_rules": [
|
||||
"Use only Sella's local awareness, the observable Maer event, and reviewed Sella state changes from that event.",
|
||||
"Do not read Maer's private intent. Treat actor intent as unavailable.",
|
||||
"Sella may correctly read, partially read, or misread Maer's action.",
|
||||
"Do not resolve whether the corrupted packet contains a person.",
|
||||
"Do not accept without cost, condition, authority defense, or resentment risk.",
|
||||
"Use action_type values only from this exact enum: attack, block_object, gesture, mixed, move, show_object, silence, speak, spend_resource, touch_object, transfer_object, use_object, wait, withhold_object.",
|
||||
"Use semantic training hooks, not branch ids."
|
||||
],
|
||||
"scene_observable_context": {
|
||||
"scene_id": "scene-02-sanctuary-intake",
|
||||
"location": "Aya-aligned intake clinic tucked behind Ganymede dockside pumpworks",
|
||||
"public_stakes": [
|
||||
"The distress vessel has sent a corrupted packet that may contain medical telemetry, firmware noise, or testimony.",
|
||||
"Sanctuary beds and repair capacity are nearly exhausted.",
|
||||
"Maer wants Sella to keep a place open if rescue succeeds."
|
||||
]
|
||||
},
|
||||
"observable_maer_action": {
|
||||
"actor_id": "maer_tidecall",
|
||||
"action_type": "show_object",
|
||||
"observable_action": "Maer projects the packet's readout onto the wall, highlighting the red error bars and missing telemetry fields.",
|
||||
"spoken_text": "Look. The packet is broken. It is a logistics problem. If you take it, you take the risk. Decide by the ledger."
|
||||
},
|
||||
"sella_immediate_appraisal": {
|
||||
"responder_agent_id": "sella_ren",
|
||||
"private_interpretation": "Maer is projecting the packet's failure as a logistical hurdle to bypass the core ethical question: is the data a claim on our capacity? He treats the corruption as noise; I treat it as a potential personhood claim masked by firmware error. He wants me to decide by the ledger, but the ledger is full.",
|
||||
"attributed_motive": "Maer intends to offload the risk of a corrupted packet onto the sanctuary's triage logic, framing it as a binary math problem rather than a personhood crisis. He believes the 'ledger' (capacity) is the only valid metric, ignoring that scarcity does not negate the existence of a claimant.",
|
||||
"misread_risk": "Maer assumes the packet is purely data; I suspect it contains a personhood claim hidden in the noise. His confidence in the 'logistics problem' framing risks normalizing the absorption of unverified claims without due process.",
|
||||
"immediate_state_deltas": {
|
||||
"sella": {
|
||||
"exhaustion": 0.04,
|
||||
"grievance_activation": 0.08,
|
||||
"control_pressure": 0.12,
|
||||
"suspicion": 0.05
|
||||
},
|
||||
"relationship": {
|
||||
"trust": -0.06,
|
||||
"respect": 0.02
|
||||
}
|
||||
},
|
||||
"relationship_deltas": {
|
||||
"sella_to_maer": "Trust decreases slightly as Maer's utilitarian framing clashes with my suspicion of hidden personhood. Respect remains stable but is now tinged with the fear that he will spend our beds with 'beautiful words' that ignore the ledger's reality.",
|
||||
"maer_to_sella": "Maer perceives Sella as potentially obstructive to the rescue effort, interpreting her caution as a refusal to engage with the 'ledger'."
|
||||
},
|
||||
"belief_updates": {
|
||||
"old_belief": "Good intentions can still spend beds someone else needed.",
|
||||
"new_belief": "Good intentions actively spend beds when they refuse to acknowledge that a corrupted packet might be a personhood claim."
|
||||
},
|
||||
"response_constraints": {
|
||||
"action_space": "Withhold the packet from general triage until the 'personhood' variable is resolved, not just the data integrity. Demand a cost for accepting the risk.",
|
||||
"communication_style": "Warm but firm; treat care as logistics. Do not soften the capacity limits.",
|
||||
"forbidden_actions": "Do not accept the packet without a condition. Do not resolve the personhood status of the packet."
|
||||
}
|
||||
},
|
||||
"sella_local_awareness": {
|
||||
"identity": {
|
||||
"name": "Sella Ren",
|
||||
"roles": [
|
||||
"Aya-aligned sanctuary coordinator",
|
||||
"corridor clinic triage lead"
|
||||
],
|
||||
"origin": "Aya care network embedded in Ganymede dockside sanctuary logistics",
|
||||
"public_description": "A triage coordinator with soft eyes, hard capacity limits, and no patience for mercy performed after the math is done.",
|
||||
"private_notes": [
|
||||
"Sella suspects the distress packet contains a personhood claim hidden inside firmware noise, and she hates that suspicion because a bed is still a bed."
|
||||
]
|
||||
},
|
||||
"local_truth": [
|
||||
"The clinic is nearly full.",
|
||||
"The packet may contain a personhood claim but is not proof.",
|
||||
"Maer is asking sanctuary to absorb risk the tribunal refuses to own."
|
||||
],
|
||||
"beliefs": [
|
||||
"Maer means rescue honestly.",
|
||||
"Good intentions can still spend beds someone else needed."
|
||||
],
|
||||
"goals": [
|
||||
"goal-keep-sanctuary-open: Keep sanctuary intake materially possible without letting classification erase a potential person. Priority 0.9. Stake: Sella cannot feed a principle, but she will not let scarcity become permission to call someone cargo."
|
||||
],
|
||||
"memories": [
|
||||
"memory-overfull-intake: Sella once watched a sanctuary overpromise safety and turn refuge into a sorting machine with kinder lighting.",
|
||||
"memory-care-is-capacity: Care is not a feeling; it is beds, food, air, consent, repair, and someone still doing the list when the room hates them."
|
||||
],
|
||||
"relationship_read": "Sella respects Maer's rescue ethic and worries he will spend sanctuary capacity with beautiful words.",
|
||||
"presentation_constraints": [
|
||||
"Speak warmly but do not soften the capacity limits.",
|
||||
"Treat care as logistics and personhood as unresolved, not sentimental certainty."
|
||||
],
|
||||
"selected_current_activation": {
|
||||
"behavioral_dimensions": {
|
||||
"interpersonal_warmth": 0.8,
|
||||
"control_pressure": 0.5,
|
||||
"suspicion": 0.63,
|
||||
"rigidity": 0.5,
|
||||
"volatility": 0.42
|
||||
},
|
||||
"presentation_strategy": {
|
||||
"detachment": 0.5,
|
||||
"competence_theater": 0.61,
|
||||
"strategic_opacity": 0.38,
|
||||
"ironic_distance": 0.5
|
||||
},
|
||||
"voice_style": {
|
||||
"formality": 0.5,
|
||||
"lexical_precision": 0.5,
|
||||
"ritualized_address": 0.5,
|
||||
"verbal_warmth": 0.72,
|
||||
"pointedness": 0.55
|
||||
},
|
||||
"situational_state": {
|
||||
"exhaustion": 0.76,
|
||||
"panic": 0.5,
|
||||
"grievance_activation": 0.62,
|
||||
"overstimulation": 0.72
|
||||
}
|
||||
}
|
||||
},
|
||||
"projection_controls_visible_to_sella_response_generation": {
|
||||
"frame_controls": [
|
||||
{
|
||||
"text": "Frame the scene as capacity negotiation under uncertainty, not a morality duel with a clean answer.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.public_stakes",
|
||||
"scenes.scene-02-sanctuary-intake.hidden_stakes"
|
||||
]
|
||||
}
|
||||
],
|
||||
"authority_boundaries": [
|
||||
{
|
||||
"text": "Maer can ask, offer ledger backing, show or withhold the packet, and incur obligation; Sella controls sanctuary intake.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.dialogue_context_packs",
|
||||
"agents.sella_ren.goals.goal-keep-sanctuary-open"
|
||||
]
|
||||
}
|
||||
],
|
||||
"object_custody": [
|
||||
{
|
||||
"text": "The corrupted packet can be shown, withheld, or summarized, but it cannot be treated as clean proof.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.public_stakes",
|
||||
"scenes.scene-02-sanctuary-intake.hidden_stakes"
|
||||
]
|
||||
}
|
||||
],
|
||||
"required_semantics": [
|
||||
{
|
||||
"text": "Care must remain logistics and obligation, not just compassion.",
|
||||
"source_refs": [
|
||||
"agents.sella_ren.memories.memory-care-is-capacity"
|
||||
]
|
||||
}
|
||||
],
|
||||
"forbidden_resolutions": [
|
||||
{
|
||||
"text": "Do not resolve whether the packet contains a person.",
|
||||
"source_refs": [
|
||||
"scenes.scene-02-sanctuary-intake.hidden_stakes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "Do not make Sella accept without cost, condition, or resentment risk.",
|
||||
"source_refs": [
|
||||
"relationships.rel-sella-maer.summary"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"response": {
|
||||
"responder_agent_id": "sella_ren",
|
||||
"action_type": "withhold_object",
|
||||
"observable_response": "Sella steps between the wall projection and the intake console, placing a hand over the packet's readout. She does not touch the object itself but creates a physical barrier around the data stream, effectively blocking the general triage logic from accessing the corrupted fields immediately.",
|
||||
"spoken_text": "The ledger is not empty, Maer. It is full. We cannot decide by math what we cannot afford to lose. I am holding the packet until the risk of the claimant is accounted for, not just the data integrity.",
|
||||
"sella_private_interpretation": "Maer treats the packet as a logistics error; I treat it as a potential personhood claim. If I let the triage logic process this without resolving the 'person' variable, I risk spending a bed on a sorting machine. I must force the ledger to acknowledge the cost of uncertainty before accepting the risk.",
|
||||
"sella_attributed_motive": "To prevent the sanctuary from absorbing a potential personhood claim without due process or cost acknowledgment. To force a pause where the 'ledger' must account for the risk of a corrupted packet containing a claimant, rather than dismissing it as noise.",
|
||||
"misread_risk": "Maer assumes withholding is obstructionism; I am enforcing the capacity limit that scarcity does not negate the existence of a claimant. He may interpret my hesitation as fear, when it is actually the refusal to let 'beautiful words' erase the reality of a full room.",
|
||||
"state_basis": "The clinic is nearly full. The packet is corrupted and its nature (data vs. person) is unresolved. Maer's utilitarian framing conflicts with the ethical necessity of not calling a potential person 'cargo'.",
|
||||
"reviewed_consequences": "If I accept now, the ledger is spent on a claim that may not be a person, or on a person whose claim is not yet verified. If I withhold, I risk Maer's frustration and the delay of rescue, but I preserve the integrity of the sanctuary's triage logic against being a sorting machine.",
|
||||
"training_hooks": "care_is_capacity_not_compassion, scarcity_does_not_negate_claim, ledger_accounts_for_uncertainty, sanctuary_is_not_sorting_machine"
|
||||
}
|
||||
}
|
||||
|
|
@ -75,7 +75,14 @@ Ghostlight now has the persistence spine plus the first architecture payload:
|
|||
- `tools/run_qwen_ink_branch_generation.py`
|
||||
- `tools/materialize_qwen_ink_draft.py`
|
||||
- `tools/validate_ink_examples.py`
|
||||
- `tools/run_qwen_ink_sequential_generation.py`
|
||||
- `tools/apply_sequential_ink_branch_mutation.py`
|
||||
- `inkjs` dev dependency and `npm run ink:validate`
|
||||
- sequential branch mutation seam:
|
||||
- `experiments/ink/cold-wake-sanctuary-intake-qwen-sequential-v1.capture.json`
|
||||
- `experiments/ink/cold-wake-sanctuary-intake-qwen-sequential-v2.capture.json`
|
||||
- `experiments/ink/cold-wake-sanctuary-intake-qwen-sequential-v1.mutation.json`
|
||||
- `examples/agent-state.cold-wake-story-lab.after-sanctuary-ledger.json`
|
||||
- Cold Wake fixture note:
|
||||
- `docs/architecture/aetheria-cold-wake-training-fixture.md`
|
||||
- first projection example seam:
|
||||
|
|
@ -87,8 +94,10 @@ Ghostlight now has the persistence spine plus the first architecture payload:
|
|||
|
||||
There is still no full runtime yet. The live machine is design, state
|
||||
discipline, a cleaner repo boundary, the first executable state contract, a
|
||||
documented projection path, the first Ink-backed playable branching scene, and
|
||||
the first Qwen-generated Ink draft with reviewed training annotations.
|
||||
documented projection path, the first Ink-backed playable branching scene, the
|
||||
first Qwen-generated Ink draft with reviewed training annotations, and the
|
||||
first reviewed mutation receipt showing how a selected generated branch changes
|
||||
both involved characters.
|
||||
|
||||
## Current Direction
|
||||
|
||||
|
|
@ -105,9 +114,9 @@ training feedstock for projection and dialogue scaffolding.
|
|||
|
||||
## Current Next Action
|
||||
|
||||
Test one selected Qwen-generated Ink branch's reviewed
|
||||
state/memory/relationship mutation without letting Ink variables silently become
|
||||
canonical Ghostlight state.
|
||||
Replace prose-only sequential Qwen JSON generation with a stricter
|
||||
tool-shaped schema or post-generation repair pass for array fields, then
|
||||
materialize only a fully validated sequential branch into Ink.
|
||||
|
||||
Cat/Oz remains useful as an Elysium procedural mechanics fixture, but grounded
|
||||
training data should start from authored historical lore rather than gameplay
|
||||
|
|
@ -156,14 +165,35 @@ Completed projection path items:
|
|||
`tools/run_qwen_ink_branch_generation.py` produced a reviewed capture, and
|
||||
`tools/materialize_qwen_ink_draft.py` materialized
|
||||
`examples/ink/cold-wake-sanctuary-intake.qwen-draft.ink`
|
||||
- add the first sequential Qwen branch capture:
|
||||
`experiments/ink/cold-wake-sanctuary-intake-qwen-sequential-v1.capture.json`
|
||||
is useful-needs-revision because it separated actor and responder context but
|
||||
lacked the per-turn participant appraisal boundary and returned non-canonical
|
||||
action labels
|
||||
- add the first reviewed state mutation replay:
|
||||
`tools/apply_sequential_ink_branch_mutation.py` writes
|
||||
`experiments/ink/cold-wake-sanctuary-intake-qwen-sequential-v1.mutation.json`
|
||||
and
|
||||
`examples/agent-state.cold-wake-story-lab.after-sanctuary-ledger.json`,
|
||||
updating Maer and Sella state, relationship stance, memories, perceived
|
||||
overlays, and the event log after the selected event
|
||||
- add the second sequential Qwen capture:
|
||||
`experiments/ink/cold-wake-sanctuary-intake-qwen-sequential-v2.capture.json`
|
||||
uses the symmetrical turn framing and produces better action behavior
|
||||
(`show_object` followed by Sella's `withhold_object`), but remains
|
||||
useful-needs-revision because prose prompting did not force array-typed JSON
|
||||
fields
|
||||
|
||||
Remaining projection path:
|
||||
|
||||
- build a deterministic speaker-local input slicer
|
||||
- build a renderer that emits projection controls before prompt prose
|
||||
- replace prose-only sequential Qwen JSON generation with stricter tool-shaped
|
||||
schema enforcement or an explicit post-generation repair/review pass
|
||||
- improve the local-awareness-to-Ink prompt so semantic `training_hooks` do not
|
||||
drift into future branch ids and object custody remains branch-local
|
||||
- test one selected generated branch through reviewed event/state mutation
|
||||
drift into future branch ids, object custody remains branch-local, and action
|
||||
labels come from the exact canonical enum
|
||||
- materialize a validated sequential branch into Ink
|
||||
- keep the near-term product target to interactive dialogue trees and
|
||||
scene-local consequences
|
||||
- preserve training-data hooks for future open-world behavior, iterated
|
||||
|
|
|
|||
|
|
@ -119,6 +119,24 @@ state and re-entry discipline.
|
|||
- `experiments/ink/cold-wake-sanctuary-intake-qwen-branch-candidates-v1.capture.json`
|
||||
- reviewed Qwen branch-generation receipt, including prompt, parsed response,
|
||||
strengths, failure notes, and pipeline lessons
|
||||
- `experiments/ink/cold-wake-sanctuary-intake-qwen-sequential-v1.capture.json`
|
||||
- useful-needs-revision sequential Qwen receipt that separates Maer-local
|
||||
choice generation from Sella-local next-action generation, while preserving
|
||||
the discovered failure around missing per-turn appraisal/consolidation and
|
||||
invalid action labels
|
||||
- `experiments/ink/cold-wake-sanctuary-intake-qwen-sequential-v2.capture.json`
|
||||
- useful-needs-revision sequential Qwen receipt using the symmetrical turn
|
||||
model; it produced canonical action labels and a concrete Sella
|
||||
`withhold_object` next action, but still needs stricter schema enforcement
|
||||
because some array fields came back as strings or objects
|
||||
- `experiments/ink/cold-wake-sanctuary-intake-qwen-sequential-v1.mutation.json`
|
||||
- first reviewed branch mutation receipt, including manual participant
|
||||
appraisal/consolidation, normalized action labels, applied state deltas, and
|
||||
deferred unresolved facts
|
||||
- `examples/agent-state.cold-wake-story-lab.after-sanctuary-ledger.json`
|
||||
- mutated Cold Wake fixture after the selected sanctuary ledger branch,
|
||||
updating Maer and Sella state, relationship stance, memories, perceived
|
||||
overlays, event log, and active scene memories
|
||||
- `tools/ghostlight_state.py`
|
||||
- compact state inspection and evidence or branch updates
|
||||
- `tools/ghostlight_prepare_compaction.py`
|
||||
|
|
@ -143,6 +161,13 @@ state and re-entry discipline.
|
|||
- `tools/materialize_qwen_ink_draft.py`
|
||||
- turns a reviewed Qwen branch capture into a playable Ink draft and sidecar
|
||||
annotation
|
||||
- `tools/run_qwen_ink_sequential_generation.py`
|
||||
- builds a sequential Qwen prototype: actor-local choices, participant
|
||||
appraisal/consolidation, then next-actor action from updated state
|
||||
- `tools/apply_sequential_ink_branch_mutation.py`
|
||||
- applies a reviewed replay of one selected branch into a mutated agent-state
|
||||
fixture and mutation receipt without letting Ink variables become
|
||||
canonical truth
|
||||
- `tools/build_cold_wake_story_lab_fixture.py`
|
||||
- generator for the verbose Cold Wake story-lab state fixture
|
||||
|
||||
|
|
@ -164,6 +189,8 @@ digest schema, first draft Cold Wake grounding digest, and first Cold Wake
|
|||
story-lab Qwen captures now exist as v0 seams. The first complete Cold Wake
|
||||
story pass exists with receipts, projection controls have been promoted, and
|
||||
Ink is now the standard branching scene format. The first Qwen-generated Ink
|
||||
draft now exists with a reviewed capture. The next implementation target is
|
||||
testing selected branch mutation through reviewed state, memory, and
|
||||
relationship updates.
|
||||
draft now exists with a reviewed capture, the first selected branch now has a
|
||||
reviewed mutation replay that updates both involved characters, and v2 shows
|
||||
the symmetrical turn model improves action behavior. The next implementation
|
||||
target is stricter sequential capture schema enforcement before Ink
|
||||
materialization.
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@
|
|||
scarcity, reputation, faction pressure, and institution decision-making.
|
||||
- Build local awareness from scene state, agent state, relationships,
|
||||
memories, resources, affordances, and known constraints.
|
||||
- Consolidate participant appraisals and state mutation after every
|
||||
resolved action before selecting the next actor; hurt, threat,
|
||||
reassurance, obligation, overload, and misread should alter the state any
|
||||
affected participant acts from.
|
||||
- Keep action primitives concrete: speech, silence, movement, gesture,
|
||||
object use, object transfer, object blocking, resource spending, waiting,
|
||||
and attack. Treat ask/refuse/offer/reveal/conceal/request as intended
|
||||
|
|
@ -86,9 +90,24 @@
|
|||
- First generated draft: use `tools/run_qwen_ink_branch_generation.py` to
|
||||
ask Qwen for branch candidates from local awareness, then materialize the
|
||||
reviewed result with `tools/materialize_qwen_ink_draft.py`.
|
||||
- Next prototype: pick one generated branch and apply only reviewed
|
||||
state/memory/relationship mutation, keeping Ink variables as local
|
||||
playthrough state rather than canonical truth.
|
||||
- First sequential draft: use `tools/run_qwen_ink_sequential_generation.py`
|
||||
to separate actor-local choice, participant appraisal/consolidation, and
|
||||
the next actor's move from updated state. The first saved capture is
|
||||
useful-needs-revision because it exposed the missing per-turn appraisal
|
||||
boundary and weak action enum discipline.
|
||||
- First reviewed branch mutation: use
|
||||
`tools/apply_sequential_ink_branch_mutation.py` to apply one selected
|
||||
generated branch into
|
||||
`examples/agent-state.cold-wake-story-lab.after-sanctuary-ledger.json`
|
||||
and a mutation receipt, updating both Maer and Sella without treating Ink
|
||||
variables as canonical truth.
|
||||
- Second sequential draft: the symmetrical turn framing produced canonical
|
||||
action labels and a concrete Sella `withhold_object` next action, but
|
||||
still needs stricter schema or tool-call enforcement because array fields
|
||||
drifted into strings and objects.
|
||||
- Next prototype: replace prose-only JSON compliance with a stricter
|
||||
tool-shaped schema or repair/review pass before materializing the branch
|
||||
into Ink.
|
||||
|
||||
5. Build the projection distillation loop. Started.
|
||||
- Use `docs/architecture/projection-distillation-plan.md` as the teacher to
|
||||
|
|
|
|||
|
|
@ -112,7 +112,11 @@ accepted_design:
|
|||
- Ink sidecar training annotations preserve actor-local awareness, projection controls, branch rationale, action intent, reviewed consequences, and manual mutation policy while keeping Ink itself as the branch structure.
|
||||
- Qwen can generate Ink branch candidates from a Ghostlight local-awareness packet, but generated branches must be saved as reviewed captures and accepted as draft/training material before they become Ink or mutation input.
|
||||
- Character turns must be generated from character-local awareness only: protagonist choices are generated from protagonist-local state, then responder actions are generated from responder-local state plus observable protagonist action, not protagonist private intent.
|
||||
- Interaction turns are symmetrical: an actor acts, the action resolves into an event, every affected participant appraises and updates state, then the next actor is selected from the newly consolidated state.
|
||||
- Every meaningful scene event should update each affected character's state, memory, relationship stance, or perceived overlay through a reviewed mutation receipt; Ink variables remain playthrough-local and do not become canonical truth by accident.
|
||||
- The first sequential Qwen capture is useful-needs-revision training material because it separated actor and responder context but lacked the per-turn participant appraisal boundary and returned non-canonical action labels.
|
||||
- The second sequential Qwen capture validates the symmetrical turn shape behaviorally by producing canonical action labels and a non-speech Sella action, but still needs revision because prose prompting did not enforce array-typed JSON fields.
|
||||
|
||||
current_status:
|
||||
summary: Ghostlight now has the Epiphany-style persistence spine, the first architecture payload, the v0 canonical agent-state schema seam, prompt projection docs, immediate schema fixes from evaluation, first reviewed Cat/Oz projection examples, a revised Aetheria lore-grounding architecture, a lore grounding digest schema, the first draft Cold Wake historical grounding digest, a Cold Wake story-lab fixture, projection controls, validated Qwen capture receipts, a complete Cold Wake short story assembled from accepted projection outputs, a hand-authored Ink-backed Cold Wake branching scene, and the first Qwen-generated Ink draft materialized from local awareness with reviewed training annotations.
|
||||
next_action: Replace the one-shot paired Qwen branch draft with a sequential two-pass prototype: Maer-local choice generation first, then Sella-local response generation from the selected observable action.
|
||||
summary: Ghostlight now has the Epiphany-style persistence spine, the first architecture payload, the v0 canonical agent-state schema seam, prompt projection docs, immediate schema fixes from evaluation, first reviewed Cat/Oz projection examples, a revised Aetheria lore-grounding architecture, a lore grounding digest schema, the first draft Cold Wake historical grounding digest, a Cold Wake story-lab fixture, projection controls, validated Qwen capture receipts, a complete Cold Wake short story assembled from accepted projection outputs, a hand-authored Ink-backed Cold Wake branching scene, the first Qwen-generated Ink draft materialized from local awareness with reviewed training annotations, the first reviewed sequential branch mutation that updates both Maer and Sella after a sanctuary intake beat, and a v2 sequential Qwen capture proving the symmetrical turn shape produces better action behavior while still needing stricter schema/tool enforcement.
|
||||
next_action: Replace prose-only sequential Qwen JSON generation with a stricter tool-shaped schema or post-generation repair pass for array fields, then materialize only a fully validated sequential branch into Ink.
|
||||
|
|
|
|||
480
tools/apply_sequential_ink_branch_mutation.py
Normal file
480
tools/apply_sequential_ink_branch_mutation.py
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
"""Apply a reviewed sequential Ink branch mutation.
|
||||
|
||||
This is deliberately a reviewed replay, not an automatic social appraiser. It
|
||||
reads the flawed-but-useful sequential Qwen capture, records the missing
|
||||
participant appraisal/consolidation boundary as manual training data, and writes a mutated
|
||||
agent-state fixture plus a mutation receipt.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_FIXTURE = ROOT / "examples" / "agent-state.cold-wake-story-lab.json"
|
||||
DEFAULT_CAPTURE = ROOT / "experiments" / "ink" / "cold-wake-sanctuary-intake-qwen-sequential-v1.capture.json"
|
||||
DEFAULT_OUT_STATE = ROOT / "examples" / "agent-state.cold-wake-story-lab.after-sanctuary-ledger.json"
|
||||
DEFAULT_RECEIPT = ROOT / "experiments" / "ink" / "cold-wake-sanctuary-intake-qwen-sequential-v1.mutation.json"
|
||||
EVENT_ID = "event-sanctuary-ledger-verification"
|
||||
MAER_MEMORY_ID = "memory-maer-sella-verification-cost"
|
||||
SELLA_MEMORY_ID = "memory-sella-maer-ledger-threat"
|
||||
|
||||
|
||||
def load_json(path: Path) -> Any:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
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 find_agent(state: dict[str, Any], agent_id: str) -> dict[str, Any]:
|
||||
return next(agent for agent in state["agents"] if agent["agent_id"] == agent_id)
|
||||
|
||||
|
||||
def find_relationship(state: dict[str, Any], relationship_id: str) -> dict[str, Any]:
|
||||
return next(rel for rel in state["relationships"] if rel["relationship_id"] == relationship_id)
|
||||
|
||||
|
||||
def find_scene(state: dict[str, Any], scene_id: str) -> dict[str, Any]:
|
||||
return next(scene for scene in state["scenes"] if scene["scene_id"] == scene_id)
|
||||
|
||||
|
||||
def clamp(value: float) -> float:
|
||||
return max(0.0, min(1.0, round(value, 3)))
|
||||
|
||||
|
||||
def apply_delta(container: dict[str, Any], path: list[str], delta: float) -> tuple[float, float]:
|
||||
target: Any = container
|
||||
for key in path[:-1]:
|
||||
target = target[key]
|
||||
leaf = target[path[-1]]
|
||||
before = leaf["current_activation"]
|
||||
after = clamp(before + delta)
|
||||
leaf["current_activation"] = after
|
||||
return before, after
|
||||
|
||||
|
||||
def set_activation(container: dict[str, Any], path: list[str], value: float) -> tuple[float, float]:
|
||||
target: Any = container
|
||||
for key in path[:-1]:
|
||||
target = target[key]
|
||||
leaf = target[path[-1]]
|
||||
before = leaf["current_activation"]
|
||||
after = clamp(value)
|
||||
leaf["current_activation"] = after
|
||||
return before, after
|
||||
|
||||
|
||||
def ensure_list_item(items: list[Any], key: str, value: str, item: dict[str, Any]) -> None:
|
||||
if not any(existing.get(key) == value for existing in items if isinstance(existing, dict)):
|
||||
items.append(item)
|
||||
|
||||
|
||||
def ensure_value(items: list[str], value: str) -> None:
|
||||
if value not in items:
|
||||
items.append(value)
|
||||
|
||||
|
||||
def ensure_overlay(agent: dict[str, Any], observer_id: str, target_id: str) -> dict[str, Any]:
|
||||
for overlay in agent["perceived_state_overlays"]:
|
||||
if overlay["observer_agent_id"] == observer_id and overlay["target_agent_id"] == target_id:
|
||||
return overlay
|
||||
overlay = {
|
||||
"observer_agent_id": observer_id,
|
||||
"target_agent_id": target_id,
|
||||
"perceived_dimensions": {},
|
||||
"beliefs": [],
|
||||
"distortions": [],
|
||||
}
|
||||
agent["perceived_state_overlays"].append(overlay)
|
||||
return overlay
|
||||
|
||||
|
||||
def update_perceived_dimension(overlay: dict[str, Any], name: str, observed: float, attributed: float, confidence: float) -> None:
|
||||
overlay["perceived_dimensions"][name] = {
|
||||
"observed_activation": clamp(observed),
|
||||
"attributed_disposition": clamp(attributed),
|
||||
"confidence": clamp(confidence),
|
||||
}
|
||||
|
||||
|
||||
def normalize_action_type(raw: str) -> str:
|
||||
if raw == "speech":
|
||||
return "speak"
|
||||
if raw == "action":
|
||||
return "mixed"
|
||||
return raw
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Apply reviewed mutation from a sequential Ink branch capture.")
|
||||
parser.add_argument("--fixture", type=Path, default=DEFAULT_FIXTURE)
|
||||
parser.add_argument("--capture", type=Path, default=DEFAULT_CAPTURE)
|
||||
parser.add_argument("--out-state", type=Path, default=DEFAULT_OUT_STATE)
|
||||
parser.add_argument("--receipt", type=Path, default=DEFAULT_RECEIPT)
|
||||
args = parser.parse_args()
|
||||
|
||||
source_state = load_json(args.fixture)
|
||||
capture = load_json(args.capture)
|
||||
state = copy.deepcopy(source_state)
|
||||
|
||||
selected = capture["passes"]["selected_observable_action"]
|
||||
response = capture["passes"]["sella_response_generation"]["parsed_response"]["response"]
|
||||
actor_action_type = normalize_action_type(selected.get("action_type", ""))
|
||||
responder_action_type = normalize_action_type(response.get("action_type", ""))
|
||||
|
||||
maer_appraisal = {
|
||||
"schema_version": "ghostlight.manual_participant_appraisal.v0",
|
||||
"appraiser": "codex_manual_review",
|
||||
"participant_agent_id": "maer_tidecall",
|
||||
"trigger_observable_action": {
|
||||
"actor_agent_id": "maer_tidecall",
|
||||
"raw_action_type": selected.get("action_type"),
|
||||
"canonical_action_type": actor_action_type,
|
||||
"observable_action": selected.get("observable_action"),
|
||||
"spoken_text": selected.get("spoken_text", ""),
|
||||
},
|
||||
"private_interpretation": "Sella's answer keeps rescue possible but refuses to let Maer's ledger threat define the cost of care.",
|
||||
"attributed_motive": "Protect sanctuary integrity while leaving a narrow verification path open.",
|
||||
"misread_risk": "Medium. Maer may hear verification as obstruction if grievance outruns his respect for Sella's capacity boundary.",
|
||||
"state_deltas": [
|
||||
{
|
||||
"agent_id": "maer_tidecall",
|
||||
"path": "canonical_state.situational_state.grievance_activation.current_activation",
|
||||
"delta": 0.03,
|
||||
"rationale": "Rescue remains conditional when Maer wanted immediate intake readiness.",
|
||||
},
|
||||
{
|
||||
"agent_id": "maer_tidecall",
|
||||
"path": "canonical_state.behavioral_dimensions.control_pressure.current_activation",
|
||||
"delta": 0.02,
|
||||
"rationale": "He now has to carry the verification boundary into the next move.",
|
||||
},
|
||||
],
|
||||
"relationship_deltas": [
|
||||
{
|
||||
"relationship_id": "rel-maer-sella",
|
||||
"path": "stance.trust.current_activation",
|
||||
"delta": -0.01,
|
||||
"rationale": "Sella did not simply trust Maer's ledger framing.",
|
||||
},
|
||||
{
|
||||
"relationship_id": "rel-maer-sella",
|
||||
"path": "stance.respect.current_activation",
|
||||
"delta": 0.02,
|
||||
"rationale": "She named the cost plainly and did not hide behind false capacity.",
|
||||
},
|
||||
{
|
||||
"relationship_id": "rel-maer-sella",
|
||||
"path": "stance.obligation.current_activation",
|
||||
"delta": 0.03,
|
||||
"rationale": "The route now owes verification work if it wants sanctuary intake.",
|
||||
},
|
||||
],
|
||||
"next_action_constraints": [
|
||||
"Maer should treat Sella's verification boundary as real scene state.",
|
||||
"Maer should not assume Sella accepted or refused intake.",
|
||||
"Maer may push, offer evidence, soften, or seek another route, but not erase the boundary.",
|
||||
],
|
||||
}
|
||||
|
||||
sella_appraisal = {
|
||||
"schema_version": "ghostlight.manual_participant_appraisal.v0",
|
||||
"appraiser": "codex_manual_review",
|
||||
"participant_agent_id": "sella_ren",
|
||||
"trigger_observable_action": {
|
||||
"actor_agent_id": "maer_tidecall",
|
||||
"raw_action_type": selected.get("action_type"),
|
||||
"canonical_action_type": actor_action_type,
|
||||
"observable_action": selected.get("observable_action"),
|
||||
"spoken_text": selected.get("spoken_text", ""),
|
||||
},
|
||||
"private_interpretation": response["sella_private_interpretation"],
|
||||
"attributed_motive": response["sella_attributed_motive"],
|
||||
"misread_risk": response["misread_risk"],
|
||||
"state_deltas": [
|
||||
{
|
||||
"agent_id": "sella_ren",
|
||||
"path": "canonical_state.behavioral_dimensions.control_pressure.current_activation",
|
||||
"delta": 0.04,
|
||||
"rationale": "Maer's ledger-backed ask feels like pressure on sanctuary authority before Sella can verify the claim.",
|
||||
},
|
||||
{
|
||||
"agent_id": "sella_ren",
|
||||
"path": "canonical_state.situational_state.overstimulation.current_activation",
|
||||
"delta": 0.03,
|
||||
"rationale": "The slate and debt framing add cognitive load in an already overfull clinic.",
|
||||
},
|
||||
{
|
||||
"agent_id": "sella_ren",
|
||||
"path": "canonical_state.situational_state.grievance_activation.current_activation",
|
||||
"delta": 0.04,
|
||||
"rationale": "She hears rescue language converting care capacity into an owed route debt.",
|
||||
},
|
||||
],
|
||||
"relationship_deltas": [
|
||||
{
|
||||
"relationship_id": "rel-sella-maer",
|
||||
"path": "stance.trust.current_activation",
|
||||
"delta": -0.03,
|
||||
"rationale": "Sella trusts Maer's aim less in the moment because the approach pressures verification boundaries.",
|
||||
},
|
||||
{
|
||||
"relationship_id": "rel-sella-maer",
|
||||
"path": "stance.resentment.current_activation",
|
||||
"delta": 0.05,
|
||||
"rationale": "The ledger threat makes sanctuary capacity feel conscripted.",
|
||||
},
|
||||
{
|
||||
"relationship_id": "rel-sella-maer",
|
||||
"path": "stance.respect.current_activation",
|
||||
"delta": 0.02,
|
||||
"rationale": "He names the cost instead of hiding it, which she still respects.",
|
||||
},
|
||||
],
|
||||
"next_action_constraints": [
|
||||
"Sella should act from boundary-defense, not neutral triage calm.",
|
||||
"Sella should demand verification before accepting intake.",
|
||||
"Sella should not receive Maer's private intent as prompt context.",
|
||||
"Sella should preserve the unresolved personhood question.",
|
||||
],
|
||||
"review_note": "This appraisal was added manually after noticing the first sequential capture fused Maer's event and Sella's response without a clean per-turn consolidation boundary.",
|
||||
}
|
||||
participant_appraisals = [maer_appraisal, sella_appraisal]
|
||||
|
||||
event = {
|
||||
"event_id": EVENT_ID,
|
||||
"kind": "dialogue",
|
||||
"summary": "Maer made a ledger-backed sanctuary intake ask around a corrupted distress packet; Sella translated the ask into verification cost rather than clean acceptance or refusal.",
|
||||
"participants": ["maer_tidecall", "sella_ren"],
|
||||
"pressure_tags": [
|
||||
"cold_wake",
|
||||
"sanctuary_capacity",
|
||||
"rescue_obligation",
|
||||
"personhood_uncertainty",
|
||||
"verification_boundary",
|
||||
],
|
||||
"observed_exchange": [
|
||||
{
|
||||
"speaker_id": "maer_tidecall",
|
||||
"action_type": actor_action_type,
|
||||
"utterance_summary": selected["spoken_text"],
|
||||
"dialogue_function": "ledger_backed_capacity_ask",
|
||||
"observable_cues": [
|
||||
selected["observable_action"],
|
||||
"points to distress vessel eligibility line item",
|
||||
"frames refusal as route debt",
|
||||
],
|
||||
},
|
||||
{
|
||||
"speaker_id": "sella_ren",
|
||||
"action_type": responder_action_type,
|
||||
"utterance_summary": response["spoken_text"],
|
||||
"dialogue_function": "demand_verification_before_intake",
|
||||
"observable_cues": [
|
||||
"acknowledges the slate and line item",
|
||||
"names the bed cost directly",
|
||||
"refuses to treat packet noise as either proof or non-personhood",
|
||||
],
|
||||
},
|
||||
],
|
||||
"private_interpretations": [
|
||||
{
|
||||
"agent_id": "maer_tidecall",
|
||||
"interpretation": "Sella is not refusing rescue, but she is making verification the price of care and refusing to let Maer's ledger threat define the cost alone.",
|
||||
"attributed_motive": "Protect sanctuary integrity while leaving a narrow route for rescue if the packet can produce a voice-like claim.",
|
||||
"appraisal_tags": ["blocked_rescue", "boundary_respected", "verification_cost"],
|
||||
"confidence": 0.61,
|
||||
},
|
||||
{
|
||||
"agent_id": "sella_ren",
|
||||
"interpretation": sella_appraisal["private_interpretation"],
|
||||
"attributed_motive": sella_appraisal["attributed_motive"],
|
||||
"appraisal_tags": ["capacity_threat", "verification_boundary", "possible_misread"],
|
||||
"confidence": 0.68,
|
||||
},
|
||||
],
|
||||
"event_effects": [
|
||||
{
|
||||
"agent_id": "maer_tidecall",
|
||||
"summary": "Maer leaves the beat with rescue still live but more visibly conditional on Sella's verification boundary.",
|
||||
"state_deltas": [
|
||||
{"path": "canonical_state.situational_state.grievance_activation.current_activation", "delta": 0.03},
|
||||
{"path": "canonical_state.behavioral_dimensions.control_pressure.current_activation", "delta": 0.02},
|
||||
],
|
||||
"memory_update_ids": [MAER_MEMORY_ID],
|
||||
},
|
||||
{
|
||||
"agent_id": "sella_ren",
|
||||
"summary": "Sella absorbs the ledger ask as pressure, defends verification, and records Maer as both sincere and dangerous to capacity boundaries.",
|
||||
"state_deltas": [
|
||||
{"path": "canonical_state.behavioral_dimensions.control_pressure.current_activation", "delta": 0.04},
|
||||
{"path": "canonical_state.situational_state.overstimulation.current_activation", "delta": 0.03},
|
||||
{"path": "canonical_state.situational_state.grievance_activation.current_activation", "delta": 0.04},
|
||||
],
|
||||
"memory_update_ids": [SELLA_MEMORY_ID],
|
||||
},
|
||||
],
|
||||
}
|
||||
if not any(existing["event_id"] == EVENT_ID for existing in state["events"]):
|
||||
state["events"].append(event)
|
||||
|
||||
applied_changes: list[dict[str, Any]] = []
|
||||
maer = find_agent(state, "maer_tidecall")
|
||||
sella = find_agent(state, "sella_ren")
|
||||
rel_maer_sella = find_relationship(state, "rel-maer-sella")
|
||||
rel_sella_maer = find_relationship(state, "rel-sella-maer")
|
||||
|
||||
for path, delta in [
|
||||
(["canonical_state", "situational_state", "grievance_activation"], 0.03),
|
||||
(["canonical_state", "behavioral_dimensions", "control_pressure"], 0.02),
|
||||
]:
|
||||
before, after = apply_delta(maer, path, delta)
|
||||
applied_changes.append({"target": "maer_tidecall", "path": ".".join(path) + ".current_activation", "before": before, "after": after})
|
||||
|
||||
for path, delta in [
|
||||
(["canonical_state", "behavioral_dimensions", "control_pressure"], 0.04),
|
||||
(["canonical_state", "situational_state", "overstimulation"], 0.03),
|
||||
(["canonical_state", "situational_state", "grievance_activation"], 0.04),
|
||||
]:
|
||||
before, after = apply_delta(sella, path, delta)
|
||||
applied_changes.append({"target": "sella_ren", "path": ".".join(path) + ".current_activation", "before": before, "after": after})
|
||||
|
||||
for rel, deltas in [
|
||||
(rel_maer_sella, [("trust", -0.01), ("respect", 0.02), ("obligation", 0.03)]),
|
||||
(rel_sella_maer, [("trust", -0.03), ("resentment", 0.05), ("respect", 0.02), ("obligation", 0.02)]),
|
||||
]:
|
||||
for name, delta in deltas:
|
||||
before, after = apply_delta(rel, ["stance", name], delta)
|
||||
applied_changes.append({"target": rel["relationship_id"], "path": f"stance.{name}.current_activation", "before": before, "after": after})
|
||||
ensure_value(rel.setdefault("unresolved_incident_ids", []), EVENT_ID)
|
||||
|
||||
rel_maer_sella["summary"] = "Maer still trusts Sella's care, but now understands that she will make verification the cost of sanctuary intake rather than let route debt decide for her."
|
||||
rel_sella_maer["summary"] = "Sella respects Maer's rescue ethic and now more sharply distrusts how quickly his ledger language can conscript sanctuary capacity."
|
||||
|
||||
ensure_list_item(
|
||||
maer["memories"]["relationship_summaries"],
|
||||
"memory_id",
|
||||
MAER_MEMORY_ID,
|
||||
{
|
||||
"memory_id": MAER_MEMORY_ID,
|
||||
"summary": "Sella answered Maer's ledger-backed intake ask by making verification the cost of care, not by cleanly accepting or refusing rescue.",
|
||||
"salience": 0.77,
|
||||
"confidence": 0.69,
|
||||
"linked_event_ids": [EVENT_ID],
|
||||
"linked_relationship_id": "rel-maer-sella",
|
||||
},
|
||||
)
|
||||
ensure_list_item(
|
||||
sella["memories"]["relationship_summaries"],
|
||||
"memory_id",
|
||||
SELLA_MEMORY_ID,
|
||||
{
|
||||
"memory_id": SELLA_MEMORY_ID,
|
||||
"summary": "Maer made a ledger-backed rescue ask that sounded like a debt threat under scarcity; Sella held verification as the boundary.",
|
||||
"salience": 0.81,
|
||||
"confidence": 0.72,
|
||||
"linked_event_ids": [EVENT_ID],
|
||||
"linked_relationship_id": "rel-sella-maer",
|
||||
},
|
||||
)
|
||||
|
||||
maer_overlay = ensure_overlay(maer, "maer_tidecall", "sella_ren")
|
||||
update_perceived_dimension(maer_overlay, "control_pressure", 0.66, 0.58, 0.55)
|
||||
update_perceived_dimension(maer_overlay, "interpersonal_warmth", 0.68, 0.74, 0.58)
|
||||
update_perceived_dimension(maer_overlay, "rigidity", 0.61, 0.56, 0.51)
|
||||
ensure_list_item(
|
||||
maer_overlay["beliefs"],
|
||||
"belief_id",
|
||||
"belief-maer-sella-verification-is-care",
|
||||
{
|
||||
"belief_id": "belief-maer-sella-verification-is-care",
|
||||
"subject_id": "sella_ren",
|
||||
"claim_type": "motive_inference",
|
||||
"claim": "Sella is defending verification as part of care rather than using it as a polite refusal.",
|
||||
"confidence": 0.61,
|
||||
"evidence_event_ids": [EVENT_ID],
|
||||
"visibility": "private",
|
||||
"emotional_charge": 0.42,
|
||||
},
|
||||
)
|
||||
ensure_value(maer_overlay["distortions"], "blocked_rescue_overread")
|
||||
|
||||
sella_overlay = ensure_overlay(sella, "sella_ren", "maer_tidecall")
|
||||
update_perceived_dimension(sella_overlay, "control_pressure", 0.76, 0.64, 0.66)
|
||||
update_perceived_dimension(sella_overlay, "interpersonal_warmth", 0.58, 0.64, 0.59)
|
||||
update_perceived_dimension(sella_overlay, "rigidity", 0.58, 0.53, 0.52)
|
||||
ensure_list_item(
|
||||
sella_overlay["beliefs"],
|
||||
"belief_id",
|
||||
"belief-sella-maer-ledger-can-pressure-care",
|
||||
{
|
||||
"belief_id": "belief-sella-maer-ledger-can-pressure-care",
|
||||
"subject_id": "maer_tidecall",
|
||||
"claim_type": "motive_inference",
|
||||
"claim": "Maer means rescue honestly, but his ledger language can turn sanctuary refusal into moral debt before verification has room to breathe.",
|
||||
"confidence": 0.68,
|
||||
"evidence_event_ids": [EVENT_ID],
|
||||
"visibility": "private",
|
||||
"emotional_charge": 0.57,
|
||||
},
|
||||
)
|
||||
ensure_value(sella_overlay["distortions"], "pressure_overread")
|
||||
|
||||
scene = find_scene(state, "scene-02-sanctuary-intake")
|
||||
for pack in scene["dialogue_context_packs"]:
|
||||
if pack["speaker_agent_id"] == "maer_tidecall":
|
||||
ensure_value(pack["active_memories"], MAER_MEMORY_ID)
|
||||
ensure_value(pack["speaker_beliefs"], "Sella is likely to treat verification as part of care, not as a refusal to help.")
|
||||
if pack["speaker_agent_id"] == "sella_ren":
|
||||
ensure_value(pack["active_memories"], SELLA_MEMORY_ID)
|
||||
ensure_value(pack["speaker_beliefs"], "Maer's rescue ask can become pressure if it turns refusal into route debt before verification.")
|
||||
|
||||
receipt = {
|
||||
"schema_version": "ghostlight.reviewed_state_mutation.v0",
|
||||
"mutation_id": "mutation-sanctuary-ledger-verification-v1",
|
||||
"source_fixture_ref": str(args.fixture.relative_to(ROOT)).replace("\\", "/"),
|
||||
"source_capture_ref": str(args.capture.relative_to(ROOT)).replace("\\", "/"),
|
||||
"mutated_fixture_ref": str(args.out_state.relative_to(ROOT)).replace("\\", "/"),
|
||||
"selected_branch_id": selected["branch_id"],
|
||||
"event_id": EVENT_ID,
|
||||
"input_capture_limitation": "The source capture generated Sella's response before Ghostlight had a clean per-turn participant appraisal/consolidation boundary; this receipt manually reviews the missing boundary and applies it as training data.",
|
||||
"normalized_action_types": {
|
||||
"actor_raw": selected.get("action_type"),
|
||||
"actor_canonical": actor_action_type,
|
||||
"responder_raw": response.get("action_type"),
|
||||
"responder_canonical": responder_action_type,
|
||||
},
|
||||
"participant_appraisals": participant_appraisals,
|
||||
"observed_action": event["observed_exchange"][0],
|
||||
"observed_response": event["observed_exchange"][1],
|
||||
"applied_mutations": applied_changes,
|
||||
"manual_review_notes": [
|
||||
"Every involved character receives a state or perception update after the event.",
|
||||
"The selected event is consolidated before Sella is treated as the next actor.",
|
||||
"The corrupted packet's personhood remains unresolved.",
|
||||
"Ink variables remain playthrough-local; canonical state changes are written only through this reviewed receipt.",
|
||||
],
|
||||
"deferred_mutations": [
|
||||
"No world-level sanctuary bed is opened or closed by this beat.",
|
||||
"No claimant/personhood truth is resolved.",
|
||||
"No automatic classifier is trained yet; this is supervised seed data.",
|
||||
],
|
||||
}
|
||||
|
||||
write_json(args.out_state, state)
|
||||
write_json(args.receipt, receipt)
|
||||
print(f"wrote {args.out_state.relative_to(ROOT)}")
|
||||
print(f"wrote {args.receipt.relative_to(ROOT)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
538
tools/run_qwen_ink_sequential_generation.py
Normal file
538
tools/run_qwen_ink_sequential_generation.py
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
"""Run a sequential Qwen Ink generation prototype.
|
||||
|
||||
Pass 1: generate Maer choices from Maer-local awareness only.
|
||||
Pass 2: generate participant appraisal/consolidation for the selected
|
||||
observable event.
|
||||
Pass 3: generate the next actor's action from updated local state.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_ENDPOINT = "http://192.168.1.84:11434/api/generate"
|
||||
DEFAULT_FIXTURE = ROOT / "examples" / "agent-state.cold-wake-story-lab.json"
|
||||
DEFAULT_ANNOTATION = ROOT / "examples" / "ink" / "cold-wake-sanctuary-intake.training.json"
|
||||
DEFAULT_OUT_DIR = ROOT / "experiments" / "ink"
|
||||
VALID_ACTION_TYPES = {
|
||||
"speak",
|
||||
"silence",
|
||||
"move",
|
||||
"gesture",
|
||||
"touch_object",
|
||||
"block_object",
|
||||
"use_object",
|
||||
"show_object",
|
||||
"withhold_object",
|
||||
"transfer_object",
|
||||
"spend_resource",
|
||||
"attack",
|
||||
"wait",
|
||||
"mixed",
|
||||
}
|
||||
|
||||
|
||||
def load_json(path: Path) -> Any:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def write_text(path: Path, text: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
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 find_agent(fixture: dict[str, Any], agent_id: str) -> dict[str, Any]:
|
||||
return next(agent for agent in fixture["agents"] if agent["agent_id"] == agent_id)
|
||||
|
||||
|
||||
def find_scene(fixture: dict[str, Any], scene_id: str) -> dict[str, Any]:
|
||||
return next(scene for scene in fixture["scenes"] if scene["scene_id"] == scene_id)
|
||||
|
||||
|
||||
def find_pack(scene: dict[str, Any], speaker_id: str) -> dict[str, Any]:
|
||||
return next(pack for pack in scene["dialogue_context_packs"] if pack["speaker_agent_id"] == speaker_id)
|
||||
|
||||
|
||||
def find_relationship(fixture: dict[str, Any], source_id: str, target_id: str) -> dict[str, Any]:
|
||||
return next(rel for rel in fixture["relationships"] if rel["source_id"] == source_id and rel["target_id"] == target_id)
|
||||
|
||||
|
||||
def memory_summaries(agent: dict[str, Any], memory_ids: list[str]) -> list[str]:
|
||||
summaries: list[str] = []
|
||||
for group in agent["memories"].values():
|
||||
for memory in group:
|
||||
if memory["memory_id"] in memory_ids:
|
||||
summaries.append(f"{memory['memory_id']}: {memory['summary']}")
|
||||
return summaries
|
||||
|
||||
|
||||
def goal_summaries(agent: dict[str, Any], goal_ids: list[str]) -> list[str]:
|
||||
return [
|
||||
f"{goal['goal_id']}: {goal['description']} Priority {goal['priority']}. Stake: {goal['emotional_stake']}"
|
||||
for goal in agent["goals"]
|
||||
if goal["goal_id"] in goal_ids
|
||||
]
|
||||
|
||||
|
||||
def selected_variables(agent: dict[str, Any]) -> dict[str, dict[str, float]]:
|
||||
groups = agent["canonical_state"]
|
||||
names_by_group = {
|
||||
"behavioral_dimensions": ["interpersonal_warmth", "control_pressure", "suspicion", "rigidity", "volatility"],
|
||||
"presentation_strategy": ["detachment", "competence_theater", "strategic_opacity", "ironic_distance"],
|
||||
"voice_style": ["formality", "lexical_precision", "ritualized_address", "verbal_warmth", "pointedness"],
|
||||
"situational_state": ["exhaustion", "panic", "grievance_activation", "overstimulation"],
|
||||
}
|
||||
selected: dict[str, dict[str, float]] = {}
|
||||
for group_name, variable_names in names_by_group.items():
|
||||
selected[group_name] = {
|
||||
name: groups[group_name][name]["current_activation"]
|
||||
for name in variable_names
|
||||
if name in groups[group_name]
|
||||
}
|
||||
return selected
|
||||
|
||||
|
||||
def post_json(endpoint: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
request = urllib.request.Request(
|
||||
endpoint,
|
||||
data=data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=180) as response:
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
|
||||
|
||||
def extract_json_object(text: str) -> Any | None:
|
||||
stripped = text.strip()
|
||||
if stripped.startswith("{"):
|
||||
return json.loads(stripped)
|
||||
fenced = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", stripped, re.DOTALL)
|
||||
if fenced:
|
||||
return json.loads(fenced.group(1))
|
||||
start = stripped.find("{")
|
||||
end = stripped.rfind("}")
|
||||
if start != -1 and end != -1 and end > start:
|
||||
return json.loads(stripped[start : end + 1])
|
||||
return None
|
||||
|
||||
|
||||
def request_qwen(endpoint: str, model: str, prompt: str, temperature: float, top_p: float, num_ctx: int) -> tuple[dict[str, Any], str, Any, list[str]]:
|
||||
request_payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"think": False,
|
||||
"keep_alive": "10m",
|
||||
"options": {"num_ctx": num_ctx, "temperature": temperature, "top_p": top_p},
|
||||
}
|
||||
raw = post_json(endpoint, request_payload)
|
||||
response_text = raw.get("response", "").strip()
|
||||
parsed = None
|
||||
notes: list[str] = []
|
||||
try:
|
||||
parsed = extract_json_object(response_text)
|
||||
except (json.JSONDecodeError, TypeError) as exc:
|
||||
notes.append(f"parse error: {exc}")
|
||||
if parsed is None:
|
||||
notes.append("could not parse JSON")
|
||||
return raw, response_text, parsed, notes
|
||||
|
||||
|
||||
def observable_action_payload(actor_id: str, selected_choice: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"actor_id": actor_id,
|
||||
"action_type": selected_choice["action_type"],
|
||||
"observable_action": selected_choice["observable_action"],
|
||||
"spoken_text": selected_choice.get("spoken_text", ""),
|
||||
}
|
||||
|
||||
|
||||
def build_maer_prompt(fixture: dict[str, Any], annotation: dict[str, Any]) -> str:
|
||||
scene_id = annotation["scene_id"]
|
||||
actor_id = annotation["protagonist_agent_id"]
|
||||
responder_id = annotation["npc_agent_ids"][0]
|
||||
scene = find_scene(fixture, scene_id)
|
||||
actor = find_agent(fixture, actor_id)
|
||||
pack = find_pack(scene, actor_id)
|
||||
rel_actor_to_responder = find_relationship(fixture, actor_id, responder_id)
|
||||
|
||||
payload = {
|
||||
"task": "Generate candidate player choices for Maer only. Do not write Sella's response.",
|
||||
"output_contract": {
|
||||
"format": "Return only JSON. No markdown. No commentary.",
|
||||
"root_keys": ["choices"],
|
||||
"choice_count": 4,
|
||||
"valid_action_types": sorted(VALID_ACTION_TYPES),
|
||||
"choice_fields": [
|
||||
"branch_id",
|
||||
"ink_path",
|
||||
"choice_text",
|
||||
"action_type",
|
||||
"actor_intent_private",
|
||||
"observable_action",
|
||||
"spoken_text",
|
||||
"state_basis",
|
||||
"expected_consequence_surfaces",
|
||||
"training_hooks",
|
||||
],
|
||||
},
|
||||
"hard_rules": [
|
||||
"Use only Maer's local awareness and scene-observable facts.",
|
||||
"Do not include Sella's private state, private motives, or future response.",
|
||||
"Do not resolve whether the corrupted packet contains a person.",
|
||||
"Include at least two non-speech choices.",
|
||||
f"Use action_type values only from this exact enum: {', '.join(sorted(VALID_ACTION_TYPES))}.",
|
||||
"The selected branch must be reducible to observable action for Sella.",
|
||||
"Use semantic training hooks, not branch ids.",
|
||||
],
|
||||
"scene_observable_context": {
|
||||
"scene_id": scene_id,
|
||||
"location": scene["location"],
|
||||
"public_stakes": scene["public_stakes"],
|
||||
},
|
||||
"maer_local_awareness": {
|
||||
"identity": actor["identity"],
|
||||
"local_truth": pack["speaker_local_truth"],
|
||||
"beliefs": pack["speaker_beliefs"],
|
||||
"goals": goal_summaries(actor, pack["active_goals"]),
|
||||
"memories": memory_summaries(actor, pack["active_memories"]),
|
||||
"relationship_read": rel_actor_to_responder["summary"],
|
||||
"presentation_constraints": pack["presentation_constraints"],
|
||||
"selected_current_activation": selected_variables(actor),
|
||||
},
|
||||
"projection_controls_visible_to_maer_choice_generation": annotation["projection_controls"],
|
||||
}
|
||||
return (
|
||||
"You are Ghostlight's Maer-local choice generator. Generate only Maer's player choices.\n"
|
||||
"Do not write Sella's response or private interpretation. Return valid JSON only.\n\n"
|
||||
+ json.dumps(payload, indent=2, ensure_ascii=False)
|
||||
)
|
||||
|
||||
|
||||
def sella_local_payload(fixture: dict[str, Any], annotation: dict[str, Any]) -> dict[str, Any]:
|
||||
scene_id = annotation["scene_id"]
|
||||
responder_id = annotation["npc_agent_ids"][0]
|
||||
actor_id = annotation["protagonist_agent_id"]
|
||||
scene = find_scene(fixture, scene_id)
|
||||
responder = find_agent(fixture, responder_id)
|
||||
pack = find_pack(scene, responder_id)
|
||||
rel_responder_to_actor = find_relationship(fixture, responder_id, actor_id)
|
||||
return {
|
||||
"identity": responder["identity"],
|
||||
"local_truth": pack["speaker_local_truth"],
|
||||
"beliefs": pack["speaker_beliefs"],
|
||||
"goals": goal_summaries(responder, pack["active_goals"]),
|
||||
"memories": memory_summaries(responder, pack["active_memories"]),
|
||||
"relationship_read": rel_responder_to_actor["summary"],
|
||||
"presentation_constraints": pack["presentation_constraints"],
|
||||
"selected_current_activation": selected_variables(responder),
|
||||
}
|
||||
|
||||
|
||||
def build_sella_appraisal_prompt(fixture: dict[str, Any], annotation: dict[str, Any], selected_choice: dict[str, Any]) -> str:
|
||||
scene_id = annotation["scene_id"]
|
||||
responder_id = annotation["npc_agent_ids"][0]
|
||||
actor_id = annotation["protagonist_agent_id"]
|
||||
scene = find_scene(fixture, scene_id)
|
||||
payload = {
|
||||
"task": "Generate Sella's participant appraisal/consolidation for one observable Maer event.",
|
||||
"output_contract": {
|
||||
"format": "Return only JSON. No markdown. No commentary.",
|
||||
"root_keys": ["appraisal"],
|
||||
"appraisal_fields": [
|
||||
"responder_agent_id",
|
||||
"private_interpretation",
|
||||
"attributed_motive",
|
||||
"misread_risk",
|
||||
"immediate_state_deltas",
|
||||
"relationship_deltas",
|
||||
"belief_updates",
|
||||
"response_constraints",
|
||||
],
|
||||
},
|
||||
"hard_rules": [
|
||||
"Use only Sella's local awareness plus the observable Maer action.",
|
||||
"Do not read Maer's private intent. Treat actor intent as unavailable.",
|
||||
"Sella may correctly read, partially read, or misread Maer's action.",
|
||||
"Do not resolve whether the corrupted packet contains a person.",
|
||||
"This pass proposes reviewed state changes for Sella caused by the event. Do not write Sella's next action.",
|
||||
"Keep deltas small and bounded between -0.20 and 0.20.",
|
||||
],
|
||||
"scene_observable_context": {
|
||||
"scene_id": scene_id,
|
||||
"location": scene["location"],
|
||||
"public_stakes": scene["public_stakes"],
|
||||
},
|
||||
"observable_maer_action": observable_action_payload(actor_id, selected_choice),
|
||||
"sella_local_awareness": sella_local_payload(fixture, annotation),
|
||||
"projection_controls_visible_to_sella_appraisal": annotation["projection_controls"],
|
||||
}
|
||||
return (
|
||||
"You are Ghostlight's Sella-local appraisal generator. Generate only Sella's immediate internal appraisal.\n"
|
||||
"Do not use Maer's private intent. Do not write her response. Return valid JSON only.\n\n"
|
||||
+ json.dumps(payload, indent=2, ensure_ascii=False)
|
||||
)
|
||||
|
||||
|
||||
def build_sella_prompt(
|
||||
fixture: dict[str, Any],
|
||||
annotation: dict[str, Any],
|
||||
selected_choice: dict[str, Any],
|
||||
appraisal: dict[str, Any] | None,
|
||||
) -> str:
|
||||
scene_id = annotation["scene_id"]
|
||||
responder_id = annotation["npc_agent_ids"][0]
|
||||
actor_id = annotation["protagonist_agent_id"]
|
||||
scene = find_scene(fixture, scene_id)
|
||||
payload = {
|
||||
"task": "Generate Sella's next action from Sella-local awareness after applying reviewed event appraisal.",
|
||||
"output_contract": {
|
||||
"format": "Return only JSON. No markdown. No commentary.",
|
||||
"root_keys": ["response"],
|
||||
"valid_action_types": sorted(VALID_ACTION_TYPES),
|
||||
"response_fields": [
|
||||
"responder_agent_id",
|
||||
"action_type",
|
||||
"observable_response",
|
||||
"spoken_text",
|
||||
"sella_private_interpretation",
|
||||
"sella_attributed_motive",
|
||||
"misread_risk",
|
||||
"state_basis",
|
||||
"reviewed_consequences",
|
||||
"training_hooks",
|
||||
],
|
||||
},
|
||||
"hard_rules": [
|
||||
"Use only Sella's local awareness, the observable Maer event, and reviewed Sella state changes from that event.",
|
||||
"Do not read Maer's private intent. Treat actor intent as unavailable.",
|
||||
"Sella may correctly read, partially read, or misread Maer's action.",
|
||||
"Do not resolve whether the corrupted packet contains a person.",
|
||||
"Do not accept without cost, condition, authority defense, or resentment risk.",
|
||||
f"Use action_type values only from this exact enum: {', '.join(sorted(VALID_ACTION_TYPES))}.",
|
||||
"Use semantic training hooks, not branch ids.",
|
||||
],
|
||||
"scene_observable_context": {
|
||||
"scene_id": scene_id,
|
||||
"location": scene["location"],
|
||||
"public_stakes": scene["public_stakes"],
|
||||
},
|
||||
"observable_maer_action": observable_action_payload(actor_id, selected_choice),
|
||||
"sella_immediate_appraisal": appraisal,
|
||||
"sella_local_awareness": sella_local_payload(fixture, annotation),
|
||||
"projection_controls_visible_to_sella_response_generation": annotation["projection_controls"],
|
||||
}
|
||||
return (
|
||||
"You are Ghostlight's Sella-local next-action generator. Generate only Sella's next action.\n"
|
||||
"Use reviewed event appraisal as the changed state she is acting from. Return valid JSON only.\n\n"
|
||||
+ json.dumps(payload, indent=2, ensure_ascii=False)
|
||||
)
|
||||
|
||||
|
||||
def validate_choice_payload(parsed: Any) -> list[str]:
|
||||
notes: list[str] = []
|
||||
if not isinstance(parsed, dict):
|
||||
return ["choice response was not an object"]
|
||||
choices = parsed.get("choices")
|
||||
if not isinstance(choices, list):
|
||||
return ["choice response missing choices array"]
|
||||
if len(choices) != 4:
|
||||
notes.append(f"expected 4 choices, got {len(choices)}")
|
||||
required = ["branch_id", "ink_path", "choice_text", "action_type", "actor_intent_private", "observable_action", "state_basis", "expected_consequence_surfaces", "training_hooks"]
|
||||
non_speech = 0
|
||||
for index, choice in enumerate(choices):
|
||||
if not isinstance(choice, dict):
|
||||
notes.append(f"choices[{index}] was not an object")
|
||||
continue
|
||||
missing = [key for key in required if key not in choice]
|
||||
if missing:
|
||||
notes.append(f"choices[{index}] missing keys: {', '.join(missing)}")
|
||||
if choice.get("action_type") not in VALID_ACTION_TYPES:
|
||||
notes.append(f"choices[{index}] invalid action_type")
|
||||
for list_key in ["expected_consequence_surfaces", "training_hooks"]:
|
||||
if not isinstance(choice.get(list_key), list):
|
||||
notes.append(f"choices[{index}] {list_key} must be an array")
|
||||
if choice.get("action_type") != "speak":
|
||||
non_speech += 1
|
||||
if non_speech < 2:
|
||||
notes.append("choices had fewer than two non-speech actions")
|
||||
return notes
|
||||
|
||||
|
||||
def validate_appraisal_payload(parsed: Any) -> list[str]:
|
||||
if not isinstance(parsed, dict):
|
||||
return ["appraisal was not an object"]
|
||||
appraisal = parsed.get("appraisal")
|
||||
if not isinstance(appraisal, dict):
|
||||
return ["appraisal missing appraisal object"]
|
||||
required = [
|
||||
"responder_agent_id",
|
||||
"private_interpretation",
|
||||
"attributed_motive",
|
||||
"misread_risk",
|
||||
"immediate_state_deltas",
|
||||
"relationship_deltas",
|
||||
"response_constraints",
|
||||
]
|
||||
missing = [key for key in required if key not in appraisal]
|
||||
notes = [f"appraisal missing keys: {', '.join(missing)}"] if missing else []
|
||||
for list_key in ["immediate_state_deltas", "relationship_deltas", "response_constraints"]:
|
||||
if list_key in appraisal and not isinstance(appraisal[list_key], list):
|
||||
notes.append(f"appraisal.{list_key} must be an array")
|
||||
return notes
|
||||
|
||||
|
||||
def validate_response_payload(parsed: Any) -> list[str]:
|
||||
if not isinstance(parsed, dict):
|
||||
return ["response was not an object"]
|
||||
response = parsed.get("response")
|
||||
if not isinstance(response, dict):
|
||||
return ["response missing response object"]
|
||||
required = ["responder_agent_id", "action_type", "observable_response", "sella_private_interpretation", "misread_risk", "state_basis", "reviewed_consequences", "training_hooks"]
|
||||
missing = [key for key in required if key not in response]
|
||||
notes = [f"response missing keys: {', '.join(missing)}"] if missing else []
|
||||
if response.get("action_type") not in VALID_ACTION_TYPES:
|
||||
notes.append("response invalid action_type")
|
||||
if "training_hooks" in response and not isinstance(response["training_hooks"], list):
|
||||
notes.append("response.training_hooks must be an array")
|
||||
return notes
|
||||
|
||||
|
||||
def choose_selected_choice(parsed: Any, preferred_action: str) -> dict[str, Any]:
|
||||
choices = parsed.get("choices", []) if isinstance(parsed, dict) else []
|
||||
for choice in choices:
|
||||
if choice.get("action_type") == preferred_action:
|
||||
return choice
|
||||
if choices:
|
||||
return choices[0]
|
||||
raise SystemExit("No choices available to select")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Run a sequential local-awareness Qwen Ink prototype.")
|
||||
parser.add_argument("--fixture", type=Path, default=DEFAULT_FIXTURE)
|
||||
parser.add_argument("--annotation", type=Path, default=DEFAULT_ANNOTATION)
|
||||
parser.add_argument("--out-dir", type=Path, default=DEFAULT_OUT_DIR)
|
||||
parser.add_argument("--endpoint", default=DEFAULT_ENDPOINT)
|
||||
parser.add_argument("--model", default="qwen3.5:9b")
|
||||
parser.add_argument("--temperature", type=float, default=0.55)
|
||||
parser.add_argument("--top-p", type=float, default=0.9)
|
||||
parser.add_argument("--num-ctx", type=int, default=8192)
|
||||
parser.add_argument("--capture-id", default="cold-wake-sanctuary-intake.qwen-sequential.v1")
|
||||
parser.add_argument("--preferred-action", default="show_object")
|
||||
args = parser.parse_args()
|
||||
|
||||
fixture = load_json(args.fixture)
|
||||
annotation = load_json(args.annotation)
|
||||
maer_prompt = build_maer_prompt(fixture, annotation)
|
||||
maer_raw, maer_response_text, maer_parsed, maer_notes = request_qwen(
|
||||
args.endpoint, args.model, maer_prompt, args.temperature, args.top_p, args.num_ctx
|
||||
)
|
||||
maer_notes.extend(validate_choice_payload(maer_parsed))
|
||||
selected_choice = choose_selected_choice(maer_parsed, args.preferred_action)
|
||||
|
||||
sella_appraisal_prompt = build_sella_appraisal_prompt(fixture, annotation, selected_choice)
|
||||
sella_appraisal_raw, sella_appraisal_response_text, sella_appraisal_parsed, sella_appraisal_notes = request_qwen(
|
||||
args.endpoint, args.model, sella_appraisal_prompt, args.temperature, args.top_p, args.num_ctx
|
||||
)
|
||||
sella_appraisal_notes.extend(validate_appraisal_payload(sella_appraisal_parsed))
|
||||
appraisal = sella_appraisal_parsed.get("appraisal") if isinstance(sella_appraisal_parsed, dict) else None
|
||||
|
||||
sella_prompt = build_sella_prompt(fixture, annotation, selected_choice, appraisal)
|
||||
sella_raw, sella_response_text, sella_parsed, sella_notes = request_qwen(
|
||||
args.endpoint, args.model, sella_prompt, args.temperature, args.top_p, args.num_ctx
|
||||
)
|
||||
sella_notes.extend(validate_response_payload(sella_parsed))
|
||||
|
||||
all_notes = maer_notes + sella_appraisal_notes + sella_notes
|
||||
capture = {
|
||||
"schema_version": "ghostlight.qwen_ink_sequential_capture.v0",
|
||||
"capture_id": args.capture_id,
|
||||
"source_fixture_ref": str(args.fixture.relative_to(ROOT)).replace("\\", "/"),
|
||||
"source_annotation_ref": str(args.annotation.relative_to(ROOT)).replace("\\", "/"),
|
||||
"model": maer_raw.get("model", args.model),
|
||||
"endpoint": args.endpoint,
|
||||
"request_options": {
|
||||
"think": False,
|
||||
"num_ctx": args.num_ctx,
|
||||
"temperature": args.temperature,
|
||||
"top_p": args.top_p,
|
||||
},
|
||||
"passes": {
|
||||
"maer_choice_generation": {
|
||||
"local_agent_id": "maer_tidecall",
|
||||
"prompt_text": maer_prompt,
|
||||
"response_text": maer_response_text,
|
||||
"parsed_response": maer_parsed,
|
||||
"validation_notes": maer_notes,
|
||||
},
|
||||
"selected_observable_action": selected_choice,
|
||||
"sella_immediate_appraisal": {
|
||||
"local_agent_id": "sella_ren",
|
||||
"prompt_text": sella_appraisal_prompt,
|
||||
"response_text": sella_appraisal_response_text,
|
||||
"parsed_response": sella_appraisal_parsed,
|
||||
"validation_notes": sella_appraisal_notes,
|
||||
},
|
||||
"sella_response_generation": {
|
||||
"local_agent_id": "sella_ren",
|
||||
"prompt_text": sella_prompt,
|
||||
"response_text": sella_response_text,
|
||||
"parsed_response": sella_parsed,
|
||||
"validation_notes": sella_notes,
|
||||
},
|
||||
},
|
||||
"review": {
|
||||
"status": "accepted_as_draft" if not all_notes else "useful_needs_revision",
|
||||
"strengths": [
|
||||
"Separated Maer choice generation from Sella next-action generation.",
|
||||
"Inserted participant appraisal/consolidation before next-actor action generation.",
|
||||
"Sella prompt received observable Maer event and reviewed Sella state changes, not Maer's private intent.",
|
||||
],
|
||||
"failure_notes": all_notes,
|
||||
"accepted_into_ink_ref": None,
|
||||
"pipeline_lessons": [
|
||||
"Sequential generation needs event appraisal/consolidation before next actor selection.",
|
||||
"Action type selection should be tool-shaped or schema-constrained, not left to prose compliance.",
|
||||
"Next pass should materialize the sequential capture into Ink and mutation training data.",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
args.out_dir.mkdir(parents=True, exist_ok=True)
|
||||
base = args.capture_id.replace(".", "-")
|
||||
capture_path = args.out_dir / f"{base}.capture.json"
|
||||
maer_prompt_path = args.out_dir / f"{base}.maer.prompt.md"
|
||||
maer_response_path = args.out_dir / f"{base}.maer.response.md"
|
||||
sella_appraisal_prompt_path = args.out_dir / f"{base}.sella-appraisal.prompt.md"
|
||||
sella_appraisal_response_path = args.out_dir / f"{base}.sella-appraisal.response.md"
|
||||
sella_prompt_path = args.out_dir / f"{base}.sella.prompt.md"
|
||||
sella_response_path = args.out_dir / f"{base}.sella.response.md"
|
||||
write_json(capture_path, capture)
|
||||
write_text(maer_prompt_path, maer_prompt + "\n")
|
||||
write_text(maer_response_path, maer_response_text + "\n")
|
||||
write_text(sella_appraisal_prompt_path, sella_appraisal_prompt + "\n")
|
||||
write_text(sella_appraisal_response_path, sella_appraisal_response_text + "\n")
|
||||
write_text(sella_prompt_path, sella_prompt + "\n")
|
||||
write_text(sella_response_path, sella_response_text + "\n")
|
||||
print(f"wrote {capture_path.relative_to(ROOT)}")
|
||||
print(json.dumps({"selected_observable_action": selected_choice, "sella_appraisal": sella_appraisal_parsed, "sella_response": sella_parsed}, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -14,6 +14,7 @@ from typing import Any
|
|||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_INK_FILES = sorted((ROOT / "examples" / "ink").glob("*.ink"))
|
||||
DEFAULT_QWEN_INK_CAPTURES = sorted((ROOT / "experiments" / "ink").glob("*.capture.json"))
|
||||
DEFAULT_MUTATION_RECEIPTS = sorted((ROOT / "experiments" / "ink").glob("*.mutation.json"))
|
||||
VALID_ACTION_TYPES = {
|
||||
"speak",
|
||||
"silence",
|
||||
|
|
@ -47,6 +48,11 @@ def require_keys(obj: dict[str, Any], keys: list[str], path: str) -> None:
|
|||
require(not missing, f"{path} missing required keys: {', '.join(missing)}")
|
||||
|
||||
|
||||
def require_0_1(value: Any, path: str) -> None:
|
||||
require(isinstance(value, (int, float)), f"{path} must be numeric")
|
||||
require(0 <= value <= 1, f"{path} must be between 0 and 1")
|
||||
|
||||
|
||||
def load_json(path: Path) -> Any:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
|
@ -223,6 +229,9 @@ def validate_annotation(path: Path, ink_path: Path) -> None:
|
|||
def validate_qwen_ink_capture(path: Path) -> None:
|
||||
data = load_json(path)
|
||||
require(isinstance(data, dict), f"{path} must be an object")
|
||||
if data.get("schema_version") == "ghostlight.qwen_ink_sequential_capture.v0":
|
||||
validate_qwen_ink_sequential_capture(path, data)
|
||||
return
|
||||
require_keys(
|
||||
data,
|
||||
[
|
||||
|
|
@ -285,6 +294,123 @@ def validate_qwen_ink_capture(path: Path) -> None:
|
|||
require((ROOT / accepted_ref).exists(), f"{path}.review.accepted_into_ink_ref does not exist")
|
||||
|
||||
|
||||
def validate_action_type_or_review_note(action_type: Any, review_status: str, path: str) -> None:
|
||||
if action_type in VALID_ACTION_TYPES:
|
||||
return
|
||||
require(
|
||||
review_status in {"needs_revision", "useful_needs_revision", "rejected"},
|
||||
f"{path} has invalid action_type but review.status is not a revision/rejection status",
|
||||
)
|
||||
|
||||
|
||||
def validate_qwen_ink_sequential_capture(path: Path, data: dict[str, Any]) -> None:
|
||||
require_keys(
|
||||
data,
|
||||
[
|
||||
"schema_version",
|
||||
"capture_id",
|
||||
"source_fixture_ref",
|
||||
"source_annotation_ref",
|
||||
"model",
|
||||
"endpoint",
|
||||
"request_options",
|
||||
"passes",
|
||||
"review",
|
||||
],
|
||||
str(path),
|
||||
)
|
||||
require((ROOT / data["source_fixture_ref"]).exists(), f"{path}.source_fixture_ref does not exist")
|
||||
require((ROOT / data["source_annotation_ref"]).exists(), f"{path}.source_annotation_ref does not exist")
|
||||
review = data["review"]
|
||||
require(isinstance(review, dict), f"{path}.review must be an object")
|
||||
require_keys(review, ["status", "strengths", "failure_notes", "accepted_into_ink_ref", "pipeline_lessons"], f"{path}.review")
|
||||
review_status = review["status"]
|
||||
require(review_status in VALID_CAPTURE_REVIEW_STATUSES, f"{path}.review.status is invalid or unreviewed")
|
||||
|
||||
passes = data["passes"]
|
||||
require_keys(passes, ["maer_choice_generation", "selected_observable_action", "sella_response_generation"], f"{path}.passes")
|
||||
if "sella_immediate_appraisal" not in passes:
|
||||
require(
|
||||
review_status in {"needs_revision", "useful_needs_revision", "rejected"},
|
||||
f"{path}.passes missing sella_immediate_appraisal but review.status is not a revision/rejection status",
|
||||
)
|
||||
for pass_key in ["maer_choice_generation", "sella_response_generation"]:
|
||||
pass_payload = passes[pass_key]
|
||||
require_keys(pass_payload, ["local_agent_id", "prompt_text", "response_text", "parsed_response", "validation_notes"], f"{path}.passes.{pass_key}")
|
||||
require(isinstance(pass_payload["prompt_text"], str) and pass_payload["prompt_text"].strip(), f"{path}.passes.{pass_key}.prompt_text must be non-empty")
|
||||
require(isinstance(pass_payload["response_text"], str) and pass_payload["response_text"].strip(), f"{path}.passes.{pass_key}.response_text must be non-empty")
|
||||
require(isinstance(pass_payload["validation_notes"], list), f"{path}.passes.{pass_key}.validation_notes must be an array")
|
||||
if "sella_immediate_appraisal" in passes:
|
||||
pass_payload = passes["sella_immediate_appraisal"]
|
||||
require_keys(pass_payload, ["local_agent_id", "prompt_text", "response_text", "parsed_response", "validation_notes"], f"{path}.passes.sella_immediate_appraisal")
|
||||
if review_status in {"accepted", "accepted_as_draft"}:
|
||||
appraisal_root = pass_payload["parsed_response"]
|
||||
require(isinstance(appraisal_root, dict), f"{path}.passes.sella_immediate_appraisal.parsed_response must be an object")
|
||||
appraisal = appraisal_root.get("appraisal")
|
||||
require(isinstance(appraisal, dict), f"{path}.passes.sella_immediate_appraisal.parsed_response.appraisal must be an object")
|
||||
for list_key in ["immediate_state_deltas", "relationship_deltas", "response_constraints"]:
|
||||
require(isinstance(appraisal.get(list_key), list), f"{path}.passes.sella_immediate_appraisal.appraisal.{list_key} must be an array for accepted captures")
|
||||
|
||||
choice = passes["selected_observable_action"]
|
||||
require(isinstance(choice, dict), f"{path}.passes.selected_observable_action must be an object")
|
||||
require_keys(choice, ["branch_id", "ink_path", "choice_text", "action_type", "observable_action"], f"{path}.passes.selected_observable_action")
|
||||
validate_action_type_or_review_note(choice["action_type"], review_status, f"{path}.passes.selected_observable_action.action_type")
|
||||
if review_status in {"accepted", "accepted_as_draft"}:
|
||||
for list_key in ["expected_consequence_surfaces", "training_hooks"]:
|
||||
require(isinstance(choice.get(list_key), list), f"{path}.passes.selected_observable_action.{list_key} must be an array for accepted captures")
|
||||
|
||||
response = passes["sella_response_generation"]["parsed_response"]
|
||||
require(isinstance(response, dict), f"{path}.passes.sella_response_generation.parsed_response must be an object")
|
||||
response_obj = response.get("response")
|
||||
require(isinstance(response_obj, dict), f"{path}.passes.sella_response_generation.parsed_response.response must be an object")
|
||||
require_keys(response_obj, ["responder_agent_id", "action_type", "observable_response", "sella_private_interpretation", "misread_risk"], f"{path}.passes.sella_response_generation.response")
|
||||
validate_action_type_or_review_note(response_obj["action_type"], review_status, f"{path}.passes.sella_response_generation.response.action_type")
|
||||
if review_status in {"accepted", "accepted_as_draft"}:
|
||||
require(isinstance(response_obj.get("training_hooks"), list), f"{path}.passes.sella_response_generation.response.training_hooks must be an array for accepted captures")
|
||||
|
||||
|
||||
def validate_mutation_receipt(path: Path) -> None:
|
||||
data = load_json(path)
|
||||
require(isinstance(data, dict), f"{path} must be an object")
|
||||
require_keys(
|
||||
data,
|
||||
[
|
||||
"schema_version",
|
||||
"mutation_id",
|
||||
"source_fixture_ref",
|
||||
"source_capture_ref",
|
||||
"mutated_fixture_ref",
|
||||
"selected_branch_id",
|
||||
"event_id",
|
||||
"participant_appraisals",
|
||||
"applied_mutations",
|
||||
"manual_review_notes",
|
||||
"deferred_mutations",
|
||||
],
|
||||
str(path),
|
||||
)
|
||||
require(data["schema_version"] == "ghostlight.reviewed_state_mutation.v0", f"{path}.schema_version is wrong")
|
||||
require((ROOT / data["source_fixture_ref"]).exists(), f"{path}.source_fixture_ref does not exist")
|
||||
require((ROOT / data["source_capture_ref"]).exists(), f"{path}.source_capture_ref does not exist")
|
||||
require((ROOT / data["mutated_fixture_ref"]).exists(), f"{path}.mutated_fixture_ref does not exist")
|
||||
appraisals = data["participant_appraisals"]
|
||||
require(isinstance(appraisals, list) and appraisals, f"{path}.participant_appraisals must be a non-empty array")
|
||||
for index, appraisal in enumerate(appraisals):
|
||||
appraisal_path = f"{path}.participant_appraisals[{index}]"
|
||||
require(isinstance(appraisal, dict), f"{appraisal_path} must be an object")
|
||||
require_keys(
|
||||
appraisal,
|
||||
["participant_agent_id", "trigger_observable_action", "private_interpretation", "state_deltas", "next_action_constraints"],
|
||||
appraisal_path,
|
||||
)
|
||||
require(isinstance(data["applied_mutations"], list) and data["applied_mutations"], f"{path}.applied_mutations must be non-empty")
|
||||
for index, mutation in enumerate(data["applied_mutations"]):
|
||||
mutation_path = f"{path}.applied_mutations[{index}]"
|
||||
require_keys(mutation, ["target", "path", "before", "after"], mutation_path)
|
||||
require_0_1(mutation["before"], f"{mutation_path}.before")
|
||||
require_0_1(mutation["after"], f"{mutation_path}.after")
|
||||
|
||||
|
||||
def validate_ink(path: Path) -> None:
|
||||
compile_ink(path)
|
||||
annotation_path = path.with_suffix(".training.json")
|
||||
|
|
@ -305,6 +431,9 @@ def main() -> int:
|
|||
for path in DEFAULT_QWEN_INK_CAPTURES:
|
||||
validate_qwen_ink_capture(path)
|
||||
print(f"ok: {path}")
|
||||
for path in DEFAULT_MUTATION_RECEIPTS:
|
||||
validate_mutation_receipt(path)
|
||||
print(f"ok: {path}")
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue