feature(event-sourcing): baseline for the architecture review fixes.

- fixed the tool overlay in tui.
- fixed tool calling - added schemas.
- getting all sessionIds via event store.
- instead of sending all events on the replay - there's a new way for replaying.
- session snapshot is now a thing getting sent for previously created sessions.
- added logging to important parts.
- revert workflowId from WorkflowCompletedEvent.
This commit is contained in:
2026-05-24 18:50:00 +04:00
parent f827685ed0
commit fc7b879891
43 changed files with 525 additions and 211 deletions
@@ -13,7 +13,11 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import java.io.IOException
import java.nio.file.Files
import java.nio.file.InvalidPathException
@@ -39,7 +43,35 @@ class FileEditTool(
override val name: String = "file_edit"
override val description: String = "Edit content in a file at the specified path"
override val parametersSchema: JsonObject = buildJsonObject {}
override val parametersSchema: JsonObject = buildJsonObject {
put("type", "object")
putJsonObject("properties") {
putJsonObject("path") {
put("type", "string")
put("description", "Relative path to the file to edit")
}
putJsonObject("operation") {
put("type", "string")
put("description", "Either 'append', 'replace', or 'patch'")
}
putJsonObject("content") {
put("type", "string")
put("description", "Content to write. Used for 'append' and 'replace'.")
}
putJsonObject("old_str") {
put("type", "string")
put("description", "Exact string to find. Required for 'patch'.")
}
putJsonObject("new_str") {
put("type", "string")
put("description", "Replacement string. Required for 'patch'.")
}
}
put("required", buildJsonArray {
add(JsonPrimitive("path"))
add(JsonPrimitive("operation"))
})
}
override val tier: Tier = Tier.T3
override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_WRITE)
@@ -7,11 +7,14 @@ import com.correx.core.tools.contract.ToolCapability
import com.correx.core.tools.contract.ToolExecutor
import com.correx.core.tools.contract.ToolResult
import com.correx.core.tools.contract.ValidationResult
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import java.io.IOException
import java.nio.file.Files
import java.nio.file.InvalidPathException
@@ -24,76 +27,92 @@ class FileReadTool(
override val name: String = "file_read"
override val description: String = "Read content from a file at the specified path"
override val parametersSchema: JsonObject = buildJsonObject {}
override val parametersSchema: JsonObject = buildJsonObject {
put("type", "object")
putJsonObject("properties") {
putJsonObject("path") {
put("type", "string")
put("description", "Relative path to the file to read")
}
putJsonObject("start_line") {
put("type", "integer")
put("description", "First line to read, 1-indexed inclusive. Omit to read from beginning.")
}
putJsonObject("end_line") {
put("type", "integer")
put("description", "Last line to read, 1-indexed inclusive. Omit to read to end of file.")
}
}
put("required", buildJsonArray { add(JsonPrimitive("path")) })
}
override val tier: Tier = Tier.T1
override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_READ)
override fun validateRequest(request: ToolRequest): ValidationResult =
(request.parameters["path"] as? String)?.let { pathString ->
runCatching {
val path = Paths.get(pathString).normalize().toAbsolutePath()
if (allowedPaths.none { path.startsWith(it.normalize().toAbsolutePath()) }) {
return@runCatching ValidationResult.Invalid("Path '$pathString' is not in the allowed list")
}
override fun validateRequest(request: ToolRequest): ValidationResult {
val pathString = request.parameters["path"] as? String
?: return ValidationResult.Invalid("Missing or invalid 'path' parameter. Expected String.")
val startLine = request.parameters["start_line"]?.toString()?.toIntOrNull()
val endLine = request.parameters["end_line"]?.toString()?.toIntOrNull()
if (startLine != null && startLine < 1)
return ValidationResult.Invalid("'start_line' must be >= 1.")
if (endLine != null && endLine < 1)
return ValidationResult.Invalid("'end_line' must be >= 1.")
if (startLine != null && endLine != null && endLine < startLine)
return ValidationResult.Invalid("'end_line' must be >= 'start_line'.")
return runCatching {
val path = Paths.get(pathString).normalize().toAbsolutePath()
if (allowedPaths.isNotEmpty() && allowedPaths.none { path.startsWith(it.normalize().toAbsolutePath()) })
ValidationResult.Invalid("Path '$pathString' is not in the allowed list")
else
ValidationResult.Valid
}.getOrElse {
when (it) {
is InvalidPathException -> ValidationResult.Invalid("Invalid path format: ${it.message}")
is IOException -> ValidationResult.Invalid("IO error: ${it.message}")
is SecurityException -> ValidationResult.Invalid("Security error: ${it.message}")
else -> ValidationResult.Invalid(it.message ?: "Unknown error occured")
}
}.getOrElse {
when (it) {
is InvalidPathException -> ValidationResult.Invalid("Invalid path format: ${it.message}")
is IOException -> ValidationResult.Invalid("IO error: ${it.message}")
is SecurityException -> ValidationResult.Invalid("Security error: ${it.message}")
else -> ValidationResult.Invalid(it.message ?: "Unknown error occurred")
}
} ?: ValidationResult.Invalid("Missing or invalid 'path' parameter. Expected String.")
}
}
override suspend fun execute(request: ToolRequest): ToolResult = withContext(Dispatchers.IO) {
validateRequest(request).run {
takeIf { it is ValidationResult.Valid }?.let {
val pathString = request.parameters["path"] as String
val path = Paths.get(pathString).normalize().toAbsolutePath()
if (Files.exists(path)) {
readStringCatching(request, path, pathString)
} else {
ToolResult.Failure(
invocationId = request.invocationId,
reason = "File not found: $pathString",
recoverable = false,
)
}
} ?: ToolResult.Failure(
val validation = validateRequest(request)
if (validation is ValidationResult.Invalid)
return@withContext ToolResult.Failure(
invocationId = request.invocationId,
reason = (this as ValidationResult.Invalid).reason,
reason = validation.reason,
recoverable = false,
)
val pathString = request.parameters["path"] as String
val path = Paths.get(pathString).normalize().toAbsolutePath()
val startLine = request.parameters["start_line"]?.toString()?.toIntOrNull()
val endLine = request.parameters["end_line"]?.toString()?.toIntOrNull()
if (!Files.exists(path))
return@withContext ToolResult.Failure(
invocationId = request.invocationId,
reason = "File not found: $pathString",
recoverable = false,
)
runCatching {
val lines = Files.readAllLines(path)
val start = ((startLine ?: 1) - 1).coerceAtLeast(0)
val end = (endLine ?: lines.size).coerceAtMost(lines.size)
val content = lines.subList(start, end).joinToString("\n")
ToolResult.Success(
invocationId = request.invocationId,
output = content,
)
}.getOrElse {
ToolResult.Failure(
invocationId = request.invocationId,
reason = "Failed to read file: ${it.message}",
recoverable = false,
)
}
}
private fun readStringCatching(request: ToolRequest, path: Path, pathString: String): ToolResult =
runCatching {
ToolResult.Success(
invocationId = request.invocationId,
output = Files.readString(path),
)
}.getOrElse {
when (it) {
is CancellationException -> throw it
is SecurityException -> ToolResult.Failure(
invocationId = request.invocationId,
reason = "Access denied: $pathString, ${it.message}",
recoverable = false,
)
is IOException -> ToolResult.Failure(
invocationId = request.invocationId,
reason = "IO error: ${it.message}",
recoverable = false,
)
else -> ToolResult.Failure(
invocationId = request.invocationId,
reason = it.message ?: "Unknown error occurred while reading file",
recoverable = false,
)
}
}
}