diff --git a/core/artifacts/build.gradle b/core/artifacts/build.gradle index 29c06aaa..2c9f9705 100644 --- a/core/artifacts/build.gradle +++ b/core/artifacts/build.gradle @@ -6,4 +6,5 @@ plugins { dependencies { implementation(project(":core:events")) + testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" } diff --git a/core/artifacts/src/main/kotlin/com/correx/core/artifacts/MaterializingArtifactWriter.kt b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/MaterializingArtifactWriter.kt new file mode 100644 index 00000000..abc47e20 --- /dev/null +++ b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/MaterializingArtifactWriter.kt @@ -0,0 +1,14 @@ +package com.correx.core.artifacts + +import com.correx.core.artifacts.kind.FileWrittenArtifact +import com.correx.core.artifacts.kind.FileWrittenPayload +import java.nio.file.Path + +interface MaterializingArtifactWriter { + suspend fun materialize(payload: FileWrittenPayload, sandboxRoot: Path): MaterializationResult +} + +sealed interface MaterializationResult { + data class Success(val artifact: FileWrittenArtifact, val resolvedPath: Path) : MaterializationResult + data class Failure(val reason: String) : MaterializationResult +} diff --git a/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/ArtifactKind.kt b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/ArtifactKind.kt new file mode 100644 index 00000000..06a5b049 --- /dev/null +++ b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/ArtifactKind.kt @@ -0,0 +1,10 @@ +package com.correx.core.artifacts.kind + +import kotlinx.serialization.KSerializer + +interface ArtifactKind { + val id: String + val payloadSerializer: KSerializer<*> + fun deriveJsonSchema(): JsonSchema + val llmEmitted: Boolean +} diff --git a/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/ArtifactKindRegistry.kt b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/ArtifactKindRegistry.kt new file mode 100644 index 00000000..bf07cc20 --- /dev/null +++ b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/ArtifactKindRegistry.kt @@ -0,0 +1,21 @@ +package com.correx.core.artifacts.kind + +interface ArtifactKindRegistry { + fun register(kind: ArtifactKind) + fun get(id: String): ArtifactKind? +} + +class DefaultArtifactKindRegistry : ArtifactKindRegistry { + private val kinds: MutableMap = mutableMapOf() + + init { + register(FileWrittenKind) + register(ProcessResultKind) + } + + override fun register(kind: ArtifactKind) { + kinds[kind.id] = kind + } + + override fun get(id: String): ArtifactKind? = kinds[id] +} diff --git a/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/FileWrittenKind.kt b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/FileWrittenKind.kt new file mode 100644 index 00000000..451c57cf --- /dev/null +++ b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/FileWrittenKind.kt @@ -0,0 +1,20 @@ +package com.correx.core.artifacts.kind + +import kotlinx.serialization.KSerializer + +object FileWrittenKind : ArtifactKind { + override val id: String = "file_written" + override val payloadSerializer: KSerializer<*> = FileWrittenPayload.serializer() + override val llmEmitted: Boolean = false + + override fun deriveJsonSchema(): JsonSchema = JsonSchema( + type = "object", + properties = mapOf( + "path" to JsonSchemaProperty(type = "string", description = "Relative file path, no .. components"), + "content" to JsonSchemaProperty(type = "string", description = "File content"), + "mode" to JsonSchemaProperty(type = "string", description = "Unix file mode", default = "0644"), + ), + required = listOf("path", "content"), + additionalProperties = false, + ) +} diff --git a/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/FileWrittenPayload.kt b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/FileWrittenPayload.kt new file mode 100644 index 00000000..d0e4e3de --- /dev/null +++ b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/FileWrittenPayload.kt @@ -0,0 +1,18 @@ +package com.correx.core.artifacts.kind + +import kotlinx.serialization.Serializable + +@Serializable +data class FileWrittenPayload( + val path: String, + val content: String, + val mode: String = "0644", +) + +@Serializable +data class FileWrittenArtifact( + val path: String, + val contentHash: String, + val size: Long, + val mode: String, +) diff --git a/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/JsonSchema.kt b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/JsonSchema.kt new file mode 100644 index 00000000..e55bbb58 --- /dev/null +++ b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/JsonSchema.kt @@ -0,0 +1,18 @@ +package com.correx.core.artifacts.kind + +import kotlinx.serialization.Serializable + +@Serializable +data class JsonSchema( + val type: String, + val properties: Map = emptyMap(), + val required: List = emptyList(), + val additionalProperties: Boolean = false, +) + +@Serializable +data class JsonSchemaProperty( + val type: String, + val description: String = "", + val default: String? = null, +) diff --git a/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/ProcessResultArtifact.kt b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/ProcessResultArtifact.kt new file mode 100644 index 00000000..6239be6c --- /dev/null +++ b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/ProcessResultArtifact.kt @@ -0,0 +1,12 @@ +package com.correx.core.artifacts.kind + +import kotlinx.serialization.Serializable + +@Serializable +data class ProcessResultArtifact( + val command: String, + val exitCode: Int, + val stdout: String, + val stderr: String, + val durationMs: Long, +) diff --git a/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/ProcessResultKind.kt b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/ProcessResultKind.kt new file mode 100644 index 00000000..1b31a109 --- /dev/null +++ b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/ProcessResultKind.kt @@ -0,0 +1,22 @@ +package com.correx.core.artifacts.kind + +import kotlinx.serialization.KSerializer + +object ProcessResultKind : ArtifactKind { + override val id: String = "process_result" + override val payloadSerializer: KSerializer<*> = ProcessResultArtifact.serializer() + override val llmEmitted: Boolean = false + + override fun deriveJsonSchema(): JsonSchema = JsonSchema( + type = "object", + properties = mapOf( + "command" to JsonSchemaProperty(type = "string"), + "exitCode" to JsonSchemaProperty(type = "integer"), + "stdout" to JsonSchemaProperty(type = "string"), + "stderr" to JsonSchemaProperty(type = "string"), + "durationMs" to JsonSchemaProperty(type = "integer"), + ), + required = listOf("command", "exitCode", "stdout", "stderr", "durationMs"), + additionalProperties = false, + ) +} diff --git a/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/TypedArtifactSlot.kt b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/TypedArtifactSlot.kt new file mode 100644 index 00000000..49e051b7 --- /dev/null +++ b/core/artifacts/src/main/kotlin/com/correx/core/artifacts/kind/TypedArtifactSlot.kt @@ -0,0 +1,8 @@ +package com.correx.core.artifacts.kind + +import com.correx.core.events.types.ArtifactId + +data class TypedArtifactSlot( + val name: ArtifactId, + val kind: ArtifactKind, +) diff --git a/core/artifacts/src/test/kotlin/com/correx/core/artifacts/kind/ArtifactKindRegistryTest.kt b/core/artifacts/src/test/kotlin/com/correx/core/artifacts/kind/ArtifactKindRegistryTest.kt new file mode 100644 index 00000000..2e57745c --- /dev/null +++ b/core/artifacts/src/test/kotlin/com/correx/core/artifacts/kind/ArtifactKindRegistryTest.kt @@ -0,0 +1,39 @@ +package com.correx.core.artifacts.kind + +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class ArtifactKindRegistryTest { + private val registry = DefaultArtifactKindRegistry() + + @Test + fun `built-in kinds are registered`() { + assertNotNull(registry.get("file_written")) + assertNotNull(registry.get("process_result")) + } + + @Test + fun `get returns null for unknown kind`() { + assertNull(registry.get("nonexistent")) + } + + @Test + fun `FileWrittenKind is not llmEmitted`() { + assertEquals(false, registry.get("file_written")!!.llmEmitted) + } + + @Test + fun `ProcessResultKind is not llmEmitted`() { + assertEquals(false, registry.get("process_result")!!.llmEmitted) + } + + @Test + fun `FileWrittenKind schema has required fields`() { + val schema = FileWrittenKind.deriveJsonSchema() + assertEquals("object", schema.type) + assert("path" in schema.required) + assert("content" in schema.required) + } +}