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
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ class StatusCommand(
terminal.println()
terminal.println(bold("📁 Workspace"))
val workspace = link.socket.ampere.agents.environment.workspace.defaultWorkspace()
val workspacePath = workspace?.baseDirectory ?: "disabled"
val workspacePath = workspace.baseDirectory
terminal.println(" ${cyan(workspacePath)} ${gray("(watching)")}")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ data class CoordinationViewState(
val focusedAgentId: AgentId?,
)

private data class PresenterControls(
val subMode: CoordinationSubMode,
val verbose: Boolean,
val focusedAgentId: AgentId?,
val selectedMeetingId: String?,
)

/**
* Provider interface for coordination state.
*
Expand Down Expand Up @@ -119,29 +126,33 @@ class CoordinationPresenter(
init {
// Combine coordination state and agent states to produce view state
collectJob = scope.launch {
combine(
coordinationStateProvider.state,
agentStateProvider.agentStates,
val controls = combine(
_subMode,
_verbose,
_focusedAgentId,
_selectedMeetingId,
) { flows: Array<Any?> ->
val coordinationState = flows[0] as CoordinationState
val agentStates = flows[1] as Map<AgentId, String>
val subMode = flows[2] as CoordinationSubMode
val verbose = flows[3] as Boolean
val focusedAgentId = flows[4] as AgentId?
val selectedMeetingId = flows[5] as String?

updateViewState(
coordinationState = coordinationState,
agentStates = agentStates,
) { subMode, verbose, focusedAgentId, selectedMeetingId ->
PresenterControls(
subMode = subMode,
verbose = verbose,
focusedAgentId = focusedAgentId,
selectedMeetingId = selectedMeetingId,
)
}

combine(
coordinationStateProvider.state,
agentStateProvider.agentStates,
controls,
) { coordinationState, agentStates, controls ->
updateViewState(
coordinationState = coordinationState,
agentStates = agentStates,
subMode = controls.subMode,
verbose = controls.verbose,
focusedAgentId = controls.focusedAgentId,
selectedMeetingId = controls.selectedMeetingId,
)
}.collect { newState ->
_viewState.value = newState
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,11 @@ class TopologyLayoutCalculator {
val reverseEdge = state.edges.find {
it.sourceAgentId == target && it.targetAgentId == source
}

val isBidirectional = reverseEdge != null

// Combine interaction types from both directions if bidirectional
val interactionTypes = if (isBidirectional) {
coordinationEdge.interactionTypes + (reverseEdge?.interactionTypes ?: emptySet())
val interactionTypes = if (reverseEdge != null) {
coordinationEdge.interactionTypes + reverseEdge.interactionTypes
} else {
coordinationEdge.interactionTypes
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ class WaveformCellAdapterTest {

val cell = buffer.getCell(0, 0)
assertEquals('#', cell.char)
assertNotNull(cell.ansiColor)
assertTrue(cell.ansiColor!!.contains("38;5;196"))
val ansiColor = assertNotNull(cell.ansiColor)
assertTrue(ansiColor.contains("38;5;196"))
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package link.socket.ampere.cli.watch.presentation
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

Expand Down Expand Up @@ -54,10 +55,10 @@ class SparkPresentationTest {
assertEquals(1, history.size)
assertEquals(SparkTransitionDirection.REMOVED, history.first().direction)

val state = collector.getCognitiveState(agentId)
assertEquals("OPERATIONAL", state?.affinityName)
assertTrue(state?.sparkNames?.isEmpty() == true)
assertEquals(0, state?.depth)
val state = assertNotNull(collector.getCognitiveState(agentId))
assertEquals("OPERATIONAL", state.affinityName)
assertTrue(state.sparkNames.isEmpty())
assertEquals(0, state.depth)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,18 @@ class McpConfigTest {

val config = ConfigParser.parse(yaml)

assertNotNull(config.mcp)
assertEquals(2, config.mcp!!.servers.size)
val mcp = assertNotNull(config.mcp)
assertEquals(2, mcp.servers.size)

val github = config.mcp!!.servers[0]
val github = mcp.servers[0]
assertEquals("github", github.id)
assertEquals("GitHub CLI", github.name)
assertEquals("stdio", github.protocol)
assertEquals("/usr/local/bin/github-mcp-server", github.endpoint)
assertNull(github.authToken)
assertEquals("act-with-notification", github.autonomy)

val database = config.mcp!!.servers[1]
val database = mcp.servers[1]
assertEquals("database", database.id)
assertEquals("Database Service", database.name)
assertEquals("http", database.protocol)
Expand Down
28 changes: 16 additions & 12 deletions ampere-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)

import com.vanniktech.maven.publish.JavadocJar
import com.vanniktech.maven.publish.KotlinMultiplatform
Expand Down Expand Up @@ -25,6 +26,7 @@ compose.resources {
}

val ampereVersion: String by project
val composeVersion = findProperty("compose.version") as String

group = "link.socket"
version = ampereVersion
Expand Down Expand Up @@ -87,6 +89,10 @@ sqldelight {
kotlin {
applyDefaultHierarchyTemplate()

compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
}

androidTarget {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
Expand Down Expand Up @@ -132,19 +138,15 @@ kotlin {
}

sourceSets {
all {
languageSettings.enableLanguageFeature("ExpectActualClasses")
}

val commonMain by getting {
dependencies {
implementation(kotlin("reflect"))

implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.components.resources)
implementation(compose.materialIconsExtended)
implementation("org.jetbrains.compose.runtime:runtime:$composeVersion")
implementation("org.jetbrains.compose.foundation:foundation:$composeVersion")
implementation("org.jetbrains.compose.material:material:$composeVersion")
implementation("org.jetbrains.compose.components:components-resources:$composeVersion")
implementation("org.jetbrains.compose.material:material-icons-extended:1.7.3")

implementation("ai.koog:koog-agents:0.5.4")
implementation("app.cash.sqldelight:coroutines-extensions:2.2.1")
Expand All @@ -166,7 +168,7 @@ kotlin {
val androidMain by getting {
dependencies {
implementation(project(":ampere-compose"))
implementation(compose.uiTooling)
implementation("org.jetbrains.compose.ui:ui-tooling:$composeVersion")

api("androidx.activity:activity-compose:1.11.0")
api("androidx.appcompat:appcompat:1.7.1")
Expand All @@ -178,7 +180,7 @@ kotlin {
}
val jvmMain by getting {
dependencies {
implementation(compose.desktop.common)
implementation("org.jetbrains.compose.desktop:desktop-jvm:$composeVersion")

implementation("app.cash.sqldelight:sqlite-driver:2.2.1")
implementation("com.charleskorn.kaml:kaml:0.72.0")
Expand Down Expand Up @@ -224,7 +226,9 @@ kotlin {

// https://kotlinlang.org/docs/native-objc-interop.html#export-of-kdoc-comments-to-generated-objective-c-headers
targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
compilations.get("main").compilerOptions.options.freeCompilerArgs.add("-Xexport-kdoc")
compilations.get("main").compileTaskProvider.configure {
compilerOptions.freeCompilerArgs.add("-Xexport-kdoc")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,6 @@ abstract class AutonomousAgent<S : AgentState> : Agent<S>, NeuralAgent<S> {
private var agentRuntimeScope: CoroutineScope? = null
private var agentRuntimeLoopJob: Job? = null

@Transient
private val phaseSparkManager: PhaseSparkManager<S> by lazy(LazyThreadSafetyMode.NONE) {
PhaseSparkManager.create(this, agentConfiguration.cognitiveConfig.phaseSparks)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,3 @@ data class EscalationDecision(
CRITICAL,
}
}

/**
* Compare urgency levels for ordering.
*/
internal operator fun EscalationDecision.UrgencyLevel.compareTo(
other: EscalationDecision.UrgencyLevel,
): Int = this.ordinal.compareTo(other.ordinal)
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class FunctionExecutor(
executorId = id,
timestamp = Clock.System.now(),
result = createFailureOutcome(
request = request as ExecutionRequest<ExecutionContext>,
request = request,
startTime = startTime,
error = ExecutionError(
type = ExecutionError.Type.TOOL_UNAVAILABLE,
Expand Down Expand Up @@ -126,11 +126,7 @@ class FunctionExecutor(
logger.i { "Executing FunctionTool '${tool.name}'" }

// Execute the tool
// Cast is safe because we validated tool is FunctionTool above
@Suppress("UNCHECKED_CAST")
val outcome = (tool as FunctionTool<ExecutionContext>).execute(
request as ExecutionRequest<ExecutionContext>,
)
val outcome = tool.executeWith(request)

// Emit appropriate status based on outcome type
when (outcome) {
Expand All @@ -154,25 +150,7 @@ class FunctionExecutor(
),
)
}
else -> {
logger.w { "Unexpected outcome type: ${outcome::class.simpleName}" }
emit(
ExecutionStatus.Failed(
executorId = id,
timestamp = Clock.System.now(),
result = createFailureOutcome(
request = request as ExecutionRequest<ExecutionContext>,
startTime = startTime,
error = ExecutionError(
type = ExecutionError.Type.UNEXPECTED,
message = "Unexpected outcome type",
details = outcome::class.simpleName,
isRetryable = false,
),
),
),
)
}
else -> emitUnexpectedOutcomeFailure(request, startTime, outcome)
}
} catch (e: Exception) {
// Catch any unexpected exceptions and convert to Failed status
Expand All @@ -182,7 +160,7 @@ class FunctionExecutor(
executorId = id,
timestamp = Clock.System.now(),
result = createFailureOutcome(
request = request as ExecutionRequest<ExecutionContext>,
request = request,
startTime = startTime,
error = ExecutionError(
type = ExecutionError.Type.UNEXPECTED,
Expand All @@ -203,7 +181,7 @@ class FunctionExecutor(
* different error scenarios.
*/
private fun createFailureOutcome(
request: ExecutionRequest<ExecutionContext>,
request: ExecutionRequest<*>,
startTime: kotlinx.datetime.Instant,
error: ExecutionError,
): ExecutionOutcome.NoChanges.Failure {
Expand All @@ -217,6 +195,37 @@ class FunctionExecutor(
)
}

private suspend fun FunctionTool<*>.executeWith(
request: ExecutionRequest<*>,
): link.socket.ampere.agents.domain.outcome.Outcome {
@Suppress("UNCHECKED_CAST")
return (this as FunctionTool<ExecutionContext>).execute(request as ExecutionRequest<ExecutionContext>)
}

private suspend fun kotlinx.coroutines.flow.FlowCollector<ExecutionStatus>.emitUnexpectedOutcomeFailure(
request: ExecutionRequest<*>,
startTime: kotlinx.datetime.Instant,
outcome: link.socket.ampere.agents.domain.outcome.Outcome,
) {
logger.w { "Unexpected outcome type: ${outcome::class.simpleName}" }
emit(
ExecutionStatus.Failed(
executorId = id,
timestamp = Clock.System.now(),
result = createFailureOutcome(
request = request,
startTime = startTime,
error = ExecutionError(
type = ExecutionError.Type.UNEXPECTED,
message = "Unexpected outcome type",
details = outcome::class.simpleName,
isRetryable = false,
),
),
),
)
}

companion object {
/**
* Default function executor ID
Expand Down
Loading
Loading