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
2 changes: 1 addition & 1 deletion ampere-cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ kotlin {
val jvmMain by getting {
dependencies {
implementation(project(":ampere-core"))
implementation("link.socket:phosphor-core:0.2.3")
implementation("link.socket:phosphor-core:0.3.0")

// CLI argument parsing
implementation("com.github.ajalt.clikt:clikt:4.4.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.runBlocking
import link.socket.phosphor.choreography.AgentLayer
import link.socket.phosphor.choreography.AgentLayoutOrientation
import link.socket.phosphor.bridge.CognitiveEmitterBridge
import link.socket.phosphor.emitter.EmitterManager
import link.socket.phosphor.field.FlowLayer
import link.socket.phosphor.field.SubstrateState
import link.socket.phosphor.timeline.PlaybackState
import link.socket.phosphor.timeline.TimelineController
import link.socket.phosphor.timeline.TimelineEvent
import link.socket.phosphor.timeline.WaveformDemoTimeline
import link.socket.ampere.cli.render.AmperePhosphorBridge
import link.socket.ampere.cli.render.WaveformPaneRenderer
import link.socket.ampere.repl.TerminalFactory

Expand Down Expand Up @@ -68,14 +68,14 @@ class WaveformDemoSubcommand : CliktCommand(
val agents = AgentLayer(width, contentHeight, AgentLayoutOrientation.CIRCULAR)
val flow = FlowLayer(width, contentHeight)
val emitters = EmitterManager()
val emitterBridge = CognitiveEmitterBridge(emitters)
val emitterBridge = AmperePhosphorBridge(emitters)
val substrate = SubstrateState.create(width, contentHeight, baseDensity = 0.2f)

// Create waveform renderer
val waveformPane = WaveformPaneRenderer(
agentLayer = agents,
emitterManager = emitters,
cognitiveEmitterBridge = emitterBridge
amperePhosphorBridge = emitterBridge
)

// Build timeline and controller
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package link.socket.ampere.cli.hybrid

import com.github.ajalt.mordant.terminal.Terminal
import link.socket.phosphor.bridge.CognitiveEmitterBridge
import link.socket.phosphor.emitter.EmitterManager
import link.socket.phosphor.field.ParticleSystem
import link.socket.ampere.cli.animation.render.AmperePalette
import link.socket.ampere.cli.animation.render.CompositeRenderer
import link.socket.ampere.cli.render.AmperePhosphorBridge
import link.socket.phosphor.field.SubstrateAnimator
import link.socket.phosphor.field.SubstrateGlyphs
import link.socket.phosphor.field.SubstrateState
Expand Down Expand Up @@ -67,7 +67,7 @@ class HybridDashboardRenderer(

// 3D waveform pipeline
private lateinit var emitterManager: EmitterManager
private lateinit var cognitiveEmitterBridge: CognitiveEmitterBridge
private lateinit var amperePhosphorBridge: AmperePhosphorBridge

/**
* The waveform pane renderer for the middle pane. Callers can pass this
Expand Down Expand Up @@ -149,19 +149,22 @@ class HybridDashboardRenderer(

// Initialize waveform pipeline
emitterManager = EmitterManager()
cognitiveEmitterBridge = CognitiveEmitterBridge(emitterManager)
amperePhosphorBridge = AmperePhosphorBridge(emitterManager)

if (config.enableWaveform) {
val wfPane = WaveformPaneRenderer(
agentLayer = bridge.agentLayer,
emitterManager = emitterManager,
cognitiveEmitterBridge = cognitiveEmitterBridge
amperePhosphorBridge = amperePhosphorBridge
)
waveformPane = wfPane

// Wire cognitive events from bridge to emitter bridge
bridge.onCognitiveEvent = { event, position ->
cognitiveEmitterBridge.onCognitiveEvent(event, position)
amperePhosphorBridge.onCognitiveEvent(event, position)
}
bridge.onProviderTelemetry = { telemetry, position ->
amperePhosphorBridge.onProviderCallCompleted(telemetry, position)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import link.socket.phosphor.field.SubstrateState
import link.socket.phosphor.math.Vector2
import link.socket.ampere.cli.watch.presentation.AgentState
import link.socket.ampere.cli.watch.presentation.EventSignificance
import link.socket.ampere.cli.watch.presentation.ProviderCallTelemetrySummary
import link.socket.ampere.cli.watch.presentation.WatchViewState

/**
Expand All @@ -44,6 +45,7 @@ class WatchStateAnimationBridge(
private val maxParticles: Int = 30
) {
private var previousEventCount = 0
private var previousProviderTelemetryIds = emptySet<String>()
private var previousAgentStates = mapOf<String, AgentState>()
private val burstEmitter = BurstEmitter()

Expand All @@ -69,6 +71,11 @@ class WatchStateAnimationBridge(
*/
var onCognitiveEvent: ((CognitiveEvent, Vector3) -> Unit)? = null

/**
* Callback invoked when provider telemetry should generate metadata-rich emitters.
*/
var onProviderTelemetry: ((ProviderCallTelemetrySummary, Vector3) -> Unit)? = null

/**
* Update animation state based on current watch state.
*
Expand All @@ -89,14 +96,17 @@ class WatchStateAnimationBridge(
// Update substrate hotspots based on active agents
var result = updateHotspotsFromAgentActivity(viewState, substrate)

// Sync agent positions before event-driven emitters use them.
syncAgentLayer(viewState)

// Detect new significant events and spawn particles
detectNewEvents(viewState)
detectProviderTelemetry(viewState)

// Detect agent state transitions and trigger pulses
detectStateTransitions(viewState, result)?.let { result = it }

// Sync agent layer from view state and run choreographer
syncAgentLayer(viewState)
// Run phase-based choreography on the synchronized agent layer
result = choreographer.update(agentLayer, result, deltaSeconds)

// Apply ambient animation
Expand All @@ -110,6 +120,8 @@ class WatchStateAnimationBridge(

// Track state for next frame
previousEventCount = viewState.recentSignificantEvents.size
previousProviderTelemetryIds = viewState.recentProviderTelemetry
.mapTo(linkedSetOf()) { it.eventId }
previousAgentStates = viewState.agentStates.mapValues { it.value.currentState }

return result
Expand Down Expand Up @@ -259,6 +271,18 @@ class WatchStateAnimationBridge(
}
}

private fun detectProviderTelemetry(viewState: WatchViewState) {
if (viewState.recentProviderTelemetry.isEmpty()) return

viewState.recentProviderTelemetry
.asReversed()
.filter { it.eventId !in previousProviderTelemetryIds }
.forEach { telemetry ->
val agentPos = agentLayer.getAgent(telemetry.agentId)?.position3D ?: Vector3.ZERO
onProviderTelemetry?.invoke(telemetry, agentPos)
}
}

private fun detectStateTransitions(
viewState: WatchViewState,
substrate: SubstrateState
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package link.socket.ampere.cli.render

import link.socket.ampere.agents.domain.cognition.sparks.CognitivePhase as AmperePhase
import link.socket.ampere.cli.watch.presentation.ProviderCallTelemetrySummary
import link.socket.phosphor.bridge.CognitiveEmitterBridge
import link.socket.phosphor.bridge.CognitiveEvent
import link.socket.phosphor.emitter.EmitterEffect
import link.socket.phosphor.emitter.EmitterManager
import link.socket.phosphor.emitter.MetadataKeys
import link.socket.phosphor.math.Vector3
import link.socket.phosphor.palette.AsciiLuminancePalette
import link.socket.phosphor.palette.CognitiveColorRamp
import link.socket.phosphor.signal.CognitivePhase as PhosphorPhase

/**
* Preserves the existing cognitive emitter choreography and adds Ampere-specific
* provider-call metadata channels for Phosphor 0.3.0.
*/
class AmperePhosphorBridge(
private val emitterManager: EmitterManager,
private val cognitiveEmitterBridge: CognitiveEmitterBridge = CognitiveEmitterBridge(emitterManager)
) {
fun onCognitiveEvent(event: CognitiveEvent, agentPosition: Vector3) {
cognitiveEmitterBridge.onCognitiveEvent(event, agentPosition)
}

fun onProviderCallCompleted(
event: ProviderCallTelemetrySummary,
agentPosition: Vector3,
currentTime: Float = 0f
) {
emitterManager.emit(
effect = effectForPhase(event.cognitivePhase),
position = agentPosition,
currentTime = currentTime,
metadata = metadataFor(event)
)
}

internal fun metadataFor(event: ProviderCallTelemetrySummary): Map<String, Float> = buildMap {
event.estimatedCost?.let { put(MetadataKeys.HEAT, CostNormalizer.normalizeCost(it)) }
event.totalTokens?.let { put(MetadataKeys.DENSITY, CostNormalizer.normalizeTokens(it)) }
put(MetadataKeys.INTENSITY, CostNormalizer.normalizeLatency(event.latencyMs))
}

private fun effectForPhase(phase: AmperePhase?): EmitterEffect = when (phase) {
AmperePhase.PERCEIVE -> EmitterEffect.HeightPulse()
AmperePhase.PLAN -> EmitterEffect.ColorWash(
colorRamp = CognitiveColorRamp.forPhase(PhosphorPhase.PLAN)
)
AmperePhase.EXECUTE -> EmitterEffect.SparkBurst(
palette = AsciiLuminancePalette.EXECUTE
)
AmperePhase.LEARN -> EmitterEffect.ColorWash(
colorRamp = CognitiveColorRamp.forPhase(PhosphorPhase.EVALUATE)
)
null -> EmitterEffect.HeightPulse()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package link.socket.ampere.cli.render

import kotlin.math.log10

/**
* Normalizes provider telemetry into 0.0-1.0 metadata channels for Phosphor.
*/
object CostNormalizer {
fun normalizeCost(costUsd: Double): Float {
if (costUsd <= 0.0) return 0f
return normalizeLog1p(value = costUsd, pivot = 0.0001, ceiling = 0.2)
}

fun normalizeLatency(latencyMs: Long): Float {
if (latencyMs <= 0L) return 0f
return normalizeLog1p(value = latencyMs.toDouble(), pivot = 100.0, ceiling = 15_000.0)
}

fun normalizeTokens(totalTokens: Int): Float {
if (totalTokens <= 0) return 0f
return normalizeLog1p(value = totalTokens.toDouble(), pivot = 250.0, ceiling = 150_000.0)
}

private fun normalizeLog1p(
value: Double,
pivot: Double,
ceiling: Double
): Float {
val capped = value.coerceIn(0.0, ceiling)
if (capped == 0.0) return 0f

val normalized = log10(1.0 + capped / pivot) / log10(1.0 + ceiling / pivot)
return normalized.toFloat().coerceIn(0f, 1f)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package link.socket.ampere.cli.render

import link.socket.phosphor.choreography.AgentLayer
import link.socket.phosphor.choreography.AgentLayoutOrientation
import link.socket.phosphor.bridge.CognitiveEmitterBridge
import link.socket.phosphor.bridge.CognitiveEvent
import link.socket.phosphor.emitter.EmitterManager
import link.socket.phosphor.field.FlowLayer
Expand Down Expand Up @@ -33,7 +32,7 @@ import link.socket.ampere.cli.layout.PaneRenderer
class WaveformPaneRenderer(
private val agentLayer: AgentLayer,
private val emitterManager: EmitterManager,
private val cognitiveEmitterBridge: CognitiveEmitterBridge
private val amperePhosphorBridge: AmperePhosphorBridge
) : PaneRenderer {

private var cameraOrbit = CameraOrbit(
Expand Down Expand Up @@ -79,7 +78,7 @@ class WaveformPaneRenderer(
* Fire a cognitive event through the emitter bridge.
*/
fun onCognitiveEvent(event: CognitiveEvent, agentPosition: Vector3) {
cognitiveEmitterBridge.onCognitiveEvent(event, agentPosition)
amperePhosphorBridge.onCognitiveEvent(event, agentPosition)
}

override fun render(width: Int, height: Int): List<String> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import link.socket.ampere.agents.domain.event.MeetingEvent
import link.socket.ampere.agents.domain.event.MemoryEvent
import link.socket.ampere.agents.domain.event.MessageEvent
import link.socket.ampere.agents.domain.event.ProductEvent
import link.socket.ampere.agents.domain.event.ProviderCallCompletedEvent
import link.socket.ampere.agents.domain.event.SparkAppliedEvent
import link.socket.ampere.agents.domain.event.SparkEvent
import link.socket.ampere.agents.domain.event.SparkRemovedEvent
Expand Down Expand Up @@ -45,6 +46,7 @@ class WatchPresenter(
// Mutable state - only modified by event handler
private val agentStates = mutableMapOf<String, AgentActivityState>()
private val significantEvents = mutableListOf<SignificantEventSummary>()
private val providerTelemetry = mutableListOf<ProviderCallTelemetrySummary>()
private var systemVitals = SystemVitals()

private var eventCollectionJob: Job? = null
Expand Down Expand Up @@ -98,6 +100,10 @@ class WatchPresenter(
addSignificantEvent(event, significance)
}

if (event is ProviderCallCompletedEvent) {
addProviderTelemetry(agentId, event)
}

// Update system vitals
updateSystemVitals(significance)

Expand Down Expand Up @@ -229,6 +235,31 @@ class WatchPresenter(
invalidateCache()
}

private fun addProviderTelemetry(agentId: String, event: ProviderCallCompletedEvent) {
val totalTokens = listOfNotNull(event.usage.inputTokens, event.usage.outputTokens)
.takeIf { it.isNotEmpty() }
?.sum()

providerTelemetry.add(
0,
ProviderCallTelemetrySummary(
eventId = event.eventId,
agentId = agentId,
cognitivePhase = event.cognitivePhase,
latencyMs = event.latencyMs,
estimatedCost = event.usage.estimatedCost,
totalTokens = totalTokens,
success = event.success
)
)

while (providerTelemetry.size > 20) {
providerTelemetry.removeLast()
}

invalidateCache()
}

private fun updateSystemVitals(significance: EventSignificance) {
val activeCount = agentStates.count { !it.value.isIdle }
val hasCritical = significance == EventSignificance.CRITICAL
Expand Down Expand Up @@ -372,7 +403,8 @@ class WatchPresenter(
val viewState = WatchViewState(
systemVitals = systemVitals,
agentStates = agentStates.toMap(), // Immutable copy
recentSignificantEvents = significantEvents.toList() // Immutable copy
recentSignificantEvents = significantEvents.toList(), // Immutable copy
recentProviderTelemetry = providerTelemetry.toList()
)

cachedViewState = viewState
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package link.socket.ampere.cli.watch.presentation

import kotlinx.datetime.Instant
import link.socket.ampere.agents.domain.cognition.sparks.CognitivePhase

/**
* Immutable snapshot of watch state for rendering.
*/
data class WatchViewState(
val systemVitals: SystemVitals,
val agentStates: Map<String, AgentActivityState>,
val recentSignificantEvents: List<SignificantEventSummary>
val recentSignificantEvents: List<SignificantEventSummary>,
val recentProviderTelemetry: List<ProviderCallTelemetrySummary> = emptyList()
)

/**
Expand All @@ -22,3 +24,16 @@ data class SignificantEventSummary(
val summaryText: String,
val significance: EventSignificance
)

/**
* Telemetry summary for bridging provider calls into Phosphor emitter metadata.
*/
data class ProviderCallTelemetrySummary(
val eventId: String,
val agentId: String,
val cognitivePhase: CognitivePhase?,
val latencyMs: Long,
val estimatedCost: Double?,
val totalTokens: Int?,
val success: Boolean
)
Loading