Skip to content

Commit 3c778b2

Browse files
Miley ChandonnetMiley Chandonnet
authored andcommitted
Merge remote-tracking branch 'origin/main' into miley/ampr-171-cognitivephaseevent-publish-phase-transitions-on
# Conflicts: # ampere-cli/src/jvmMain/kotlin/link/socket/ampere/cli/watch/presentation/EventCategorizer.kt # ampere-cli/src/jvmMain/kotlin/link/socket/ampere/renderer/EventRenderer.kt # ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/events/utils/SignificanceAwareEventLogger.kt # ampere-core/src/commonMain/kotlin/link/socket/ampere/api/service/EventService.kt # ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventSerializationTest.kt
2 parents c79e965 + f544bce commit 3c778b2

18 files changed

Lines changed: 587 additions & 0 deletions

File tree

ampere-cli/src/jvmMain/kotlin/link/socket/ampere/cli/watch/presentation/EventCategorizer.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package link.socket.ampere.cli.watch.presentation
22

33
import link.socket.ampere.agents.domain.event.AgentSurfaceEvent
4+
import link.socket.ampere.agents.domain.event.CognitiveEvent
45
import link.socket.ampere.agents.domain.event.CognitivePhaseEvent
56
import link.socket.ampere.agents.domain.event.Event
67
import link.socket.ampere.agents.domain.event.FileSystemEvent
@@ -53,6 +54,7 @@ object EventCategorizer {
5354
private fun categorizeInternal(event: Event): EventSignificance = when (event) {
5455
// Critical events require immediate human awareness
5556
is Event.QuestionRaised,
57+
is CognitiveEvent.EscalationFired,
5658
is TicketEvent.TicketBlocked,
5759
is MessageEvent.EscalationRequested,
5860
is HumanInteractionEvent.InputRequested,

ampere-cli/src/jvmMain/kotlin/link/socket/ampere/cli/watch/presentation/WatchPresenter.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.catch
1010
import kotlinx.coroutines.isActive
1111
import kotlinx.coroutines.launch
1212
import kotlinx.datetime.Clock
13+
import link.socket.ampere.agents.domain.event.CognitiveEvent
1314
import link.socket.ampere.agents.domain.event.CognitiveStateSnapshot
1415
import link.socket.ampere.agents.domain.event.Event
1516
import link.socket.ampere.agents.domain.event.EventSource
@@ -371,6 +372,9 @@ class WatchPresenter(
371372
"\"$content\""
372373
}
373374
is MessageEvent.EscalationRequested -> "⚠️ Escalation: ${event.reason.take(50)}"
375+
is CognitiveEvent.EscalationFired -> {
376+
"Uncertainty escalation: ${event.uncertaintyValue} >= ${event.threshold}"
377+
}
374378
is MemoryEvent.KnowledgeRecalled -> {
375379
val count = event.resultsFound
376380
if (count > 0) "Recalled $count items (relevance: ${String.format("%.0f%%", event.averageRelevance * 100)})"

ampere-cli/src/jvmMain/kotlin/link/socket/ampere/renderer/EventRenderer.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import kotlinx.datetime.TimeZone
1313
import kotlinx.datetime.toLocalDateTime
1414
import link.socket.ampere.agents.domain.Urgency
1515
import link.socket.ampere.agents.domain.event.AgentSurfaceEvent
16+
import link.socket.ampere.agents.domain.event.CognitiveEvent
1617
import link.socket.ampere.agents.domain.event.CognitivePhaseEvent
1718
import link.socket.ampere.agents.domain.event.Event
1819
import link.socket.ampere.agents.domain.event.EventSource
@@ -137,6 +138,7 @@ class EventRenderer(
137138
is Event.CodeSubmitted -> "💻" to cyan
138139
is Event.QuestionRaised -> "" to magenta
139140
is Event.TaskCreated -> "📋" to green
141+
is CognitiveEvent.EscalationFired -> "" to red
140142
is TaskEvent -> "📋" to green
141143
is FileSystemEvent -> "📄" to cyan
142144
is GitEvent -> "🪾" to cyan

ampere-cli/src/jvmTest/kotlin/link/socket/ampere/cli/watch/presentation/EventCategorizerTest.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import kotlinx.datetime.Clock
44
import link.socket.ampere.agents.domain.Urgency
55
import link.socket.ampere.agents.domain.knowledge.KnowledgeType
66
import link.socket.ampere.agents.domain.status.TicketStatus
7+
import link.socket.ampere.agents.domain.event.CognitiveEvent
78
import link.socket.ampere.agents.domain.event.Event
89
import link.socket.ampere.agents.domain.event.EventSource
910
import link.socket.ampere.agents.domain.event.MemoryEvent
@@ -67,6 +68,24 @@ class EventCategorizerTest {
6768
assertEquals(EventSignificance.CRITICAL, significance)
6869
}
6970

71+
@Test
72+
fun `CRITICAL - EscalationFired events are critical`() {
73+
val event = CognitiveEvent.EscalationFired(
74+
eventId = "evt-escalation-fired",
75+
timestamp = Clock.System.now(),
76+
eventSource = EventSource.Agent("agent-test"),
77+
urgency = Urgency.HIGH,
78+
agentId = "agent-test",
79+
uncertaintyValue = 0.9,
80+
threshold = 0.7,
81+
prompt = "Need confidence threshold escalation",
82+
cognitivePhase = null,
83+
)
84+
85+
val significance = EventCategorizer.categorize(event)
86+
assertEquals(EventSignificance.CRITICAL, significance)
87+
}
88+
7089
@Test
7190
fun `SIGNIFICANT - TaskCreated events are significant`() {
7291
val event = Event.TaskCreated(

ampere-cli/src/jvmTest/kotlin/link/socket/ampere/renderer/EventRendererTest.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import kotlinx.datetime.Clock
55
import kotlinx.datetime.Instant
66
import kotlinx.datetime.TimeZone
77
import kotlinx.datetime.toLocalDateTime
8+
import link.socket.ampere.agents.domain.event.CognitiveEvent
89
import link.socket.ampere.agents.domain.event.Event
910
import link.socket.ampere.agents.domain.event.CognitiveStateSnapshot
1011
import link.socket.ampere.agents.domain.event.EventSource
@@ -209,6 +210,23 @@ class EventRendererTest {
209210
success = success,
210211
)
211212

213+
private fun escalationFiredEvent(
214+
eventId: String = "evt-escalation-fired-1",
215+
timestamp: Instant = Clock.System.now(),
216+
source: EventSource = EventSource.Agent("agent-test"),
217+
urgency: Urgency = Urgency.HIGH,
218+
): CognitiveEvent.EscalationFired = CognitiveEvent.EscalationFired(
219+
eventId = eventId,
220+
timestamp = timestamp,
221+
eventSource = source,
222+
urgency = urgency,
223+
agentId = "agent-test",
224+
uncertaintyValue = 0.9,
225+
threshold = 0.7,
226+
prompt = "Need human decision",
227+
cognitivePhase = null,
228+
)
229+
212230
@Test
213231
fun `render TaskCreated event shows task ID, description, and assignment`() {
214232
val output = captureTerminalOutput { _, renderer ->
@@ -403,6 +421,12 @@ class EventRendererTest {
403421
renderer.render(codeEvent())
404422
}
405423
assertContains(codeOutput, "💻")
424+
425+
val escalationOutput = captureTerminalOutput { _, renderer ->
426+
renderer.render(escalationFiredEvent())
427+
}
428+
assertContains(escalationOutput, "")
429+
assertContains(escalationOutput, "EscalationFired")
406430
}
407431

408432
@Test

ampere-cli/src/jvmTest/kotlin/link/socket/ampere/util/EventTypeParserTest.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import kotlin.test.assertNotNull
55
import kotlin.test.assertNull
66
import kotlin.test.assertTrue
77
import link.socket.ampere.agents.domain.event.Event
8+
import link.socket.ampere.agents.domain.event.CognitiveEvent
89
import link.socket.ampere.agents.domain.event.MeetingEvent
910
import link.socket.ampere.agents.domain.event.MessageEvent
1011
import link.socket.ampere.agents.domain.event.NotificationEvent
@@ -46,6 +47,11 @@ class EventTypeParserTest {
4647
assertNotNull(EventTypeParser.parse("CodeSubmitted"))
4748
}
4849

50+
@Test
51+
fun `parse handles CognitiveEvent types`() {
52+
assertNotNull(EventTypeParser.parse("EscalationFired"))
53+
}
54+
4955
@Test
5056
fun `parse handles all MeetingEvent types`() {
5157
assertNotNull(EventTypeParser.parse("MeetingScheduled"))
@@ -149,6 +155,7 @@ class EventTypeParserTest {
149155
assertTrue(allTypes.contains(Event.TaskCreated.EVENT_TYPE))
150156
assertTrue(allTypes.contains(Event.QuestionRaised.EVENT_TYPE))
151157
assertTrue(allTypes.contains(Event.CodeSubmitted.EVENT_TYPE))
158+
assertTrue(allTypes.contains(CognitiveEvent.EscalationFired.EVENT_TYPE))
152159

153160
// Should have meeting events
154161
assertTrue(allTypes.contains(MeetingEvent.MeetingScheduled.EVENT_TYPE))
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package link.socket.ampere.agents.domain.event
2+
3+
import kotlinx.datetime.Instant
4+
import kotlinx.serialization.SerialName
5+
import kotlinx.serialization.Serializable
6+
import link.socket.ampere.agents.definition.AgentId
7+
import link.socket.ampere.agents.domain.Urgency
8+
import link.socket.ampere.agents.domain.cognition.sparks.CognitivePhase
9+
10+
/**
11+
* Events emitted by cognitive evaluators outside normal phase transitions.
12+
*/
13+
@Serializable
14+
sealed interface CognitiveEvent : Event {
15+
16+
val agentId: AgentId
17+
18+
/**
19+
* Emitted when an agent's uncertainty meets or exceeds its configured escalation threshold.
20+
*/
21+
@Serializable
22+
@SerialName("CognitiveEvent.EscalationFired")
23+
data class EscalationFired(
24+
override val eventId: EventId,
25+
override val timestamp: Instant,
26+
override val eventSource: EventSource,
27+
override val urgency: Urgency = Urgency.HIGH,
28+
override val agentId: AgentId,
29+
val uncertaintyValue: Double,
30+
val threshold: Double,
31+
val prompt: String,
32+
val cognitivePhase: CognitivePhase?,
33+
) : CognitiveEvent {
34+
35+
override val eventType: EventType = EVENT_TYPE
36+
37+
override fun getSummary(
38+
formatUrgency: (Urgency) -> String,
39+
formatSource: (EventSource) -> String,
40+
): String = buildString {
41+
append("Escalation fired for $agentId: uncertainty ")
42+
append(uncertaintyValue.formatRatio())
43+
append(" >= threshold ")
44+
append(threshold.formatRatio())
45+
cognitivePhase?.let { append(" [${it.name}]") }
46+
if (prompt.isNotBlank()) {
47+
append(" - ${prompt.take(80)}")
48+
if (prompt.length > 80) append("...")
49+
}
50+
append(" ${formatUrgency(urgency)}")
51+
append(" from ${formatSource(eventSource)}")
52+
}
53+
54+
companion object {
55+
const val EVENT_TYPE: EventType = "EscalationFired"
56+
}
57+
}
58+
}
59+
60+
private fun Double.formatRatio(): String {
61+
val rounded = (this * 1000.0).toInt() / 1000.0
62+
return rounded.toString()
63+
}

ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/domain/event/EventRegistry.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ object EventRegistry {
3030
Event.QuestionRaised.EVENT_TYPE,
3131
Event.CodeSubmitted.EVENT_TYPE,
3232

33+
// CognitiveEvent types
34+
CognitiveEvent.EscalationFired.EVENT_TYPE,
35+
3336
// MeetingEvent types
3437
MeetingEvent.MeetingScheduled.EVENT_TYPE,
3538
MeetingEvent.MeetingStarted.EVENT_TYPE,

ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/domain/event/MemoryEvent.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ data class RetrievedKnowledgeSummary(
3535
*/
3636
sealed interface MemoryEvent : Event {
3737

38+
// TODO(AMPR-176): Add `ProvenanceCommitted` here when the provenance-chain
39+
// surface is promoted from design placeholder to implementation. Design
40+
// sketch in docs/design/provenance-event.md covers event shape
41+
// (entryId / parentHash / contentHash / signer), hash/signature scheme,
42+
// publish-site choice, and the relationship to the OpenAI grant's
43+
// formal-verification workstream. Do not implement here without first
44+
// satisfying one of the promotion criteria in that doc.
45+
3846
/**
3947
* Event emitted when a Knowledge entry is successfully stored.
4048
*

ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/events/api/AgentEventApi.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import kotlinx.datetime.Instant
55
import link.socket.ampere.agents.config.AgentActionAutonomy
66
import link.socket.ampere.agents.definition.AgentId
77
import link.socket.ampere.agents.domain.Urgency
8+
import link.socket.ampere.agents.domain.event.CognitiveEvent
89
import link.socket.ampere.agents.domain.event.Event
910
import link.socket.ampere.agents.domain.event.EventSource
1011
import link.socket.ampere.agents.domain.event.EventType
@@ -198,6 +199,20 @@ class AgentEventApi(
198199
}
199200
}
200201

202+
/** Subscribe to threshold-driven cognitive escalation events. */
203+
fun onEscalationFired(
204+
filter: EventFilter<CognitiveEvent.EscalationFired> = EventFilter.noFilter(),
205+
handler: suspend (CognitiveEvent.EscalationFired, Subscription?) -> Unit,
206+
): Subscription =
207+
eventSerialBus.subscribe<CognitiveEvent.EscalationFired, EventSubscription.ByEventClassType>(
208+
agentId = agentId,
209+
eventType = CognitiveEvent.EscalationFired.EVENT_TYPE,
210+
) { event, subscription ->
211+
if (filter.execute(event)) {
212+
handler(event, subscription)
213+
}
214+
}
215+
201216
/** Retrieve all events since the provided timestamp, or all if null. */
202217
suspend fun getRecentEvents(
203218
since: Instant?,

0 commit comments

Comments
 (0)