feat(toolintent): stale-write gate
Block writing a file whose on-disk content changed since the session read it — a concurrent/external edit the agent's view doesn't reflect. file_read records a content hash on whole-file reads; the orchestrator's completion path now carries the tool's structuredOutput (it previously dropped it, unlike SandboxedToolExecutor — the two paths were inconsistent), so SessionContext.readHashes folds it. StaleWriteRule compares the current hash (WorldProbe.contentHash) against the read-time hash and BLOCKs a mismatch, telling the agent to re-read first. Files the session wrote itself are excluded, so its own edits never look stale; partial reads set no baseline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+8
@@ -142,9 +142,13 @@ class FileReadTool(
|
||||
val start = ((startLine ?: 1) - 1).coerceAtLeast(0)
|
||||
val end = (endLine ?: lines.size).coerceAtMost(lines.size)
|
||||
val content = lines.subList(start, end).joinToString("\n")
|
||||
// Record a content hash only for a whole-file read, so the stale-write gate baselines against
|
||||
// what the agent actually saw; a partial read establishes no baseline.
|
||||
val metadata = if (startLine == null && endLine == null) mapOf("contentHash" to sha256(path)) else emptyMap()
|
||||
ToolResult.Success(
|
||||
invocationId = request.invocationId,
|
||||
output = content,
|
||||
metadata = metadata,
|
||||
)
|
||||
}.getOrElse {
|
||||
ToolResult.Failure(
|
||||
@@ -154,6 +158,10 @@ class FileReadTool(
|
||||
)
|
||||
}
|
||||
|
||||
private fun sha256(path: Path): String =
|
||||
java.security.MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(path))
|
||||
.joinToString("") { "%02x".format(it) }
|
||||
|
||||
private fun listDir(path: Path, request: ToolRequest): ToolResult = runCatching {
|
||||
val entries = path.listDirectoryEntries().sortedBy { it.name }
|
||||
val output = entries.joinToString("\n") { entry ->
|
||||
|
||||
Reference in New Issue
Block a user