Bin29's picture
Sync from main: e764154 feat(plot-skill): add math-exam-diagram SKILL.md for exam-style math figures
abcf568
import type { StudioProcessorStreamEvent } from '../../domain/types'
import { throwIfStudioRunCancelled } from '../../runtime/execution/run-cancellation'
import { buildStudioPreToolCommentary } from '../../runtime/tools/pre-tool-commentary'
import { createStudioToolCallExecutionEvents } from '../../runtime/tools/tool-call-adapter'
import { logPlotStudioTiming, readRunElapsedMs } from '../../observability/plot-studio-timing'
import type {
StudioChatToolCall,
StudioLoopAutonomy,
StudioLoopRuntime,
StudioLoopStepResult,
StudioOpenAIToolLoopInput
} from './types'
export async function* executeStudioToolCallsForStep(
input: StudioOpenAIToolLoopInput,
runtime: StudioLoopRuntime,
result: StudioLoopStepResult,
autonomy: StudioLoopAutonomy
): AsyncGenerator<StudioProcessorStreamEvent, { failureMessage: string | null }> {
const hasAssistantText = Boolean(result.assistantText)
for (const toolCall of result.toolCalls) {
throwIfStudioRunCancelled(input.abortSignal)
const execution = executeStudioSingleToolCall(input, runtime, toolCall, autonomy, hasAssistantText)
let toolResult: IteratorResult<StudioProcessorStreamEvent, { transcript: string; failureMessage: string | null }>
while (true) {
toolResult = await execution.next()
if (toolResult.done) {
break
}
yield toolResult.value
}
runtime.conversation.push({
role: 'tool',
tool_call_id: toolCall.id,
content: toolResult.value.transcript || '(no tool output)'
})
if (toolResult.value.failureMessage) {
return { failureMessage: toolResult.value.failureMessage }
}
}
return { failureMessage: null }
}
async function* executeStudioSingleToolCall(
input: StudioOpenAIToolLoopInput,
runtime: StudioLoopRuntime,
toolCall: StudioChatToolCall,
autonomy: StudioLoopAutonomy,
hasAssistantText: boolean
): AsyncGenerator<StudioProcessorStreamEvent, { transcript: string; failureMessage: string | null }> {
const toolName = toolCall.function.name
const toolCallId = toolCall.id
const parsedInput = parseStudioToolArguments(toolName, toolCall.function.arguments)
throwIfStudioRunCancelled(input.abortSignal)
if (!parsedInput.ok) {
const fatal = autonomy.consecutiveFailures + 1 >= autonomy.maxConsecutiveFailures
logPlotStudioTiming(input.session.studioKind, 'tool.failure.detected', {
sessionId: input.session.id,
runId: input.run.id,
assistantMessageId: runtime.currentAssistantMessage.id,
toolName,
callId: toolCallId,
failureStage: 'argument_parse',
failureKind: 'invalid_arguments',
error: parsedInput.error,
rawArgumentsPreview: summarizeRawArguments(toolCall.function.arguments),
runElapsedMs: readRunElapsedMs(input.run),
}, 'warn')
yield {
type: 'tool-input-start',
id: toolCallId,
toolName,
raw: toolCall.function.arguments
}
yield {
type: 'tool-call',
toolCallId,
toolName,
input: {}
}
yield {
type: 'tool-error',
toolCallId,
error: parsedInput.error,
metadata: {
failureStage: 'argument_parse',
failureKind: 'invalid_arguments',
rawArgumentsPreview: summarizeRawArguments(toolCall.function.arguments),
recoverable: !fatal,
failureCount: autonomy.consecutiveFailures + 1,
}
}
return {
transcript: parsedInput.error,
failureMessage: parsedInput.error
}
}
let transcript = ''
for await (const event of createStudioToolCallExecutionEvents({
projectId: input.projectId,
session: input.session,
run: input.run,
assistantMessage: runtime.currentAssistantMessage,
toolCallId,
toolName,
toolInput: parsedInput.value,
registry: input.registry,
eventBus: input.eventBus,
messageStore: input.messageStore,
partStore: input.partStore,
sessionStore: input.sessionStore,
taskStore: input.taskStore,
workStore: input.workStore,
workResultStore: input.workResultStore,
runSubagent: input.runSubagent,
resolveSkill: input.resolveSkill,
listSkills: input.listSkills,
listSkillSummaries: input.listSkillSummaries,
recordSkillUsage: input.recordSkillUsage,
setToolMetadata: (callId, metadata) => input.setToolMetadata(runtime.currentAssistantMessage, callId, metadata),
customApiConfig: input.customApiConfig,
abortSignal: input.abortSignal,
commentary: hasAssistantText
? null
: buildStudioPreToolCommentary({
toolName,
toolInput: parsedInput.value
})
})) {
transcript = studioEventToTranscript(event, transcript)
if (event.type === 'tool-error') {
const fatal = autonomy.consecutiveFailures + 1 >= autonomy.maxConsecutiveFailures
yield {
...event,
metadata: {
...(event.metadata ?? {}),
recoverable: !fatal,
failureCount: autonomy.consecutiveFailures + 1,
}
}
return {
transcript,
failureMessage: event.error
}
}
yield event
}
return {
transcript,
failureMessage: null
}
}
function summarizeRawArguments(rawArguments: string): string {
if (rawArguments.length <= 300) {
return rawArguments
}
return `${rawArguments.slice(0, 297)}...`
}
function parseStudioToolArguments(
toolName: string,
rawArguments: string
): { ok: true; value: Record<string, unknown> } | { ok: false; error: string } {
if (!rawArguments.trim()) {
return { ok: true, value: {} }
}
try {
const parsed = JSON.parse(rawArguments)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return { ok: false, error: `Tool ${toolName} arguments must be a JSON object.` }
}
return { ok: true, value: parsed as Record<string, unknown> }
} catch (error) {
return {
ok: false,
error: `Tool ${toolName} arguments could not be parsed as JSON: ${error instanceof Error ? error.message : String(error)}`
}
}
}
function studioEventToTranscript(event: StudioProcessorStreamEvent, current: string): string {
if (event.type === 'tool-result') {
return event.output || '(empty tool result)'
}
if (event.type === 'tool-error') {
return `Tool execution failed: ${event.error}`
}
return current
}