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 &&
|
inferenceResult.response.finishReason is FinishReason.ToolCall &&
|
||||||
toolRounds < MAX_TOOL_ROUNDS
|
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:") }
|
val fatalEntry = toolEntries.firstOrNull { it.content.startsWith("FATAL:") }
|
||||||
if (fatalEntry != null) {
|
if (fatalEntry != null) {
|
||||||
storeFailedProcessResult(sessionId, stageId, stageConfig,
|
emitProcessResultEvents(sessionId, stageId, stageConfig)
|
||||||
inferenceResult.response.toolCalls, fatalEntry)
|
|
||||||
return StageExecutionResult.Failure(
|
return StageExecutionResult.Failure(
|
||||||
fatalEntry.content.removePrefix("FATAL: "),
|
fatalEntry.content.removePrefix("FATAL: "),
|
||||||
retryable = false,
|
retryable = false,
|
||||||
@@ -251,6 +251,7 @@ abstract class SessionOrchestrator(
|
|||||||
}
|
}
|
||||||
val errorEntry = toolEntries.firstOrNull { it.content.startsWith("ERROR:") }
|
val errorEntry = toolEntries.firstOrNull { it.content.startsWith("ERROR:") }
|
||||||
if (errorEntry != null) {
|
if (errorEntry != null) {
|
||||||
|
emitProcessResultEvents(sessionId, stageId, stageConfig)
|
||||||
return StageExecutionResult.Failure(
|
return StageExecutionResult.Failure(
|
||||||
errorEntry.content.removePrefix("ERROR: "),
|
errorEntry.content.removePrefix("ERROR: "),
|
||||||
retryable = true,
|
retryable = true,
|
||||||
@@ -299,8 +300,10 @@ abstract class SessionOrchestrator(
|
|||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
stageId: StageId,
|
stageId: StageId,
|
||||||
toolCalls: List<ToolCallRequest>,
|
toolCalls: List<ToolCallRequest>,
|
||||||
|
stageConfig: StageConfig,
|
||||||
): List<ContextEntry> {
|
): List<ContextEntry> {
|
||||||
val executor = toolExecutor ?: return emptyList()
|
val executor = toolExecutor ?: return emptyList()
|
||||||
|
val processResultSlots = stageConfig.produces.filter { it.kind.id == "process_result" }
|
||||||
return toolCalls.flatMap { toolCall ->
|
return toolCalls.flatMap { toolCall ->
|
||||||
val invocationId = ToolInvocationId(UUID.randomUUID().toString())
|
val invocationId = ToolInvocationId(UUID.randomUUID().toString())
|
||||||
val parameters = runCatching {
|
val parameters = runCatching {
|
||||||
@@ -391,6 +394,32 @@ abstract class SessionOrchestrator(
|
|||||||
emit(sessionId, OrchestrationResumedEvent(sessionId, stageId))
|
emit(sessionId, OrchestrationResumedEvent(sessionId, stageId))
|
||||||
}
|
}
|
||||||
val result = executor.execute(request)
|
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 sourceId = toolCall.id ?: invocationId.value
|
||||||
val assistantEntry = ContextEntry(
|
val assistantEntry = ContextEntry(
|
||||||
id = ContextEntryId(UUID.randomUUID().toString()),
|
id = ContextEntryId(UUID.randomUUID().toString()),
|
||||||
@@ -477,28 +506,14 @@ abstract class SessionOrchestrator(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun storeFailedProcessResult(
|
private suspend fun emitProcessResultEvents(
|
||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
stageId: StageId,
|
stageId: StageId,
|
||||||
stageConfig: StageConfig,
|
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 ->
|
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, ArtifactCreatedEvent(slot.name, sessionId, stageId, schemaVersion = 1))
|
||||||
emit(sessionId, ArtifactValidatingEvent(slot.name, sessionId, stageId))
|
emit(sessionId, ArtifactValidatingEvent(slot.name, sessionId, stageId))
|
||||||
emit(sessionId, ArtifactValidatedEvent(slot.name, sessionId, stageId))
|
emit(sessionId, ArtifactValidatedEvent(slot.name, sessionId, stageId))
|
||||||
|
|||||||
Reference in New Issue
Block a user