@things-factory/board-ai 10.0.0-beta.67 → 10.0.0-beta.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -80,6 +80,15 @@ export async function runAgenticLoop(input) {
80
80
  log('iter %d: text=%s, toolCalls=%d, stopReason=%s', iter, result.text ? `"${result.text.slice(0, 60)}..."` : '(none)', result.toolCalls.length, result.stopReason);
81
81
  if (result.toolCalls.length === 0)
82
82
  break;
83
+ // (1b) AbortSignal — chat() 후 tool 실행 직전 재체크.
84
+ // chat() 진행 중 abort 는 AbortError 로 catch 되지만, chat() 이
85
+ // 정상 반환된 직후 ~ 다음 iter 시작 전 abort 가 들어오면 processToolCalls
86
+ // 내부 tool 들 (write 포함) 이 모두 실행돼버린다. 여기서 한 번 더 막음.
87
+ if (signal?.aborted) {
88
+ log('iter %d: aborted between chat() and tool execution', iter);
89
+ abortReason = { type: 'aborted', iter };
90
+ break;
91
+ }
83
92
  // tool 결과 빌드 — read 는 즉시 실행, write 는 누적, action 은 별도 채널
84
93
  const beforeUsages = toolUsages.length;
85
94
  const toolResultParts = await processToolCalls(result.toolCalls, currentBoard, selectedRefids, dispatch, accumulatedWriteCalls, accumulatedActions, toolUsages);
@@ -1 +1 @@
1
- {"version":3,"file":"agentic-loop.js","sourceRoot":"","sources":["../../../server/service/agentic-loop.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * agentic-loop — multi-turn LLM ↔ tool 루프.\n *\n * assistant.ts 에서 추출 — loop 자체를 *순수 함수* 로 만들어:\n * 1. 단위 테스트 가능 (assistant.ts 의 board-import → @aws-sdk transitive 경로 회피)\n * 2. 관측성 (error boundary / iter log / AbortSignal / repeated-failure 조기 종료)\n * 을 한 곳에 응집\n * 3. 향후 split (Phase 3) 의 자연 경계\n *\n * 입력 — `chat` 함수와 dispatch helper 들을 의존성 주입. board-ai 본체는 본 모듈에\n * 의존하지만 본 모듈은 board-ai 의 어떤 무거운 import 도 모름.\n */\nimport debug from 'debug'\nimport type {\n AIMessage,\n AIToolCall,\n AIToolChatResult\n} from '@things-factory/ai-client-base'\nimport type { BoardModel } from '@things-factory/board-import'\nimport type { BoardActionOp, ToolUsage } from './types'\n\nconst log = debug('things-factory:board-ai:loop')\n\n// ── Public types ─────────────────────────────────────────────────\n\n/**\n * Loop 가 호출하는 dispatch 함수들. 모두 의존성 주입.\n *\n * assistant.ts 가 자기 자신의 함수 (isReadTool, executeReadTool 등) 를 그대로\n * 넘겨주는 구조. 본 모듈은 이 함수들의 *시그니처* 만 알면 됨.\n */\nexport interface ToolDispatchHelpers {\n isReadTool: (name: string) => boolean\n isWriteTool: (name: string) => boolean\n isActionTool: (name: string) => boolean\n executeReadTool: (\n tc: AIToolCall,\n board: BoardModel | undefined,\n selectedRefids: number[]\n ) => any\n validateWriteToolCall: (\n tc: AIToolCall,\n components: any[]\n ) => { valid: boolean; errors?: any[]; suggestion?: string }\n toolCallToBoardActionOp: (tc: AIToolCall) => BoardActionOp | null\n summarizeToolResult: (value: any) => any\n}\n\nexport interface AgenticLoopOptions {\n systemPrompt: string\n model?: string\n temperature?: number\n maxTokens?: number\n /** 최대 iteration. 기본 8. */\n maxIterations?: number\n /**\n * 사용자 abort 신호. iteration 시작 직전 / chat() 호출 후 체크.\n * 이미 abort 된 신호로 들어오면 첫 iteration 도 안 돔.\n */\n signal?: AbortSignal\n /**\n * 같은 tool 의 validation 실패가 N회 연속이면 조기 종료. 기본 3.\n * LLM 이 같은 잘못을 무한 반복하는 경우 방어.\n */\n repeatedFailureLimit?: number\n}\n\nexport interface AgenticLoopInput {\n initialConversation: AIMessage[]\n tools: any[]\n options: AgenticLoopOptions\n /** LLM provider 의 chatWithTools 위임. provider-specific 구현. */\n chat: (\n messages: AIMessage[],\n tools: any[],\n options: any\n ) => Promise<AIToolChatResult>\n dispatch: ToolDispatchHelpers\n currentBoard: BoardModel | undefined\n selectedRefids: number[]\n}\n\nexport interface AgenticLoopResult {\n lastText: string | undefined\n accumulatedWriteCalls: AIToolCall[]\n accumulatedActions: BoardActionOp[]\n toolUsages: ToolUsage[]\n /** LLM 마지막 응답의 stopReason. provider-specific 문자열. */\n stopReason: string\n /**\n * 비정상 종료 사유 — 정상 종료 시 undefined.\n * 호출자 (assistant.ts) 가 사용자 메시지에 반영 (예: '시도 중 오류가 발생했어요').\n */\n abortReason?:\n | { type: 'provider_error'; message: string; iter: number }\n | { type: 'aborted'; iter: number }\n | { type: 'max_iterations'; iter: number }\n | { type: 'repeated_validation_failure'; toolName: string; consecutive: number; iter: number }\n}\n\n// ── Loop ─────────────────────────────────────────────────────────\n\nconst DEFAULT_MAX_ITERATIONS = 8\n\n/**\n * multi-turn agentic loop.\n *\n * LLM 이 read tool 을 호출 → 서버에서 즉시 실행 → 결과 회신 → LLM 추론 반복.\n * write tool 은 누적해 client 에 patch 로 전달. 무한 루프 방지를 위해 iteration cap.\n *\n * 본 함수는 *순수* — input 만 보고 결정. 외부 mutable state 없음.\n */\nexport async function runAgenticLoop(\n input: AgenticLoopInput\n): Promise<AgenticLoopResult> {\n const {\n initialConversation,\n tools,\n options,\n chat,\n dispatch,\n currentBoard,\n selectedRefids\n } = input\n\n const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS\n const failureLimit = options.repeatedFailureLimit ?? 3\n const signal = options.signal\n\n let conversation = initialConversation\n const accumulatedWriteCalls: AIToolCall[] = []\n const accumulatedActions: BoardActionOp[] = []\n const toolUsages: ToolUsage[] = []\n let lastText: string | undefined\n let stopReason: string = 'end_turn'\n let abortReason: AgenticLoopResult['abortReason']\n\n // 같은 tool 의 validation 실패 연속 카운터 — 다른 tool 호출 시 reset\n let lastFailedToolName: string | undefined\n let consecutiveFailures = 0\n\n let iter = 0\n for (; iter < maxIterations; iter++) {\n // (1) AbortSignal — iteration 시작 시 체크\n if (signal?.aborted) {\n log('iter %d: aborted by signal', iter)\n abortReason = { type: 'aborted', iter }\n break\n }\n\n log('iter %d: chat() 호출', iter)\n let result: AIToolChatResult\n try {\n // (2) Error boundary — provider 예외 시 graceful 종료\n result = await chat(conversation, tools, {\n systemPrompt: options.systemPrompt,\n model: options.model,\n temperature: options.temperature,\n maxTokens: options.maxTokens,\n toolChoice: 'auto',\n allowParallelToolCalls: true,\n signal\n })\n } catch (e: any) {\n // AbortError — 별도 분류\n if (e?.name === 'AbortError' || signal?.aborted) {\n log('iter %d: provider AbortError', iter)\n abortReason = { type: 'aborted', iter }\n } else {\n log('iter %d: provider error: %s', iter, e?.message)\n abortReason = {\n type: 'provider_error',\n message: e?.message ?? String(e),\n iter\n }\n }\n break\n }\n\n lastText = result.text || lastText\n stopReason = result.stopReason\n\n log(\n 'iter %d: text=%s, toolCalls=%d, stopReason=%s',\n iter,\n result.text ? `\"${result.text.slice(0, 60)}...\"` : '(none)',\n result.toolCalls.length,\n result.stopReason\n )\n\n if (result.toolCalls.length === 0) break\n\n // tool 결과 빌드 — read 는 즉시 실행, write 는 누적, action 은 별도 채널\n const beforeUsages = toolUsages.length\n const toolResultParts = await processToolCalls(\n result.toolCalls,\n currentBoard,\n selectedRefids,\n dispatch,\n accumulatedWriteCalls,\n accumulatedActions,\n toolUsages\n )\n\n // (4) 같은 tool 의 validation 실패 연속 감지 — LLM 이 같은 잘못 반복 시 abort\n const newUsages = toolUsages.slice(beforeUsages)\n const repeatedFailureAbort = detectRepeatedValidationFailure(\n newUsages,\n lastFailedToolName,\n consecutiveFailures,\n failureLimit\n )\n if (repeatedFailureAbort.shouldAbort) {\n log(\n 'iter %d: repeated validation failure on %s (%d consecutive) — abort',\n iter,\n repeatedFailureAbort.toolName,\n repeatedFailureAbort.consecutive\n )\n abortReason = {\n type: 'repeated_validation_failure',\n toolName: repeatedFailureAbort.toolName,\n consecutive: repeatedFailureAbort.consecutive,\n iter\n }\n break\n }\n lastFailedToolName = repeatedFailureAbort.lastFailedToolName\n consecutiveFailures = repeatedFailureAbort.consecutive\n\n // assistant 의 tool_use turn + user 의 tool_result turn 을 conversation 에 누적\n const assistantContent: any[] = []\n if (result.text) assistantContent.push({ type: 'text', text: result.text })\n for (const tc of result.toolCalls) {\n // providerMeta (Gemini thoughtSignature 등) 를 그대로 round-trip — 안 보내면 400.\n assistantContent.push({\n type: 'tool_use',\n id: tc.id,\n name: tc.name,\n arguments: tc.arguments,\n ...(tc.providerMeta && { providerMeta: tc.providerMeta })\n })\n }\n\n conversation = [\n ...conversation,\n { role: 'assistant', content: assistantContent },\n { role: 'user', content: toolResultParts }\n ]\n }\n\n // (3) max iterations 도달 시 사용자에게 표면화\n if (iter >= maxIterations && !abortReason) {\n log('iter %d: max iterations reached', iter)\n abortReason = { type: 'max_iterations', iter }\n }\n\n return {\n lastText,\n accumulatedWriteCalls,\n accumulatedActions,\n toolUsages,\n stopReason,\n abortReason\n }\n}\n\n/**\n * 같은 tool 의 validation 실패 연속 감지 — pure helper.\n *\n * 같은 tool 이 반복 fail 하면 LLM 이 self-correct 못 하는 상태.\n * 다른 tool 이 fail 하면 시퀀스 새로 시작 (전혀 다른 패턴이라).\n * tool 이 succeed 하면 카운터 reset.\n */\nfunction detectRepeatedValidationFailure(\n newUsages: ToolUsage[],\n lastFailedToolName: string | undefined,\n consecutiveFailures: number,\n limit: number\n): {\n shouldAbort: boolean\n toolName: string\n consecutive: number\n lastFailedToolName: string | undefined\n} {\n // 이번 iteration 에서 실패한 write tool 들 (validation 거절)\n const failed = newUsages.filter(\n u => u.kind === 'write' && (u.result as any)?.error === 'Tool args failed validation'\n )\n\n if (failed.length === 0) {\n // 실패 없음 — reset\n return {\n shouldAbort: false,\n toolName: '',\n consecutive: 0,\n lastFailedToolName: undefined\n }\n }\n\n // 모든 실패가 같은 tool 인지 확인\n const firstName = failed[0].name\n const allSame = failed.every(u => u.name === firstName)\n\n if (allSame && firstName === lastFailedToolName) {\n const consecutive = consecutiveFailures + 1\n return {\n shouldAbort: consecutive >= limit,\n toolName: firstName,\n consecutive,\n lastFailedToolName: firstName\n }\n }\n\n // 다른 tool 또는 첫 실패\n return {\n shouldAbort: false,\n toolName: firstName,\n consecutive: 1,\n lastFailedToolName: firstName\n }\n}\n\n/**\n * 한 iteration 의 tool 결과를 분류·실행·누적. (loop 에서 분리해 읽기 쉽게.)\n */\nasync function processToolCalls(\n toolCalls: AIToolCall[],\n currentBoard: BoardModel | undefined,\n selectedRefids: number[],\n dispatch: ToolDispatchHelpers,\n accumulatedWriteCalls: AIToolCall[],\n accumulatedActions: BoardActionOp[],\n toolUsages: ToolUsage[]\n): Promise<any[]> {\n const toolResultParts: any[] = []\n\n for (const tc of toolCalls) {\n if (dispatch.isReadTool(tc.name)) {\n const value = dispatch.executeReadTool(tc, currentBoard, selectedRefids)\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(value)\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: dispatch.summarizeToolResult(value),\n kind: 'read'\n })\n } else if (dispatch.isWriteTool(tc.name)) {\n // 사전 검증 — args 가 schema / build 함수 검증 통과해야 누적.\n // 실패 시 LLM 한테 detailed error 반환 → 다음 turn 에서 자기 회복 (retry).\n const components = (currentBoard?.components ?? []) as any[]\n const validation = dispatch.validateWriteToolCall(tc, components)\n if (!validation.valid) {\n const errorResult = {\n error: 'Tool args failed validation',\n issues: validation.errors,\n suggestion: validation.suggestion,\n note: 'Fix the args and call the tool again.'\n }\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(errorResult)\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: errorResult,\n kind: 'write'\n })\n // continue — 누적 안 함. LLM 이 다음 turn 에서 정정 호출.\n } else {\n accumulatedWriteCalls.push(tc)\n const queuedResult = { queued: true, note: 'Will be applied on the client.' }\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(queuedResult)\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: queuedResult,\n kind: 'write'\n })\n }\n } else if (dispatch.isActionTool(tc.name)) {\n // ephemeral scene 조작 — accumulatedActions 로 누적, ChatResponse.actions 로 client 전달\n const action = dispatch.toolCallToBoardActionOp(tc)\n if (action) accumulatedActions.push(action)\n const queuedResult = {\n queued: true,\n note: 'Will be executed on the modeller (ephemeral).'\n }\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(queuedResult)\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: queuedResult,\n kind: 'write'\n })\n } else {\n const errResult = { error: `Unknown tool: ${tc.name}` }\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(errResult),\n isError: true\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: errResult,\n kind: 'unknown'\n })\n }\n }\n\n return toolResultParts\n}\n"]}
1
+ {"version":3,"file":"agentic-loop.js","sourceRoot":"","sources":["../../../server/service/agentic-loop.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * agentic-loop — multi-turn LLM ↔ tool 루프.\n *\n * assistant.ts 에서 추출 — loop 자체를 *순수 함수* 로 만들어:\n * 1. 단위 테스트 가능 (assistant.ts 의 board-import → @aws-sdk transitive 경로 회피)\n * 2. 관측성 (error boundary / iter log / AbortSignal / repeated-failure 조기 종료)\n * 을 한 곳에 응집\n * 3. 향후 split (Phase 3) 의 자연 경계\n *\n * 입력 — `chat` 함수와 dispatch helper 들을 의존성 주입. board-ai 본체는 본 모듈에\n * 의존하지만 본 모듈은 board-ai 의 어떤 무거운 import 도 모름.\n */\nimport debug from 'debug'\nimport type {\n AIMessage,\n AIToolCall,\n AIToolChatResult\n} from '@things-factory/ai-client-base'\nimport type { BoardModel } from '@things-factory/board-import'\nimport type { BoardActionOp, ToolUsage } from './types'\n\nconst log = debug('things-factory:board-ai:loop')\n\n// ── Public types ─────────────────────────────────────────────────\n\n/**\n * Loop 가 호출하는 dispatch 함수들. 모두 의존성 주입.\n *\n * assistant.ts 가 자기 자신의 함수 (isReadTool, executeReadTool 등) 를 그대로\n * 넘겨주는 구조. 본 모듈은 이 함수들의 *시그니처* 만 알면 됨.\n */\nexport interface ToolDispatchHelpers {\n isReadTool: (name: string) => boolean\n isWriteTool: (name: string) => boolean\n isActionTool: (name: string) => boolean\n executeReadTool: (\n tc: AIToolCall,\n board: BoardModel | undefined,\n selectedRefids: number[]\n ) => any\n validateWriteToolCall: (\n tc: AIToolCall,\n components: any[]\n ) => { valid: boolean; errors?: any[]; suggestion?: string }\n toolCallToBoardActionOp: (tc: AIToolCall) => BoardActionOp | null\n summarizeToolResult: (value: any) => any\n}\n\nexport interface AgenticLoopOptions {\n systemPrompt: string\n model?: string\n temperature?: number\n maxTokens?: number\n /** 최대 iteration. 기본 8. */\n maxIterations?: number\n /**\n * 사용자 abort 신호. iteration 시작 직전 / chat() 호출 후 체크.\n * 이미 abort 된 신호로 들어오면 첫 iteration 도 안 돔.\n */\n signal?: AbortSignal\n /**\n * 같은 tool 의 validation 실패가 N회 연속이면 조기 종료. 기본 3.\n * LLM 이 같은 잘못을 무한 반복하는 경우 방어.\n */\n repeatedFailureLimit?: number\n}\n\nexport interface AgenticLoopInput {\n initialConversation: AIMessage[]\n tools: any[]\n options: AgenticLoopOptions\n /** LLM provider 의 chatWithTools 위임. provider-specific 구현. */\n chat: (\n messages: AIMessage[],\n tools: any[],\n options: any\n ) => Promise<AIToolChatResult>\n dispatch: ToolDispatchHelpers\n currentBoard: BoardModel | undefined\n selectedRefids: number[]\n}\n\nexport interface AgenticLoopResult {\n lastText: string | undefined\n accumulatedWriteCalls: AIToolCall[]\n accumulatedActions: BoardActionOp[]\n toolUsages: ToolUsage[]\n /** LLM 마지막 응답의 stopReason. provider-specific 문자열. */\n stopReason: string\n /**\n * 비정상 종료 사유 — 정상 종료 시 undefined.\n * 호출자 (assistant.ts) 가 사용자 메시지에 반영 (예: '시도 중 오류가 발생했어요').\n */\n abortReason?:\n | { type: 'provider_error'; message: string; iter: number }\n | { type: 'aborted'; iter: number }\n | { type: 'max_iterations'; iter: number }\n | { type: 'repeated_validation_failure'; toolName: string; consecutive: number; iter: number }\n}\n\n// ── Loop ─────────────────────────────────────────────────────────\n\nconst DEFAULT_MAX_ITERATIONS = 8\n\n/**\n * multi-turn agentic loop.\n *\n * LLM 이 read tool 을 호출 → 서버에서 즉시 실행 → 결과 회신 → LLM 추론 반복.\n * write tool 은 누적해 client 에 patch 로 전달. 무한 루프 방지를 위해 iteration cap.\n *\n * 본 함수는 *순수* — input 만 보고 결정. 외부 mutable state 없음.\n */\nexport async function runAgenticLoop(\n input: AgenticLoopInput\n): Promise<AgenticLoopResult> {\n const {\n initialConversation,\n tools,\n options,\n chat,\n dispatch,\n currentBoard,\n selectedRefids\n } = input\n\n const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS\n const failureLimit = options.repeatedFailureLimit ?? 3\n const signal = options.signal\n\n let conversation = initialConversation\n const accumulatedWriteCalls: AIToolCall[] = []\n const accumulatedActions: BoardActionOp[] = []\n const toolUsages: ToolUsage[] = []\n let lastText: string | undefined\n let stopReason: string = 'end_turn'\n let abortReason: AgenticLoopResult['abortReason']\n\n // 같은 tool 의 validation 실패 연속 카운터 — 다른 tool 호출 시 reset\n let lastFailedToolName: string | undefined\n let consecutiveFailures = 0\n\n let iter = 0\n for (; iter < maxIterations; iter++) {\n // (1) AbortSignal — iteration 시작 시 체크\n if (signal?.aborted) {\n log('iter %d: aborted by signal', iter)\n abortReason = { type: 'aborted', iter }\n break\n }\n\n log('iter %d: chat() 호출', iter)\n let result: AIToolChatResult\n try {\n // (2) Error boundary — provider 예외 시 graceful 종료\n result = await chat(conversation, tools, {\n systemPrompt: options.systemPrompt,\n model: options.model,\n temperature: options.temperature,\n maxTokens: options.maxTokens,\n toolChoice: 'auto',\n allowParallelToolCalls: true,\n signal\n })\n } catch (e: any) {\n // AbortError — 별도 분류\n if (e?.name === 'AbortError' || signal?.aborted) {\n log('iter %d: provider AbortError', iter)\n abortReason = { type: 'aborted', iter }\n } else {\n log('iter %d: provider error: %s', iter, e?.message)\n abortReason = {\n type: 'provider_error',\n message: e?.message ?? String(e),\n iter\n }\n }\n break\n }\n\n lastText = result.text || lastText\n stopReason = result.stopReason\n\n log(\n 'iter %d: text=%s, toolCalls=%d, stopReason=%s',\n iter,\n result.text ? `\"${result.text.slice(0, 60)}...\"` : '(none)',\n result.toolCalls.length,\n result.stopReason\n )\n\n if (result.toolCalls.length === 0) break\n\n // (1b) AbortSignal — chat() 후 tool 실행 직전 재체크.\n // chat() 진행 중 abort 는 AbortError 로 catch 되지만, chat() 이\n // 정상 반환된 직후 ~ 다음 iter 시작 전 abort 가 들어오면 processToolCalls\n // 내부 tool 들 (write 포함) 이 모두 실행돼버린다. 여기서 한 번 더 막음.\n if (signal?.aborted) {\n log('iter %d: aborted between chat() and tool execution', iter)\n abortReason = { type: 'aborted', iter }\n break\n }\n\n // tool 결과 빌드 — read 는 즉시 실행, write 는 누적, action 은 별도 채널\n const beforeUsages = toolUsages.length\n const toolResultParts = await processToolCalls(\n result.toolCalls,\n currentBoard,\n selectedRefids,\n dispatch,\n accumulatedWriteCalls,\n accumulatedActions,\n toolUsages\n )\n\n // (4) 같은 tool 의 validation 실패 연속 감지 — LLM 이 같은 잘못 반복 시 abort\n const newUsages = toolUsages.slice(beforeUsages)\n const repeatedFailureAbort = detectRepeatedValidationFailure(\n newUsages,\n lastFailedToolName,\n consecutiveFailures,\n failureLimit\n )\n if (repeatedFailureAbort.shouldAbort) {\n log(\n 'iter %d: repeated validation failure on %s (%d consecutive) — abort',\n iter,\n repeatedFailureAbort.toolName,\n repeatedFailureAbort.consecutive\n )\n abortReason = {\n type: 'repeated_validation_failure',\n toolName: repeatedFailureAbort.toolName,\n consecutive: repeatedFailureAbort.consecutive,\n iter\n }\n break\n }\n lastFailedToolName = repeatedFailureAbort.lastFailedToolName\n consecutiveFailures = repeatedFailureAbort.consecutive\n\n // assistant 의 tool_use turn + user 의 tool_result turn 을 conversation 에 누적\n const assistantContent: any[] = []\n if (result.text) assistantContent.push({ type: 'text', text: result.text })\n for (const tc of result.toolCalls) {\n // providerMeta (Gemini thoughtSignature 등) 를 그대로 round-trip — 안 보내면 400.\n assistantContent.push({\n type: 'tool_use',\n id: tc.id,\n name: tc.name,\n arguments: tc.arguments,\n ...(tc.providerMeta && { providerMeta: tc.providerMeta })\n })\n }\n\n conversation = [\n ...conversation,\n { role: 'assistant', content: assistantContent },\n { role: 'user', content: toolResultParts }\n ]\n }\n\n // (3) max iterations 도달 시 사용자에게 표면화\n if (iter >= maxIterations && !abortReason) {\n log('iter %d: max iterations reached', iter)\n abortReason = { type: 'max_iterations', iter }\n }\n\n return {\n lastText,\n accumulatedWriteCalls,\n accumulatedActions,\n toolUsages,\n stopReason,\n abortReason\n }\n}\n\n/**\n * 같은 tool 의 validation 실패 연속 감지 — pure helper.\n *\n * 같은 tool 이 반복 fail 하면 LLM 이 self-correct 못 하는 상태.\n * 다른 tool 이 fail 하면 시퀀스 새로 시작 (전혀 다른 패턴이라).\n * tool 이 succeed 하면 카운터 reset.\n */\nfunction detectRepeatedValidationFailure(\n newUsages: ToolUsage[],\n lastFailedToolName: string | undefined,\n consecutiveFailures: number,\n limit: number\n): {\n shouldAbort: boolean\n toolName: string\n consecutive: number\n lastFailedToolName: string | undefined\n} {\n // 이번 iteration 에서 실패한 write tool 들 (validation 거절)\n const failed = newUsages.filter(\n u => u.kind === 'write' && (u.result as any)?.error === 'Tool args failed validation'\n )\n\n if (failed.length === 0) {\n // 실패 없음 — reset\n return {\n shouldAbort: false,\n toolName: '',\n consecutive: 0,\n lastFailedToolName: undefined\n }\n }\n\n // 모든 실패가 같은 tool 인지 확인\n const firstName = failed[0].name\n const allSame = failed.every(u => u.name === firstName)\n\n if (allSame && firstName === lastFailedToolName) {\n const consecutive = consecutiveFailures + 1\n return {\n shouldAbort: consecutive >= limit,\n toolName: firstName,\n consecutive,\n lastFailedToolName: firstName\n }\n }\n\n // 다른 tool 또는 첫 실패\n return {\n shouldAbort: false,\n toolName: firstName,\n consecutive: 1,\n lastFailedToolName: firstName\n }\n}\n\n/**\n * 한 iteration 의 tool 결과를 분류·실행·누적. (loop 에서 분리해 읽기 쉽게.)\n */\nasync function processToolCalls(\n toolCalls: AIToolCall[],\n currentBoard: BoardModel | undefined,\n selectedRefids: number[],\n dispatch: ToolDispatchHelpers,\n accumulatedWriteCalls: AIToolCall[],\n accumulatedActions: BoardActionOp[],\n toolUsages: ToolUsage[]\n): Promise<any[]> {\n const toolResultParts: any[] = []\n\n for (const tc of toolCalls) {\n if (dispatch.isReadTool(tc.name)) {\n const value = dispatch.executeReadTool(tc, currentBoard, selectedRefids)\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(value)\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: dispatch.summarizeToolResult(value),\n kind: 'read'\n })\n } else if (dispatch.isWriteTool(tc.name)) {\n // 사전 검증 — args 가 schema / build 함수 검증 통과해야 누적.\n // 실패 시 LLM 한테 detailed error 반환 → 다음 turn 에서 자기 회복 (retry).\n const components = (currentBoard?.components ?? []) as any[]\n const validation = dispatch.validateWriteToolCall(tc, components)\n if (!validation.valid) {\n const errorResult = {\n error: 'Tool args failed validation',\n issues: validation.errors,\n suggestion: validation.suggestion,\n note: 'Fix the args and call the tool again.'\n }\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(errorResult)\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: errorResult,\n kind: 'write'\n })\n // continue — 누적 안 함. LLM 이 다음 turn 에서 정정 호출.\n } else {\n accumulatedWriteCalls.push(tc)\n const queuedResult = { queued: true, note: 'Will be applied on the client.' }\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(queuedResult)\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: queuedResult,\n kind: 'write'\n })\n }\n } else if (dispatch.isActionTool(tc.name)) {\n // ephemeral scene 조작 — accumulatedActions 로 누적, ChatResponse.actions 로 client 전달\n const action = dispatch.toolCallToBoardActionOp(tc)\n if (action) accumulatedActions.push(action)\n const queuedResult = {\n queued: true,\n note: 'Will be executed on the modeller (ephemeral).'\n }\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(queuedResult)\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: queuedResult,\n kind: 'write'\n })\n } else {\n const errResult = { error: `Unknown tool: ${tc.name}` }\n toolResultParts.push({\n type: 'tool_result',\n toolUseId: tc.id,\n content: JSON.stringify(errResult),\n isError: true\n })\n toolUsages.push({\n name: tc.name,\n arguments: tc.arguments ?? {},\n result: errResult,\n kind: 'unknown'\n })\n }\n }\n\n return toolResultParts\n}\n"]}