Skip to content

Commit e9c7888

Browse files
Miley ChandonnetMiley Chandonnet
authored andcommitted
Merge remote-tracking branch 'origin/main' into miley/ampr-175-memoryeventmilestonereached-sibling-event-for-significant
# Conflicts: # ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventSerializationTest.kt
2 parents ec0cc04 + f544bce commit e9c7888

18 files changed

Lines changed: 588 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.Event
56
import link.socket.ampere.agents.domain.event.FileSystemEvent
67
import link.socket.ampere.agents.domain.event.GitEvent
@@ -52,6 +53,7 @@ object EventCategorizer {
5253
private fun categorizeInternal(event: Event): EventSignificance = when (event) {
5354
// Critical events require immediate human awareness
5455
is Event.QuestionRaised,
56+
is CognitiveEvent.EscalationFired,
5557
is TicketEvent.TicketBlocked,
5658
is MessageEvent.EscalationRequested,
5759
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.Event
1718
import link.socket.ampere.agents.domain.event.EventSource
1819
import link.socket.ampere.agents.domain.event.FileSystemEvent
@@ -134,6 +135,7 @@ class EventRenderer(
134135
is Event.CodeSubmitted -> "💻" to cyan
135136
is Event.QuestionRaised -> "" to magenta
136137
is Event.TaskCreated -> "📋" to green
138+
is CognitiveEvent.EscalationFired -> "" to red
137139
is TaskEvent -> "📋" to green
138140
is FileSystemEvent -> "📄" to cyan
139141
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
@@ -68,6 +69,24 @@ class EventCategorizerTest {
6869
assertEquals(EventSignificance.CRITICAL, significance)
6970
}
7071

72+
@Test
73+
fun `CRITICAL - EscalationFired events are critical`() {
74+
val event = CognitiveEvent.EscalationFired(
75+
eventId = "evt-escalation-fired",
76+
timestamp = Clock.System.now(),
77+
eventSource = EventSource.Agent("agent-test"),
78+
urgency = Urgency.HIGH,
79+
agentId = "agent-test",
80+
uncertaintyValue = 0.9,
81+
threshold = 0.7,
82+
prompt = "Need confidence threshold escalation",
83+
cognitivePhase = null,
84+
)
85+
86+
val significance = EventCategorizer.categorize(event)
87+
assertEquals(EventSignificance.CRITICAL, significance)
88+
}
89+
7190
@Test
7291
fun `SIGNIFICANT - TaskCreated events are significant`() {
7392
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.MemoryEvent
1011
import link.socket.ampere.agents.domain.event.MessageEvent
@@ -47,6 +48,11 @@ class EventTypeParserTest {
4748
assertNotNull(EventTypeParser.parse("CodeSubmitted"))
4849
}
4950

51+
@Test
52+
fun `parse handles CognitiveEvent types`() {
53+
assertNotNull(EventTypeParser.parse("EscalationFired"))
54+
}
55+
5056
@Test
5157
fun `parse handles all MeetingEvent types`() {
5258
assertNotNull(EventTypeParser.parse("MeetingScheduled"))
@@ -158,6 +164,7 @@ class EventTypeParserTest {
158164
assertTrue(allTypes.contains(Event.TaskCreated.EVENT_TYPE))
159165
assertTrue(allTypes.contains(Event.QuestionRaised.EVENT_TYPE))
160166
assertTrue(allTypes.contains(Event.CodeSubmitted.EVENT_TYPE))
167+
assertTrue(allTypes.contains(CognitiveEvent.EscalationFired.EVENT_TYPE))
161168

162169
// Should have meeting events
163170
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
@@ -37,6 +37,14 @@ data class RetrievedKnowledgeSummary(
3737
*/
3838
sealed interface MemoryEvent : Event {
3939

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

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
@@ -206,6 +207,20 @@ class AgentEventApi(
206207
}
207208
}
208209

210+
/** Subscribe to threshold-driven cognitive escalation events. */
211+
fun onEscalationFired(
212+
filter: EventFilter<CognitiveEvent.EscalationFired> = EventFilter.noFilter(),
213+
handler: suspend (CognitiveEvent.EscalationFired, Subscription?) -> Unit,
214+
): Subscription =
215+
eventSerialBus.subscribe<CognitiveEvent.EscalationFired, EventSubscription.ByEventClassType>(
216+
agentId = agentId,
217+
eventType = CognitiveEvent.EscalationFired.EVENT_TYPE,
218+
) { event, subscription ->
219+
if (filter.execute(event)) {
220+
handler(event, subscription)
221+
}
222+
}
223+
209224
/** Retrieve all events since the provided timestamp, or all if null. */
210225
suspend fun getRecentEvents(
211226
since: Instant?,

0 commit comments

Comments
 (0)