Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions Scripts/analyze_dialectic.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,12 @@ class Turn:
candidates: list[dict]
has_fingerprint: bool
fingerprint_model: str | None # model name from generatorFingerprint, if any
fingerprint_prompt_hash: str | None
# Witness provenance — separate from the pole fingerprint, which only
# attests the candidates' generator. nil on pre-Witness-fingerprint logs.
has_witness_fingerprint: bool
witness_fingerprint_model: str | None
witness_fingerprint_prompt_hash: str | None

def __post_init__(self) -> None:
# If outcome is somehow None or empty, fall back to a sentinel
Expand Down Expand Up @@ -330,6 +336,22 @@ def load_turns(path: Path) -> list[Turn]:
if d.get("generatorFingerprint") is not None
else None
),
fingerprint_prompt_hash=(
d.get("generatorFingerprint", {}).get("promptHash")
if d.get("generatorFingerprint") is not None
else None
),
has_witness_fingerprint=d.get("witnessGeneratorFingerprint") is not None,
witness_fingerprint_model=(
d.get("witnessGeneratorFingerprint", {}).get("model")
if d.get("witnessGeneratorFingerprint") is not None
else None
),
witness_fingerprint_prompt_hash=(
d.get("witnessGeneratorFingerprint", {}).get("promptHash")
if d.get("witnessGeneratorFingerprint") is not None
else None
),
))
rows.sort(key=lambda t: t.index)
return rows
Expand Down Expand Up @@ -1322,6 +1344,13 @@ class ProvenanceReport:
turns_with_fingerprint: int
fingerprint_rate: float
fingerprint_models: dict[str, int] # model name -> count
# Witness provenance. A Reflective run recorded after the Witness
# fingerprint landed should have one per witness-attempted turn; the
# prompt-hash collision count is the old bug's signature (a "Witness"
# transmitting the pole prompt) and must be 0.
turns_with_witness_fingerprint: int
witness_fingerprint_models: dict[str, int]
witness_pole_prompt_hash_collisions: int
note: str


Expand All @@ -1331,11 +1360,25 @@ def compute_provenance(turns: list[Turn]) -> ProvenanceReport:
for t in turns:
if t.has_fingerprint and t.fingerprint_model:
models[t.fingerprint_model] += 1
wfp = sum(1 for t in turns if t.has_witness_fingerprint)
witness_models: collections.Counter = collections.Counter()
collisions = 0
for t in turns:
if not t.has_witness_fingerprint:
continue
if t.witness_fingerprint_model:
witness_models[t.witness_fingerprint_model] += 1
if (t.witness_fingerprint_prompt_hash is not None
and t.witness_fingerprint_prompt_hash == t.fingerprint_prompt_hash):
collisions += 1
return ProvenanceReport(
total_turns=len(turns),
turns_with_fingerprint=fp,
fingerprint_rate=(fp / len(turns)) if turns else 0.0,
fingerprint_models=dict(models),
turns_with_witness_fingerprint=wfp,
witness_fingerprint_models=dict(witness_models),
witness_pole_prompt_hash_collisions=collisions,
note=("0/140 is the expected baseline pre-b9c09fd-live-app. "
"The core runtime records fingerprints (commit b9c09fd); "
"the live app will start recording them once the "
Expand Down Expand Up @@ -1575,6 +1618,16 @@ def render_report(rep: dict[str, Any]) -> str:
out.append(f" fingerprint models:")
for m, c in sorted(p['fingerprint_models'].items(), key=lambda x: -x[1]):
out.append(f" {m}: {c}")
wfp = p.get('turns_with_witness_fingerprint', 0)
out.append(f" turns with witness fp: {wfp} / {p['total_turns']} ({100*wfp/p['total_turns']:.1f}%)")
if p.get('witness_fingerprint_models'):
out.append(f" witness fingerprint models:")
for m, c in sorted(p['witness_fingerprint_models'].items(), key=lambda x: -x[1]):
out.append(f" {m}: {c}")
collisions = p.get('witness_pole_prompt_hash_collisions', 0)
if wfp:
verdict = "OK (witness prompt ≠ pole prompt)" if collisions == 0 else "BUG SIGNATURE — witness transmitted the pole prompt"
out.append(f" witness/pole hash collisions: {collisions} {verdict}")
if p.get('note'):
out.append(f" note: {p['note']}")
out.append("")
Expand Down
25 changes: 13 additions & 12 deletions Sources/BCICloudBridge/GenerationRuntimeTextGeneratingAdapter.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import BCICore
import Foundation

/// A `TextGenerating` adapter that wraps any `GenerationRuntime`
/// (and its fixed system prompt) so the existing
/// `HypnagogicDialecticLoop` — which depends on `TextGenerating`,
/// not `GenerationRuntime` — can use any of the new runtimes
/// without a loop refactor.
/// A `TextGenerating` adapter that wraps any `GenerationRuntime` so the
/// existing `HypnagogicDialecticLoop` — which depends on `TextGenerating`, not
/// `GenerationRuntime` — can use any of the runtimes without a loop refactor.
///
/// This is the *smallest* change to wire runtime selection into
/// the harness: the loop's interface stays exactly the same, and
Expand All @@ -16,14 +14,19 @@ import Foundation
/// text; it only shapes a `GenerationContext` and calls
/// `runtime.generate(prompt:context:)`).
///
/// The adapter is `Sendable` because both the wrapped runtime
/// (which is `Sendable`) and the `systemPrompt` (a `String`) are
/// safe to pass across actor boundaries; the actor isolation on
/// the loop side is unchanged.
/// **This adapter does not carry a system prompt.** It once had a
/// `systemPrompt` field that was stored and never read: the bytes actually
/// transmitted come from the wrapped runtime's own prompt. That made the field
/// worse than useless — it let a caller construct a "Witness" adapter around
/// the *pole* runtime, pass the Witness prompt into the dead field, and get an
/// object that reported one prompt while sending another. Removing it forces
/// role-specific prompts to be resolved where they take effect: in the runtime.
///
/// The adapter is `Sendable` because the wrapped runtime is; the actor
/// isolation on the loop side is unchanged.
public struct GenerationRuntimeTextGeneratingAdapter: MetadataPublishingTextGenerating {
public nonisolated let isLive: Bool
public nonisolated let modelIdentifier: String
public nonisolated let systemPrompt: String

private let runtime: any GenerationRuntime
private let maxTokens: Int
Expand Down Expand Up @@ -59,12 +62,10 @@ public struct GenerationRuntimeTextGeneratingAdapter: MetadataPublishingTextGene

public init(
runtime: any GenerationRuntime,
systemPrompt: String,
maxTokens: Int = 256,
defaultTemperature: Double = 0.7
) {
self.runtime = runtime
self.systemPrompt = systemPrompt
self.isLive = runtime.isLive
self.modelIdentifier = runtime.modelIdentifier
self.maxTokens = maxTokens
Expand Down
Loading
Loading