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:
+33
-1
@@ -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)
|
||||
|
||||
|
||||
+80
-61
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user