fix: store ProcessResult artifact for every shell tool outcome, not only FATAL
Move ProcessResult artifact content storage into dispatchToolCalls so it fires for all outcomes (success, ERROR, FATAL) — the spec mandates the artifact MUST always be produced. - dispatchToolCalls now stores ProcessResult JSON in CAS + cache for every tool call result, using the current toolCall's function.arguments as the command field (not firstOrNull) - Replace storeFailedProcessResult with emitProcessResultEvents — event emission only, content already stored by dispatchToolCalls - FATAL and ERROR branches both now emit artifact lifecycle events, fixing the gap where retryable ToolResult.Failure produced no artifact
This commit is contained in:
+35
-20
@@ -239,11 +239,11 @@ abstract class SessionOrchestrator(
|
||||
inferenceResult.response.finishReason is FinishReason.ToolCall &&
|
||||
toolRounds < MAX_TOOL_ROUNDS
|
||||
) {
|
||||
val toolEntries = dispatchToolCalls(sessionId, stageId, inferenceResult.response.toolCalls)
|
||||
val toolEntries = dispatchToolCalls(sessionId, stageId,
|
||||
inferenceResult.response.toolCalls, stageConfig)
|
||||
val fatalEntry = toolEntries.firstOrNull { it.content.startsWith("FATAL:") }
|
||||
if (fatalEntry != null) {
|
||||
storeFailedProcessResult(sessionId, stageId, stageConfig,
|
||||
inferenceResult.response.toolCalls, fatalEntry)
|
||||
emitProcessResultEvents(sessionId, stageId, stageConfig)
|
||||
return StageExecutionResult.Failure(
|
||||
fatalEntry.content.removePrefix("FATAL: "),
|
||||
retryable = false,
|
||||
@@ -251,6 +251,7 @@ abstract class SessionOrchestrator(
|
||||
}
|
||||
val errorEntry = toolEntries.firstOrNull { it.content.startsWith("ERROR:") }
|
||||
if (errorEntry != null) {
|
||||
emitProcessResultEvents(sessionId, stageId, stageConfig)
|
||||
return StageExecutionResult.Failure(
|
||||
errorEntry.content.removePrefix("ERROR: "),
|
||||
retryable = true,
|
||||
@@ -299,8 +300,10 @@ abstract class SessionOrchestrator(
|
||||
sessionId: SessionId,
|
||||
stageId: StageId,
|
||||
toolCalls: List<ToolCallRequest>,
|
||||
stageConfig: StageConfig,
|
||||
): List<ContextEntry> {
|
||||
val executor = toolExecutor ?: return emptyList()
|
||||
val processResultSlots = stageConfig.produces.filter { it.kind.id == "process_result" }
|
||||
return toolCalls.flatMap { toolCall ->
|
||||
val invocationId = ToolInvocationId(UUID.randomUUID().toString())
|
||||
val parameters = runCatching {
|
||||
@@ -391,6 +394,32 @@ abstract class SessionOrchestrator(
|
||||
emit(sessionId, OrchestrationResumedEvent(sessionId, stageId))
|
||||
}
|
||||
val result = executor.execute(request)
|
||||
|
||||
// Store ProcessResult artifact for every shell execution outcome
|
||||
for (slot in processResultSlots) {
|
||||
val processResult = when (result) {
|
||||
is ToolResult.Success -> ProcessResultArtifact(
|
||||
status = "success",
|
||||
command = toolCall.function.arguments,
|
||||
exitCode = result.exitCode,
|
||||
stdout = result.output,
|
||||
stderr = result.metadata["stderr"] ?: "",
|
||||
durationMs = 0,
|
||||
)
|
||||
is ToolResult.Failure -> ProcessResultArtifact(
|
||||
status = "failed",
|
||||
command = toolCall.function.arguments,
|
||||
exitCode = -1,
|
||||
stdout = "",
|
||||
stderr = result.reason,
|
||||
durationMs = 0,
|
||||
)
|
||||
}
|
||||
val jsonContent = Json.encodeToString(ProcessResultArtifact.serializer(), processResult)
|
||||
artifactStore.put(jsonContent.toByteArray())
|
||||
artifactContentCache["${sessionId.value}:${slot.name.value}"] = jsonContent
|
||||
}
|
||||
|
||||
val sourceId = toolCall.id ?: invocationId.value
|
||||
val assistantEntry = ContextEntry(
|
||||
id = ContextEntryId(UUID.randomUUID().toString()),
|
||||
@@ -477,28 +506,14 @@ abstract class SessionOrchestrator(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun storeFailedProcessResult(
|
||||
private suspend fun emitProcessResultEvents(
|
||||
sessionId: SessionId,
|
||||
stageId: StageId,
|
||||
stageConfig: StageConfig,
|
||||
toolCalls: List<ToolCallRequest>,
|
||||
fatalEntry: ContextEntry,
|
||||
) {
|
||||
// Emit artifact lifecycle events for process_result slots on failure branches.
|
||||
// Content was already stored in CAS + cache by dispatchToolCalls for every outcome.
|
||||
stageConfig.produces.filter { it.kind.id == "process_result" }.forEach { slot ->
|
||||
val cmd = toolCalls.firstOrNull()?.function?.arguments ?: ""
|
||||
val reason = fatalEntry.content.removePrefix("FATAL: ")
|
||||
val processResult = ProcessResultArtifact(
|
||||
status = "failed",
|
||||
command = cmd,
|
||||
exitCode = -1,
|
||||
stdout = "",
|
||||
stderr = reason,
|
||||
durationMs = 0,
|
||||
)
|
||||
val jsonContent = Json.encodeToString(ProcessResultArtifact.serializer(), processResult)
|
||||
artifactStore.put(jsonContent.toByteArray())
|
||||
val cacheKey = "${sessionId.value}:${slot.name.value}"
|
||||
artifactContentCache[cacheKey] = jsonContent
|
||||
emit(sessionId, ArtifactCreatedEvent(slot.name, sessionId, stageId, schemaVersion = 1))
|
||||
emit(sessionId, ArtifactValidatingEvent(slot.name, sessionId, stageId))
|
||||
emit(sessionId, ArtifactValidatedEvent(slot.name, sessionId, stageId))
|
||||
|
||||
Reference in New Issue
Block a user