@things-factory/ai-client-base 10.1.176 → 10.1.177

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.
@@ -323,7 +323,7 @@ iter) {
323
323
  catch (e) {
324
324
  const failure = { error: exports.TOOL_FAILED, message: String(e?.message ?? e) };
325
325
  toolResultParts.push({ type: 'tool_result', toolUseId: tc.id, content: JSON.stringify(failure), isError: true });
326
- toolUsages.push({ name: tc.name, arguments: tc.arguments ?? {}, result: failure, kind: 'read', iter, outcome: 'error' });
326
+ toolUsages.push({ name: tc.name, arguments: tc.arguments ?? {}, result: dispatch.summarizeToolResult(failure), kind: 'read', iter, outcome: 'error' });
327
327
  continue;
328
328
  }
329
329
  /* await 필수 — registry 에 등록된 read/external builder 는 async 일 수 있다(DB·외부 호출).
@@ -398,7 +398,7 @@ iter) {
398
398
  toolUsages.push({
399
399
  name: tc.name,
400
400
  arguments: tc.arguments ?? {},
401
- result: errorResult,
401
+ result: dispatch.summarizeToolResult(errorResult),
402
402
  kind: 'write',
403
403
  iter,
404
404
  outcome: 'rejected'
@@ -416,7 +416,7 @@ iter) {
416
416
  toolUsages.push({
417
417
  name: tc.name,
418
418
  arguments: tc.arguments ?? {},
419
- result: queuedResult,
419
+ result: dispatch.summarizeToolResult(queuedResult),
420
420
  kind: 'write',
421
421
  iter,
422
422
  outcome: 'queued'
@@ -440,7 +440,7 @@ iter) {
440
440
  toolUsages.push({
441
441
  name: tc.name,
442
442
  arguments: tc.arguments ?? {},
443
- result: queuedResult,
443
+ result: dispatch.summarizeToolResult(queuedResult),
444
444
  kind: 'write',
445
445
  iter,
446
446
  outcome: 'queued'
@@ -457,7 +457,7 @@ iter) {
457
457
  toolUsages.push({
458
458
  name: tc.name,
459
459
  arguments: tc.arguments ?? {},
460
- result: errResult,
460
+ result: dispatch.summarizeToolResult(errResult),
461
461
  kind: 'unknown',
462
462
  iter,
463
463
  outcome: 'error'
@@ -1 +1 @@
1
- {"version":3,"file":"agentic-loop.js","sourceRoot":"","sources":["../../../server/service/agentic-loop/agentic-loop.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * agentic-loop — multi-turn LLM ↔ tool 루프. **도메인 중립.**\n *\n * ── 왜 여기로 왔나 (2026-09-05) ─────────────────────────────────────────────\n * 같은 모양의 루프가 두 벌로 돌고 있었다(board-ai · twin-ai). 그리고 **안전 장치가 서로 반대로\n * 갈려 있었다.**\n *\n * board-ai AbortSignal · 반복실패 조기종료 · iter 로그 접지 가드 없음\n * twin-ai 접지 가드(checkGrounding) 중단 없음 · 조기종료 없음\n *\n * 어느 쪽도 둘 다 갖고 있지 않았다. 새 보호 장치가 들어올 때마다 둘 중 아무 데나 우연히\n * 떨어졌다는 뜻이고, 그래서 **같은 질문을 어느 문으로 던지느냐에 따라 안전이 달랐다** — 그 값을\n * 이미 한 번 치른 기록이 `board-ai/…/grounding.ts` 머리에 있다.\n *\n * 중복이 아니라 결함이다. 그래서 한 곳으로 모은다.\n *\n * 원래 board-ai 에 있던 파일이고, 그 머리에 「향후 split 의 자연 경계」라고 적혀 있었다 — 옮기라고\n * 지어진 자리였다.\n *\n * ── 도메인을 어떻게 뺐나 ────────────────────────────────────────────────────\n * 타입 매개변수 둘이다.\n *\n * TCtx 도구가 알아야 하는 것 한 뭉치. 루프는 **들여다보지 않고 나르기만** 한다\n * TOp 쌓이는 편집 op. 루프는 모양을 모른다\n *\n * 전에는 루프가 `currentBoard?.components` 로 문서 속을 한 번 들여다봤다. 그 한 줄이 「board 모델은\n * 이렇게 생겼다」를 이 파일에 심고 있었다. 검증기가 자기 문맥에서 꺼내 쓰도록 옮겼다 — 이제 이\n * 파일은 문서의 **모양을 모른다.**\n *\n * 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 들을 의존성 주입. 부르는 쪽은 본 모듈에 의존하지만 본\n * 모듈은 부르는 쪽의 어떤 import 도 모름.\n */\nimport { createHash } from 'node:crypto'\n\nimport debug from 'debug'\nimport type { AIImageMediaType, AIImagePart, AIMessage, AIToolCall, AIToolChatResult } from '../types.js'\nimport { retryTransient } from '../utils/index.js'\n\nconst log = debug('things-factory:ai:agentic-loop')\n\n// ── Public types ─────────────────────────────────────────────────\n\n/**\n * AI 가 응답을 만드는 동안 호출한 도구의 시간순 trace.\n *\n * UX 목적 — 사용자가 \"왜 이렇게 답했지\" 의문 가질 때 fold-able 박스에서 확인.\n * 디버그용 + 신뢰도 향상.\n */\nexport interface ToolUsage {\n /** 도구 이름 (예: 'getSelection', 'modifyBoard', 'addComponent') */\n name: string\n /** 호출 인자 JSON */\n arguments: any\n /** 도구 결과 — read tool 은 실제 값 (truncated 가능), write tool 은 queued 마커 */\n result: any\n /** read tool / write tool 분류 */\n kind: 'read' | 'write' | 'unknown'\n /**\n * 몇 번째 LLM 턴에서 불렀는가(0부터). 판단 과정을 단계로 읽을 수 있게 한다 —\n * \"조회 → 거절 → 정정 → 제안\" 이 한 턴 안의 일인지 여러 턴에 걸친 일인지가 진단의 절반이다.\n */\n iter?: number\n /**\n * 이 호출이 **어떻게 끝났는가** — 화면이 배지로 보여줄 판정.\n * ok 조회·실행이 정상 완료\n * rejected 도구가 거절(인자 누락·대상 없음 등). 무엇이 빠졌는지는 result 에 있다.\n * queued 보드 편집처럼 클라이언트에서 적용될 예정\n * proposed 조치 제안이 만들어졌다(실행 아님 — 사용자가 버튼을 누른다)\n * folded 같은 효과의 제안이 이미 있어 접혔다. **조용히 버리지 않는다** — 접힌 사실이 보여야\n * \"왜 카드가 하나뿐인가\" 를 추적할 수 있다.\n * error 알 수 없는 도구·검증 실패\n */\n outcome?: 'ok' | 'rejected' | 'queued' | 'proposed' | 'folded' | 'error'\n}\n\n/**\n * Loop 가 호출하는 dispatch 함수들. 모두 의존성 주입.\n *\n * assistant.ts 가 자기 자신의 함수 (isReadTool, executeReadTool 등) 를 그대로\n * 넘겨주는 구조. 본 모듈은 이 함수들의 *시그니처* 만 알면 됨.\n */\nexport interface ToolDispatchHelpers<TCtx = unknown, TOp = unknown> {\n isReadTool: (name: string) => boolean\n isWriteTool: (name: string) => boolean\n isActionTool: (name: string) => boolean\n /** read/external 도구 실행. **Promise 를 반환할 수 있다** — 호출부는 반드시 await 한다. */\n executeReadTool: (tc: AIToolCall, context: TCtx) => any | Promise<any>\n /**\n * 쓰기 도구 호출이 유효한가.\n *\n * **문맥을 통째로 받는다** — 전에는 루프가 `currentBoard?.components` 를 꺼내 넘겼고, 그 한 줄이\n * 문서의 모양을 이 파일에 심고 있었다. 꺼내는 일은 검증기 자신이 한다.\n */\n validateWriteToolCall: (tc: AIToolCall, context: TCtx) => { valid: boolean; errors?: any[]; suggestion?: string }\n /** 도구 호출을 쌓아 둘 op 로. 모양은 부르는 쪽의 것이다 — 루프는 나르기만 한다. */\n toolCallToOp: (tc: AIToolCall) => TOp | 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 * **첫 턴에 도구 호출을 강제**한다(toolChoice: 'required'). 두 번째 턴부터는 자유(auto).\n *\n * 왜: 접지 가드는 답이 지어낸 **식별자**를 잡지만, 식별자 없이 지어낸 **상황 서술**은 잡지 못한다\n * (\"지게차들이 바쁘게 움직이고 있다\" — 실제로 이 답이 나왔다). 그건 도구를 아예 부르지 않고 답한\n * 경우이고, 사후 검사로는 막을 수 없다. 첫 턴에 무엇이든 조회하게 만들면 근거 없이 상태를 단언할\n * 길이 없어진다 — 개념·사용법 질문도 문서 조회 도구로 답하므로 막히지 않는다.\n *\n * 대화면이 정한다(라이브 상태를 다루는 면에서만 켠다) — 여기서 질문 내용을 눈치로 분류하지 않는다.\n */\n requireToolOnFirstTurn?: boolean\n /**\n * **한도에 닿아도 알아낸 것을 버리지 않는다.** 기본 꺼짐.\n *\n * ── 왜 (twin-ai 에서 옮겨 옴, 2026-08-30 → 2026-09-05) ──────────────────────\n * 예전에는 한도에 닿으면 「질문을 좁혀 달라」 한 줄만 냈다. 그때까지 여러 번 조회해서 얻은\n * 사실이 있는데도 전부 버렸다 — 사용자는 기다린 시간만 쓰고 아무것도 못 받는다.\n *\n * 켜면 마지막으로 한 번 더 부른다. **이번에는 도구를 안 준다**(`toolChoice: 'none'`) — 그래야\n * 모델이 더 조회하지 않고 지금까지 받은 것으로 답한다. 못 채운 것이 있으면 그것을 말하게 한다.\n *\n * 그 호출마저 실패하면 원래대로 `max_iterations` 로 끝난다 — 마지막 바닥은 그대로다.\n *\n * ── 왜 루프가 하나 ─────────────────────────────────────────────────────────\n * 이 문제는 도메인과 무관하다. 어느 대화면이든 한도는 있고, 한도에 닿으면 같은 손실이 난다.\n * 두 곳에 두면 한 곳만 갖게 되고 — 실제로 그랬다(twin-ai 만 갖고 board-ai 는 없었다).\n */\n salvageOnMaxIterations?: boolean\n /**\n * 마무리 호출에 덧붙일 지시. 없으면 아래 기본 문장을 쓴다.\n *\n * 기본 문장은 도메인 중립이다 — 대화면이 자기 어휘로 바꾸고 싶으면 여기에 준다.\n */\n salvagePrompt?: string\n}\n\nexport interface AgenticLoopInput<TCtx = unknown, TOp = unknown> {\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<TCtx, TOp>\n /** 도구가 알아야 하는 것 한 뭉치 — 루프는 들여다보지 않고 나른다. */\n context: TCtx\n}\n\nexport interface AgenticLoopResult<TOp = unknown> {\n lastText: string | undefined\n accumulatedWriteCalls: AIToolCall[]\n accumulatedActions: TOp[]\n toolUsages: ToolUsage[]\n /**\n * LLM 에게 실제로 회신된 도구 결과 문자열(시간순) — **접지 판정의 근거집합**.\n *\n * `toolUsages[].result` 를 쓰면 안 된다: 그건 UI 표시용으로 압축된(잘린) 값이라 근거로 삼으면\n * 잘려 나간 식별자를 \"근거 없음\" 으로 오판한다. 여기 담기는 것은 tool_result 로 보낸 원문 그대로다.\n */\n groundedTexts: string[]\n /**\n * **제안** — 도구가 \"실행하지 않고 제안만 했다\" 고 알린 결과들(시간순).\n *\n * 규약: read/external 도구의 결과에 `proposed === true` 가 있으면 그것은 조치 제안이다. 그 도구가\n * 어느 도메인인지 이 루프는 모른다 — 결과를 그대로 실어 올리고, 해석·실행 확인은 대화면이 한다.\n * 되돌릴 수 없는 조치를 AI 가 직접 실행하지 않게 하는 경계가 이 채널이다(사람이 버튼을 누른다).\n */\n proposals: any[]\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; salvaged?: boolean }\n | { type: 'repeated_validation_failure'; toolName: string; consecutive: number; iter: number }\n}\n\n// ── Loop ─────────────────────────────────────────────────────────\n\nconst DEFAULT_MAX_ITERATIONS = 8\n\n/**\n * The wrap-up call's default instruction — domain neutral, and in English because the reader is the model.\n * The reply it produces is in the language the person wrote in; that is the model's job, not this string's.\n */\nconst DEFAULT_SALVAGE_PROMPT =\n '[wrap up] No more lookups are possible. Answer from the tool output you already have. ' +\n 'State what you found first, then say in one line what is still missing to answer the rest.'\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<TCtx = unknown, TOp = unknown>(\n input: AgenticLoopInput<TCtx, TOp>\n): Promise<AgenticLoopResult<TOp>> {\n const {\n initialConversation,\n tools,\n options,\n chat,\n dispatch,\n context\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: TOp[] = []\n const toolUsages: ToolUsage[] = []\n /* 접지 근거 — tool_result 로 LLM 에 보낸 원문(압축 전). 호출부가 답의 접지를 검사한다. */\n const groundedTexts: string[] = []\n /* 제안 — 도구가 실행하지 않고 제안만 한 것들. 실행 버튼은 대화면이 그린다. */\n const proposals: any[] = []\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: calling chat()', iter)\n let result: AIToolChatResult\n try {\n /* (2) Error boundary — provider 예외 시 graceful 종료.\n *\n * 일시 장애(429·5xx·네트워크)는 **다시 시도한다**: 모델 서비스는 가끔 잠깐 죽고(실제로 Gemini\n * 503 으로 대화가 끊겼다), 그때 한 번도 다시 걸지 않는 것은 우리 쪽 손해다. 요청·자격 오류는\n * 즉시 그대로 올린다(조용히 더 시도하면 원인이 늦게 드러난다). 판정·대기는 ai-client-base 가 소유. */\n result = await retryTransient(() => chat(conversation, tools, {\n systemPrompt: options.systemPrompt,\n model: options.model,\n temperature: options.temperature,\n maxTokens: options.maxTokens,\n /* 첫 턴만 강제 — 이후는 자유. 계속 강제하면 조회 결과를 받아 놓고도 답을 못 쓰고 도구를 또 부른다. */\n toolChoice: iter === 0 && options.requireToolOnFirstTurn ? 'required' : 'auto',\n allowParallelToolCalls: true,\n signal\n }), {\n onRetry: ({ attempt, delayMs, error }) =>\n log('iter %d: transient failure, retry %d in %dms: %s', iter, attempt, delayMs, error?.message)\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) 삭제: chat() 직후 abort 는 다음 iter 시작 시 (1) 에서 감지.\n // 현재 iter tool 실행을 중단하면 read tool 결과도 버려져 테스트/동작 불일치.\n\n // tool 결과 빌드 — read 는 즉시 실행, write 는 누적, action 은 별도 채널\n const beforeUsages = toolUsages.length\n const toolResultParts = await processToolCalls(\n result.toolCalls,\n context,\n dispatch,\n accumulatedWriteCalls,\n accumulatedActions,\n toolUsages,\n proposals,\n iter\n )\n\n /* 접지 근거 누적 — 모델이 tool_result 로 받은 원문 그대로. 압축 전 값이어야 한다(위 필드 주석). */\n for (const part of toolResultParts) {\n if (typeof part?.content === 'string') groundedTexts.push(part.content)\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 * 버리기 전에 한 번 더 — 도구 없이. 실패하면 원래대로 한도 사유만 남는다(바닥은 그대로).\n */\n if (options.salvageOnMaxIterations) {\n try {\n const salvaged = await chat(conversation, tools, {\n systemPrompt: `${options.systemPrompt}\\n\\n${options.salvagePrompt ?? DEFAULT_SALVAGE_PROMPT}`,\n model: options.model,\n temperature: options.temperature,\n maxTokens: options.maxTokens,\n toolChoice: 'none',\n signal\n })\n if (salvaged?.text) {\n lastText = salvaged.text\n /* The wrap-up answered from what was found and says what is missing; no notice need follow it. */\n abortReason = { type: 'max_iterations', iter, salvaged: true }\n stopReason = salvaged.stopReason ?? stopReason\n log('iter %d: salvaged a final answer without tools', iter)\n }\n } catch (e: any) {\n /* 마무리마저 실패했다 — 알리되 삼키지 않는다. 사유는 이미 max_iterations 로 남아 있다. */\n log('iter %d: salvage call failed — %s', iter, e?.message ?? e)\n }\n }\n }\n\n return {\n lastText,\n accumulatedWriteCalls,\n accumulatedActions,\n toolUsages,\n groundedTexts,\n proposals,\n stopReason,\n abortReason\n }\n}\n\n/**\n * 제안의 **효과 키** — 도구·명령·대상·인자만 본다(설명 문구는 제외).\n *\n * 인자는 **키 순서를 정렬**해 문자열로 만든다: 같은 인자를 다른 순서로 받았을 뿐인데 다른 제안으로\n * 보이면 카드가 둘이 되고, 사용자가 같은 명령을 두 번 실행할 수 있다.\n */\nexport function proposalEffectKey(p: any): string {\n /*\n * 카드에만 있는 것은 효과가 아니다 — 문구가 다르다고 다른 조치가 되지 않는다. `key` 도 뺀다:\n * 이 함수가 만든 값이 다음 계산의 입력이 되면 키가 키를 낳는다.\n */\n const presentation = new Set(['label', 'reason', 'icon', 'choices', 'key', 'proposed', 'source', 'autoStage'])\n const effect: Record<string, any> = {}\n for (const [name, value] of Object.entries(p ?? {})) {\n if (!presentation.has(name)) effect[name] = value\n }\n /*\n * ── 왜 인자를 접어 넣나 ───────────────────────────────────────────────────\n * 이 키는 **사람이 읽는 자리로 나간다.** 호스트가 제안의 결말을 알리면 서버가 그것을 system 행으로\n * 적고(`formatProposalOutcomeLine`), 그 행이 대화에 남는다. 인자를 그대로 이어 붙였더니 트윈\n * 모델러에서 이런 줄이 남았다 — `proposal proposeSceneEdit({\"kind\":\"scene-edit\",\"ops\":[{\"component\":\n * {\"height\":180,\"left\":100,…`. 새로 고쳐도 그대로다(2026-09-18, Twin 레인 :3000).\n *\n * 도구 이름은 남긴다 — 무엇에 대한 줄인지는 사람도 모델도 읽어야 한다. 인자는 같고 다름만 가리면\n * 되므로 짧은 요약으로 접는다. 키가 하는 일은 그것뿐이다.\n */\n const digest = createHash('sha1').update(stableStringify(effect)).digest('hex').slice(0, 12)\n return `${p?.tool ?? ''}#${digest}`\n}\n\n/** 키 정렬 직렬화 — 순서에 흔들리지 않는 비교를 위해. */\nfunction stableStringify(value: any): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value ?? null)\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`\n const keys = Object.keys(value).sort()\n return `{${keys.map(k => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`\n}\n\n/**\n * 같은 tool 의 validation 실패 연속 감지 — pure helper.\n *\n * 같은 tool 이 반복 fail 하면 LLM 이 self-correct 못 하는 상태.\n * 다른 tool 이 fail 하면 시퀀스 새로 시작 (전혀 다른 패턴이라).\n * tool 이 succeed 하면 카운터 reset.\n */\n/** What a read tool that threw answers, in place of its result. */\nexport const TOOL_FAILED = 'TOOL_FAILED'\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 거절), and read tools that threw\n const failed = newUsages.filter(\n u =>\n (u.kind === 'write' && (u.result as any)?.error === 'Tool args failed validation') ||\n (u.kind === 'read' && (u.result as any)?.error === TOOL_FAILED)\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<TCtx, TOp>(\n toolCalls: AIToolCall[],\n context: TCtx,\n dispatch: ToolDispatchHelpers<TCtx, TOp>,\n accumulatedWriteCalls: AIToolCall[],\n accumulatedActions: TOp[],\n toolUsages: ToolUsage[],\n proposals: any[],\n /** 몇 번째 LLM 턴인가 — 기록에 남겨 화면이 단계로 읽을 수 있게 한다. */\n iter: number\n): Promise<any[]> {\n const toolResultParts: any[] = []\n const toolResultImages: AIImagePart[] = []\n\n for (const tc of toolCalls) {\n if (dispatch.isReadTool(tc.name)) {\n /*\n * A tool that throws has answered: the model reads why, and says it or tries another way (figure audit\n * 2026-10-16 N1). Left to rise, the exception ended the whole turn and the person saw its raw message instead of\n * an answer. A stop asked for while the tool ran is still seen at the top of the next iteration.\n */\n let executed: any\n try {\n executed = await dispatch.executeReadTool(tc, context)\n } catch (e: any) {\n const failure = { error: TOOL_FAILED, message: String(e?.message ?? e) }\n toolResultParts.push({ type: 'tool_result', toolUseId: tc.id, content: JSON.stringify(failure), isError: true })\n toolUsages.push({ name: tc.name, arguments: tc.arguments ?? {}, result: failure, kind: 'read', iter, outcome: 'error' })\n continue\n }\n /* await 필수 — registry 에 등록된 read/external builder 는 async 일 수 있다(DB·외부 호출).\n * await 없이 JSON.stringify 하면 Promise 가 '{}' 로 직렬화되어 LLM 이 **빈 결과**를 받는다\n * (문서 가져오기·트윈 구조 조회 등 — 무엇인지는 부르는 쪽이 안다). */\n /* Pictures a read tool hands back ride as image parts after the results - never inside the JSON. */\n const { value: given, images } = takeToolImages(executed)\n toolResultImages.push(...images)\n /* What only the card needs (a table of a thousand rows) goes to the screen, never to the model. */\n const { value, forScreen } = takeForScreen(given)\n /* 제안 규약 — 결과가 `proposed: true` 면 조치 제안이다(도메인은 모른 채 그대로 실어 올린다).\n * 이 채널이 없으면 대화면은 도구 추적(trace)을 뒤져야 하고, 실행 버튼을 만들 근거가 없다. */\n /* 판정(outcome) — 화면이 단계별 배지로 보여줄 값. 조용한 무동작을 남기지 않는다. */\n let outcome: ToolUsage['outcome'] = 'ok'\n if (value && typeof value === 'object' && (value as any).proposed === true) {\n /* **같은 조치는 한 번만** — 모델이 같은 것을 두 번 제안하는 일이 실제로 있었다(하나는 인자가\n * 비어 있었다). 카드가 둘이면 사용자가 같은 명령을 두 번 실행할 수 있고, 그건 되돌릴 수 없다.\n * 판정은 **효과**(도구·명령·대상·인자)로 한다 — 설명 문구(label·reason)가 달라도 같은 조치다. */\n const candidate = { tool: tc.name, ...(value as any), ...forScreen }\n const effectKey = proposalEffectKey(candidate)\n if (proposals.some(p => p.effectKey === effectKey)) {\n outcome = 'folded' // 접힌 사실을 남긴다 — \"왜 카드가 하나뿐인가\" 가 추적 가능해야 한다\n } else {\n /*\n * 키를 함께 실어 보낸다 — 카드가 같은 규칙으로 다시 세지 않게(원본은 하나).\n *\n * **도구가 이름을 준 제안은 그 이름을 쓴다**(2026-10-02, Figure 레인 · AI 레인 합의). figure-service 의\n * 도구는 제안에 UUID `key` 를 붙이고, 모델러는 그 UUID 로 새 도형을 만든다(ADR-0097 5.6절 5항). 효과 키로\n * 덮어쓰면 「제안 식별자가 올바르지 않습니다」로 실행이 막혔다. 같은 조치인지는 효과 키(`effectKey`)로\n * 따로 판정하므로 접기는 그대로다. 도구 `key` 는 **한 대화 안에서 유일해야 한다** — 겹치면 입력안의 「새\n * 카드가 옛 카드를 대신함」 판정과 결말 줄이 섞인다. 이름을 주지 않는 도구(ai-assistant 의 제안 도구들)는\n * 지금처럼 효과 키를 받는다.\n */\n proposals.push({ ...candidate, key: typeof (candidate as any).key === 'string' && (candidate as any).key ? (candidate as any).key : effectKey, effectKey })\n outcome = 'proposed'\n }\n } else if (value && typeof value === 'object' && ((value as any).rejected === true || (value as any).error)) {\n outcome = 'rejected'\n }\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 iter,\n outcome\n })\n } else if (dispatch.isWriteTool(tc.name)) {\n // 사전 검증 — args 가 schema / build 함수 검증 통과해야 누적.\n // 실패 시 LLM 한테 detailed error 반환 → 다음 turn 에서 자기 회복 (retry).\n const validation = dispatch.validateWriteToolCall(tc, context)\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 iter,\n outcome: 'rejected'\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 iter,\n outcome: 'queued'\n })\n }\n } else if (dispatch.isActionTool(tc.name)) {\n // ephemeral scene 조작 — accumulatedActions 로 누적, ChatResponse.actions 로 client 전달\n const action = dispatch.toolCallToOp(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 iter,\n outcome: 'queued'\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 iter,\n outcome: 'error'\n })\n }\n }\n\n /* Every provider takes a picture after the results of the same turn (design/ai/shared-dock.md §9-4). */\n return [...toolResultParts, ...toolResultImages]\n}\n\n/**\n * A proposal's reserved `forScreen` field, taken out of what the model reads and kept for the card.\n *\n * A read tool that has queried many rows puts them on the card (the AI 작업판 table) but tells the model only the\n * count, the totals and a few rows - the model does not copy a table it never received, so the card's numbers are\n * the query's own (operato-application/design/ai/read-targets.md §4). Only a proposal (`proposed: true`) carries it.\n */\nexport function takeForScreen(result: any): { value: any; forScreen: Record<string, unknown> } {\n if (!result || typeof result !== 'object' || Array.isArray(result) || result.proposed !== true || !result.forScreen || typeof result.forScreen !== 'object') {\n return { value: result, forScreen: {} }\n }\n const { forScreen, ...value } = result\n return { value, forScreen }\n}\n\nconst TOOL_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp']\n\n/**\n * The pictures a read tool returned in its reserved `images` field, taken out of the result.\n *\n * A tool result reaches the model as JSON text; a picture cannot. So a read tool that must show one - a picture\n * sent in an earlier turn of the conversation - returns `images: [{ data, mediaType }]`, and the loop sends each as an\n * image part of the same turn. The bytes never stay in the JSON, the grounding texts or the usage record. A picture\n * of a type the providers do not all take is left out and said in the result.\n */\nexport function takeToolImages(result: any): { value: any; images: AIImagePart[] } {\n if (!result || typeof result !== 'object' || Array.isArray(result) || !Array.isArray(result.images)) return { value: result, images: [] }\n const { images: given, ...value } = result\n const images: AIImagePart[] = []\n let left = 0\n for (const one of given) {\n const data = typeof one?.data === 'string' ? Buffer.from(one.data, 'base64') : one?.data\n const mediaType = String(one?.mediaType ?? '').toLowerCase()\n if ((Buffer.isBuffer(data) || data instanceof Uint8Array) && data.length && TOOL_IMAGE_TYPES.includes(mediaType)) {\n images.push({ type: 'image', source: { kind: 'base64', data, mediaType: mediaType as AIImageMediaType } })\n } else left++\n }\n return {\n value: { ...value, imagesAttached: images.length, ...(left ? { imagesLeftOut: left } : {}) },\n images\n }\n}\n"]}
1
+ {"version":3,"file":"agentic-loop.js","sourceRoot":"","sources":["../../../server/service/agentic-loop/agentic-loop.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * agentic-loop — multi-turn LLM ↔ tool 루프. **도메인 중립.**\n *\n * ── 왜 여기로 왔나 (2026-09-05) ─────────────────────────────────────────────\n * 같은 모양의 루프가 두 벌로 돌고 있었다(board-ai · twin-ai). 그리고 **안전 장치가 서로 반대로\n * 갈려 있었다.**\n *\n * board-ai AbortSignal · 반복실패 조기종료 · iter 로그 접지 가드 없음\n * twin-ai 접지 가드(checkGrounding) 중단 없음 · 조기종료 없음\n *\n * 어느 쪽도 둘 다 갖고 있지 않았다. 새 보호 장치가 들어올 때마다 둘 중 아무 데나 우연히\n * 떨어졌다는 뜻이고, 그래서 **같은 질문을 어느 문으로 던지느냐에 따라 안전이 달랐다** — 그 값을\n * 이미 한 번 치른 기록이 `board-ai/…/grounding.ts` 머리에 있다.\n *\n * 중복이 아니라 결함이다. 그래서 한 곳으로 모은다.\n *\n * 원래 board-ai 에 있던 파일이고, 그 머리에 「향후 split 의 자연 경계」라고 적혀 있었다 — 옮기라고\n * 지어진 자리였다.\n *\n * ── 도메인을 어떻게 뺐나 ────────────────────────────────────────────────────\n * 타입 매개변수 둘이다.\n *\n * TCtx 도구가 알아야 하는 것 한 뭉치. 루프는 **들여다보지 않고 나르기만** 한다\n * TOp 쌓이는 편집 op. 루프는 모양을 모른다\n *\n * 전에는 루프가 `currentBoard?.components` 로 문서 속을 한 번 들여다봤다. 그 한 줄이 「board 모델은\n * 이렇게 생겼다」를 이 파일에 심고 있었다. 검증기가 자기 문맥에서 꺼내 쓰도록 옮겼다 — 이제 이\n * 파일은 문서의 **모양을 모른다.**\n *\n * 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 들을 의존성 주입. 부르는 쪽은 본 모듈에 의존하지만 본\n * 모듈은 부르는 쪽의 어떤 import 도 모름.\n */\nimport { createHash } from 'node:crypto'\n\nimport debug from 'debug'\nimport type { AIImageMediaType, AIImagePart, AIMessage, AIToolCall, AIToolChatResult } from '../types.js'\nimport { retryTransient } from '../utils/index.js'\n\nconst log = debug('things-factory:ai:agentic-loop')\n\n// ── Public types ─────────────────────────────────────────────────\n\n/**\n * AI 가 응답을 만드는 동안 호출한 도구의 시간순 trace.\n *\n * UX 목적 — 사용자가 \"왜 이렇게 답했지\" 의문 가질 때 fold-able 박스에서 확인.\n * 디버그용 + 신뢰도 향상.\n */\nexport interface ToolUsage {\n /** 도구 이름 (예: 'getSelection', 'modifyBoard', 'addComponent') */\n name: string\n /** 호출 인자 JSON */\n arguments: any\n /** 도구 결과 — read tool 은 실제 값 (truncated 가능), write tool 은 queued 마커 */\n result: any\n /** read tool / write tool 분류 */\n kind: 'read' | 'write' | 'unknown'\n /**\n * 몇 번째 LLM 턴에서 불렀는가(0부터). 판단 과정을 단계로 읽을 수 있게 한다 —\n * \"조회 → 거절 → 정정 → 제안\" 이 한 턴 안의 일인지 여러 턴에 걸친 일인지가 진단의 절반이다.\n */\n iter?: number\n /**\n * 이 호출이 **어떻게 끝났는가** — 화면이 배지로 보여줄 판정.\n * ok 조회·실행이 정상 완료\n * rejected 도구가 거절(인자 누락·대상 없음 등). 무엇이 빠졌는지는 result 에 있다.\n * queued 보드 편집처럼 클라이언트에서 적용될 예정\n * proposed 조치 제안이 만들어졌다(실행 아님 — 사용자가 버튼을 누른다)\n * folded 같은 효과의 제안이 이미 있어 접혔다. **조용히 버리지 않는다** — 접힌 사실이 보여야\n * \"왜 카드가 하나뿐인가\" 를 추적할 수 있다.\n * error 알 수 없는 도구·검증 실패\n */\n outcome?: 'ok' | 'rejected' | 'queued' | 'proposed' | 'folded' | 'error'\n}\n\n/**\n * Loop 가 호출하는 dispatch 함수들. 모두 의존성 주입.\n *\n * assistant.ts 가 자기 자신의 함수 (isReadTool, executeReadTool 등) 를 그대로\n * 넘겨주는 구조. 본 모듈은 이 함수들의 *시그니처* 만 알면 됨.\n */\nexport interface ToolDispatchHelpers<TCtx = unknown, TOp = unknown> {\n isReadTool: (name: string) => boolean\n isWriteTool: (name: string) => boolean\n isActionTool: (name: string) => boolean\n /** read/external 도구 실행. **Promise 를 반환할 수 있다** — 호출부는 반드시 await 한다. */\n executeReadTool: (tc: AIToolCall, context: TCtx) => any | Promise<any>\n /**\n * 쓰기 도구 호출이 유효한가.\n *\n * **문맥을 통째로 받는다** — 전에는 루프가 `currentBoard?.components` 를 꺼내 넘겼고, 그 한 줄이\n * 문서의 모양을 이 파일에 심고 있었다. 꺼내는 일은 검증기 자신이 한다.\n */\n validateWriteToolCall: (tc: AIToolCall, context: TCtx) => { valid: boolean; errors?: any[]; suggestion?: string }\n /** 도구 호출을 쌓아 둘 op 로. 모양은 부르는 쪽의 것이다 — 루프는 나르기만 한다. */\n toolCallToOp: (tc: AIToolCall) => TOp | 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 * **첫 턴에 도구 호출을 강제**한다(toolChoice: 'required'). 두 번째 턴부터는 자유(auto).\n *\n * 왜: 접지 가드는 답이 지어낸 **식별자**를 잡지만, 식별자 없이 지어낸 **상황 서술**은 잡지 못한다\n * (\"지게차들이 바쁘게 움직이고 있다\" — 실제로 이 답이 나왔다). 그건 도구를 아예 부르지 않고 답한\n * 경우이고, 사후 검사로는 막을 수 없다. 첫 턴에 무엇이든 조회하게 만들면 근거 없이 상태를 단언할\n * 길이 없어진다 — 개념·사용법 질문도 문서 조회 도구로 답하므로 막히지 않는다.\n *\n * 대화면이 정한다(라이브 상태를 다루는 면에서만 켠다) — 여기서 질문 내용을 눈치로 분류하지 않는다.\n */\n requireToolOnFirstTurn?: boolean\n /**\n * **한도에 닿아도 알아낸 것을 버리지 않는다.** 기본 꺼짐.\n *\n * ── 왜 (twin-ai 에서 옮겨 옴, 2026-08-30 → 2026-09-05) ──────────────────────\n * 예전에는 한도에 닿으면 「질문을 좁혀 달라」 한 줄만 냈다. 그때까지 여러 번 조회해서 얻은\n * 사실이 있는데도 전부 버렸다 — 사용자는 기다린 시간만 쓰고 아무것도 못 받는다.\n *\n * 켜면 마지막으로 한 번 더 부른다. **이번에는 도구를 안 준다**(`toolChoice: 'none'`) — 그래야\n * 모델이 더 조회하지 않고 지금까지 받은 것으로 답한다. 못 채운 것이 있으면 그것을 말하게 한다.\n *\n * 그 호출마저 실패하면 원래대로 `max_iterations` 로 끝난다 — 마지막 바닥은 그대로다.\n *\n * ── 왜 루프가 하나 ─────────────────────────────────────────────────────────\n * 이 문제는 도메인과 무관하다. 어느 대화면이든 한도는 있고, 한도에 닿으면 같은 손실이 난다.\n * 두 곳에 두면 한 곳만 갖게 되고 — 실제로 그랬다(twin-ai 만 갖고 board-ai 는 없었다).\n */\n salvageOnMaxIterations?: boolean\n /**\n * 마무리 호출에 덧붙일 지시. 없으면 아래 기본 문장을 쓴다.\n *\n * 기본 문장은 도메인 중립이다 — 대화면이 자기 어휘로 바꾸고 싶으면 여기에 준다.\n */\n salvagePrompt?: string\n}\n\nexport interface AgenticLoopInput<TCtx = unknown, TOp = unknown> {\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<TCtx, TOp>\n /** 도구가 알아야 하는 것 한 뭉치 — 루프는 들여다보지 않고 나른다. */\n context: TCtx\n}\n\nexport interface AgenticLoopResult<TOp = unknown> {\n lastText: string | undefined\n accumulatedWriteCalls: AIToolCall[]\n accumulatedActions: TOp[]\n toolUsages: ToolUsage[]\n /**\n * LLM 에게 실제로 회신된 도구 결과 문자열(시간순) — **접지 판정의 근거집합**.\n *\n * `toolUsages[].result` 를 쓰면 안 된다: 그건 UI 표시용으로 압축된(잘린) 값이라 근거로 삼으면\n * 잘려 나간 식별자를 \"근거 없음\" 으로 오판한다. 여기 담기는 것은 tool_result 로 보낸 원문 그대로다.\n */\n groundedTexts: string[]\n /**\n * **제안** — 도구가 \"실행하지 않고 제안만 했다\" 고 알린 결과들(시간순).\n *\n * 규약: read/external 도구의 결과에 `proposed === true` 가 있으면 그것은 조치 제안이다. 그 도구가\n * 어느 도메인인지 이 루프는 모른다 — 결과를 그대로 실어 올리고, 해석·실행 확인은 대화면이 한다.\n * 되돌릴 수 없는 조치를 AI 가 직접 실행하지 않게 하는 경계가 이 채널이다(사람이 버튼을 누른다).\n */\n proposals: any[]\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; salvaged?: boolean }\n | { type: 'repeated_validation_failure'; toolName: string; consecutive: number; iter: number }\n}\n\n// ── Loop ─────────────────────────────────────────────────────────\n\nconst DEFAULT_MAX_ITERATIONS = 8\n\n/**\n * The wrap-up call's default instruction — domain neutral, and in English because the reader is the model.\n * The reply it produces is in the language the person wrote in; that is the model's job, not this string's.\n */\nconst DEFAULT_SALVAGE_PROMPT =\n '[wrap up] No more lookups are possible. Answer from the tool output you already have. ' +\n 'State what you found first, then say in one line what is still missing to answer the rest.'\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<TCtx = unknown, TOp = unknown>(\n input: AgenticLoopInput<TCtx, TOp>\n): Promise<AgenticLoopResult<TOp>> {\n const {\n initialConversation,\n tools,\n options,\n chat,\n dispatch,\n context\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: TOp[] = []\n const toolUsages: ToolUsage[] = []\n /* 접지 근거 — tool_result 로 LLM 에 보낸 원문(압축 전). 호출부가 답의 접지를 검사한다. */\n const groundedTexts: string[] = []\n /* 제안 — 도구가 실행하지 않고 제안만 한 것들. 실행 버튼은 대화면이 그린다. */\n const proposals: any[] = []\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: calling chat()', iter)\n let result: AIToolChatResult\n try {\n /* (2) Error boundary — provider 예외 시 graceful 종료.\n *\n * 일시 장애(429·5xx·네트워크)는 **다시 시도한다**: 모델 서비스는 가끔 잠깐 죽고(실제로 Gemini\n * 503 으로 대화가 끊겼다), 그때 한 번도 다시 걸지 않는 것은 우리 쪽 손해다. 요청·자격 오류는\n * 즉시 그대로 올린다(조용히 더 시도하면 원인이 늦게 드러난다). 판정·대기는 ai-client-base 가 소유. */\n result = await retryTransient(() => chat(conversation, tools, {\n systemPrompt: options.systemPrompt,\n model: options.model,\n temperature: options.temperature,\n maxTokens: options.maxTokens,\n /* 첫 턴만 강제 — 이후는 자유. 계속 강제하면 조회 결과를 받아 놓고도 답을 못 쓰고 도구를 또 부른다. */\n toolChoice: iter === 0 && options.requireToolOnFirstTurn ? 'required' : 'auto',\n allowParallelToolCalls: true,\n signal\n }), {\n onRetry: ({ attempt, delayMs, error }) =>\n log('iter %d: transient failure, retry %d in %dms: %s', iter, attempt, delayMs, error?.message)\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) 삭제: chat() 직후 abort 는 다음 iter 시작 시 (1) 에서 감지.\n // 현재 iter tool 실행을 중단하면 read tool 결과도 버려져 테스트/동작 불일치.\n\n // tool 결과 빌드 — read 는 즉시 실행, write 는 누적, action 은 별도 채널\n const beforeUsages = toolUsages.length\n const toolResultParts = await processToolCalls(\n result.toolCalls,\n context,\n dispatch,\n accumulatedWriteCalls,\n accumulatedActions,\n toolUsages,\n proposals,\n iter\n )\n\n /* 접지 근거 누적 — 모델이 tool_result 로 받은 원문 그대로. 압축 전 값이어야 한다(위 필드 주석). */\n for (const part of toolResultParts) {\n if (typeof part?.content === 'string') groundedTexts.push(part.content)\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 * 버리기 전에 한 번 더 — 도구 없이. 실패하면 원래대로 한도 사유만 남는다(바닥은 그대로).\n */\n if (options.salvageOnMaxIterations) {\n try {\n const salvaged = await chat(conversation, tools, {\n systemPrompt: `${options.systemPrompt}\\n\\n${options.salvagePrompt ?? DEFAULT_SALVAGE_PROMPT}`,\n model: options.model,\n temperature: options.temperature,\n maxTokens: options.maxTokens,\n toolChoice: 'none',\n signal\n })\n if (salvaged?.text) {\n lastText = salvaged.text\n /* The wrap-up answered from what was found and says what is missing; no notice need follow it. */\n abortReason = { type: 'max_iterations', iter, salvaged: true }\n stopReason = salvaged.stopReason ?? stopReason\n log('iter %d: salvaged a final answer without tools', iter)\n }\n } catch (e: any) {\n /* 마무리마저 실패했다 — 알리되 삼키지 않는다. 사유는 이미 max_iterations 로 남아 있다. */\n log('iter %d: salvage call failed — %s', iter, e?.message ?? e)\n }\n }\n }\n\n return {\n lastText,\n accumulatedWriteCalls,\n accumulatedActions,\n toolUsages,\n groundedTexts,\n proposals,\n stopReason,\n abortReason\n }\n}\n\n/**\n * 제안의 **효과 키** — 도구·명령·대상·인자만 본다(설명 문구는 제외).\n *\n * 인자는 **키 순서를 정렬**해 문자열로 만든다: 같은 인자를 다른 순서로 받았을 뿐인데 다른 제안으로\n * 보이면 카드가 둘이 되고, 사용자가 같은 명령을 두 번 실행할 수 있다.\n */\nexport function proposalEffectKey(p: any): string {\n /*\n * 카드에만 있는 것은 효과가 아니다 — 문구가 다르다고 다른 조치가 되지 않는다. `key` 도 뺀다:\n * 이 함수가 만든 값이 다음 계산의 입력이 되면 키가 키를 낳는다.\n */\n const presentation = new Set(['label', 'reason', 'icon', 'choices', 'key', 'proposed', 'source', 'autoStage'])\n const effect: Record<string, any> = {}\n for (const [name, value] of Object.entries(p ?? {})) {\n if (!presentation.has(name)) effect[name] = value\n }\n /*\n * ── 왜 인자를 접어 넣나 ───────────────────────────────────────────────────\n * 이 키는 **사람이 읽는 자리로 나간다.** 호스트가 제안의 결말을 알리면 서버가 그것을 system 행으로\n * 적고(`formatProposalOutcomeLine`), 그 행이 대화에 남는다. 인자를 그대로 이어 붙였더니 트윈\n * 모델러에서 이런 줄이 남았다 — `proposal proposeSceneEdit({\"kind\":\"scene-edit\",\"ops\":[{\"component\":\n * {\"height\":180,\"left\":100,…`. 새로 고쳐도 그대로다(2026-09-18, Twin 레인 :3000).\n *\n * 도구 이름은 남긴다 — 무엇에 대한 줄인지는 사람도 모델도 읽어야 한다. 인자는 같고 다름만 가리면\n * 되므로 짧은 요약으로 접는다. 키가 하는 일은 그것뿐이다.\n */\n const digest = createHash('sha1').update(stableStringify(effect)).digest('hex').slice(0, 12)\n return `${p?.tool ?? ''}#${digest}`\n}\n\n/** 키 정렬 직렬화 — 순서에 흔들리지 않는 비교를 위해. */\nfunction stableStringify(value: any): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value ?? null)\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`\n const keys = Object.keys(value).sort()\n return `{${keys.map(k => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`\n}\n\n/**\n * 같은 tool 의 validation 실패 연속 감지 — pure helper.\n *\n * 같은 tool 이 반복 fail 하면 LLM 이 self-correct 못 하는 상태.\n * 다른 tool 이 fail 하면 시퀀스 새로 시작 (전혀 다른 패턴이라).\n * tool 이 succeed 하면 카운터 reset.\n */\n/** What a read tool that threw answers, in place of its result. */\nexport const TOOL_FAILED = 'TOOL_FAILED'\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 거절), and read tools that threw\n const failed = newUsages.filter(\n u =>\n (u.kind === 'write' && (u.result as any)?.error === 'Tool args failed validation') ||\n (u.kind === 'read' && (u.result as any)?.error === TOOL_FAILED)\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<TCtx, TOp>(\n toolCalls: AIToolCall[],\n context: TCtx,\n dispatch: ToolDispatchHelpers<TCtx, TOp>,\n accumulatedWriteCalls: AIToolCall[],\n accumulatedActions: TOp[],\n toolUsages: ToolUsage[],\n proposals: any[],\n /** 몇 번째 LLM 턴인가 — 기록에 남겨 화면이 단계로 읽을 수 있게 한다. */\n iter: number\n): Promise<any[]> {\n const toolResultParts: any[] = []\n const toolResultImages: AIImagePart[] = []\n\n for (const tc of toolCalls) {\n if (dispatch.isReadTool(tc.name)) {\n /*\n * A tool that throws has answered: the model reads why, and says it or tries another way (figure audit\n * 2026-10-16 N1). Left to rise, the exception ended the whole turn and the person saw its raw message instead of\n * an answer. A stop asked for while the tool ran is still seen at the top of the next iteration.\n */\n let executed: any\n try {\n executed = await dispatch.executeReadTool(tc, context)\n } catch (e: any) {\n const failure = { error: TOOL_FAILED, message: String(e?.message ?? e) }\n toolResultParts.push({ type: 'tool_result', toolUseId: tc.id, content: JSON.stringify(failure), isError: true })\n toolUsages.push({ name: tc.name, arguments: tc.arguments ?? {}, result: dispatch.summarizeToolResult(failure), kind: 'read', iter, outcome: 'error' })\n continue\n }\n /* await 필수 — registry 에 등록된 read/external builder 는 async 일 수 있다(DB·외부 호출).\n * await 없이 JSON.stringify 하면 Promise 가 '{}' 로 직렬화되어 LLM 이 **빈 결과**를 받는다\n * (문서 가져오기·트윈 구조 조회 등 — 무엇인지는 부르는 쪽이 안다). */\n /* Pictures a read tool hands back ride as image parts after the results - never inside the JSON. */\n const { value: given, images } = takeToolImages(executed)\n toolResultImages.push(...images)\n /* What only the card needs (a table of a thousand rows) goes to the screen, never to the model. */\n const { value, forScreen } = takeForScreen(given)\n /* 제안 규약 — 결과가 `proposed: true` 면 조치 제안이다(도메인은 모른 채 그대로 실어 올린다).\n * 이 채널이 없으면 대화면은 도구 추적(trace)을 뒤져야 하고, 실행 버튼을 만들 근거가 없다. */\n /* 판정(outcome) — 화면이 단계별 배지로 보여줄 값. 조용한 무동작을 남기지 않는다. */\n let outcome: ToolUsage['outcome'] = 'ok'\n if (value && typeof value === 'object' && (value as any).proposed === true) {\n /* **같은 조치는 한 번만** — 모델이 같은 것을 두 번 제안하는 일이 실제로 있었다(하나는 인자가\n * 비어 있었다). 카드가 둘이면 사용자가 같은 명령을 두 번 실행할 수 있고, 그건 되돌릴 수 없다.\n * 판정은 **효과**(도구·명령·대상·인자)로 한다 — 설명 문구(label·reason)가 달라도 같은 조치다. */\n const candidate = { tool: tc.name, ...(value as any), ...forScreen }\n const effectKey = proposalEffectKey(candidate)\n if (proposals.some(p => p.effectKey === effectKey)) {\n outcome = 'folded' // 접힌 사실을 남긴다 — \"왜 카드가 하나뿐인가\" 가 추적 가능해야 한다\n } else {\n /*\n * 키를 함께 실어 보낸다 — 카드가 같은 규칙으로 다시 세지 않게(원본은 하나).\n *\n * **도구가 이름을 준 제안은 그 이름을 쓴다**(2026-10-02, Figure 레인 · AI 레인 합의). figure-service 의\n * 도구는 제안에 UUID `key` 를 붙이고, 모델러는 그 UUID 로 새 도형을 만든다(ADR-0097 5.6절 5항). 효과 키로\n * 덮어쓰면 「제안 식별자가 올바르지 않습니다」로 실행이 막혔다. 같은 조치인지는 효과 키(`effectKey`)로\n * 따로 판정하므로 접기는 그대로다. 도구 `key` 는 **한 대화 안에서 유일해야 한다** — 겹치면 입력안의 「새\n * 카드가 옛 카드를 대신함」 판정과 결말 줄이 섞인다. 이름을 주지 않는 도구(ai-assistant 의 제안 도구들)는\n * 지금처럼 효과 키를 받는다.\n */\n proposals.push({ ...candidate, key: typeof (candidate as any).key === 'string' && (candidate as any).key ? (candidate as any).key : effectKey, effectKey })\n outcome = 'proposed'\n }\n } else if (value && typeof value === 'object' && ((value as any).rejected === true || (value as any).error)) {\n outcome = 'rejected'\n }\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 iter,\n outcome\n })\n } else if (dispatch.isWriteTool(tc.name)) {\n // 사전 검증 — args 가 schema / build 함수 검증 통과해야 누적.\n // 실패 시 LLM 한테 detailed error 반환 → 다음 turn 에서 자기 회복 (retry).\n const validation = dispatch.validateWriteToolCall(tc, context)\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: dispatch.summarizeToolResult(errorResult),\n kind: 'write',\n iter,\n outcome: 'rejected'\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: dispatch.summarizeToolResult(queuedResult),\n kind: 'write',\n iter,\n outcome: 'queued'\n })\n }\n } else if (dispatch.isActionTool(tc.name)) {\n // ephemeral scene 조작 — accumulatedActions 로 누적, ChatResponse.actions 로 client 전달\n const action = dispatch.toolCallToOp(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: dispatch.summarizeToolResult(queuedResult),\n kind: 'write',\n iter,\n outcome: 'queued'\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: dispatch.summarizeToolResult(errResult),\n kind: 'unknown',\n iter,\n outcome: 'error'\n })\n }\n }\n\n /* Every provider takes a picture after the results of the same turn (design/ai/shared-dock.md §9-4). */\n return [...toolResultParts, ...toolResultImages]\n}\n\n/**\n * A proposal's reserved `forScreen` field, taken out of what the model reads and kept for the card.\n *\n * A read tool that has queried many rows puts them on the card (the AI 작업판 table) but tells the model only the\n * count, the totals and a few rows - the model does not copy a table it never received, so the card's numbers are\n * the query's own (operato-application/design/ai/read-targets.md §4). Only a proposal (`proposed: true`) carries it.\n */\nexport function takeForScreen(result: any): { value: any; forScreen: Record<string, unknown> } {\n if (!result || typeof result !== 'object' || Array.isArray(result) || result.proposed !== true || !result.forScreen || typeof result.forScreen !== 'object') {\n return { value: result, forScreen: {} }\n }\n const { forScreen, ...value } = result\n return { value, forScreen }\n}\n\nconst TOOL_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp']\n\n/**\n * The pictures a read tool returned in its reserved `images` field, taken out of the result.\n *\n * A tool result reaches the model as JSON text; a picture cannot. So a read tool that must show one - a picture\n * sent in an earlier turn of the conversation - returns `images: [{ data, mediaType }]`, and the loop sends each as an\n * image part of the same turn. The bytes never stay in the JSON, the grounding texts or the usage record. A picture\n * of a type the providers do not all take is left out and said in the result.\n */\nexport function takeToolImages(result: any): { value: any; images: AIImagePart[] } {\n if (!result || typeof result !== 'object' || Array.isArray(result) || !Array.isArray(result.images)) return { value: result, images: [] }\n const { images: given, ...value } = result\n const images: AIImagePart[] = []\n let left = 0\n for (const one of given) {\n const data = typeof one?.data === 'string' ? Buffer.from(one.data, 'base64') : one?.data\n const mediaType = String(one?.mediaType ?? '').toLowerCase()\n if ((Buffer.isBuffer(data) || data instanceof Uint8Array) && data.length && TOOL_IMAGE_TYPES.includes(mediaType)) {\n images.push({ type: 'image', source: { kind: 'base64', data, mediaType: mediaType as AIImageMediaType } })\n } else left++\n }\n return {\n value: { ...value, imagesAttached: images.length, ...(left ? { imagesLeftOut: left } : {}) },\n images\n }\n}\n"]}
@@ -93,13 +93,14 @@ async function registryAssistantChat(input) {
93
93
  return result;
94
94
  }
95
95
  catch (e) {
96
- await (0, call_recorder_js_1.recordToolCall)({ ...record, error: String(e?.message ?? e), durationMs: Date.now() - started }, context);
96
+ await (0, call_recorder_js_1.recordToolCall)({ ...record, error: e, durationMs: Date.now() - started }, context);
97
97
  throw e;
98
98
  }
99
99
  },
100
100
  validateWriteToolCall: () => ({ valid: false, errors: ['this conversation has no document to write to'] }),
101
101
  toolCallToOp: () => null,
102
- summarizeToolResult: summarize_js_1.summarizeToolResult
102
+ /* The conversation row keeps the shape of each answer, as the audit does - never its values (ADR-0061 decision 8). */
103
+ summarizeToolResult: summarize_js_1.resultShapeOf
103
104
  },
104
105
  groundingSources: input.groundingSources,
105
106
  t: input.t
@@ -1 +1 @@
1
- {"version":3,"file":"registry-assistant.js","sourceRoot":"","sources":["../../../server/service/assistant/registry-assistant.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A conversation for a product that has no editor of its own.\n *\n * ── What this is for ───────────────────────────────────────────────────────────\n * `board-ai` assembles its conversation from two sources: its own board editing tools, and\n * whatever other packages registered in the tool registry. The second half is the same work for\n * every product — read the specs, build the definitions, dispatch by name — but it only existed\n * inside the board assistant, so a product without a board had nowhere to ask a question from.\n * operato-figure hit exactly that: the dock was mounted, the tools were registered, and the only\n * mutation that could run them lived in a package figure does not load.\n *\n * ── Read only ──────────────────────────────────────────────────────────────────\n * A registry category may also carry `write` and `action` tools. Those produce edit ops for a\n * document the host owns, and a host with no document cannot apply them. Offering them here would\n * declare a door with no road behind it — the model would call one and the call would go nowhere.\n * `external` tools change something outside the conversation (board-import's\n * `materializeImportSession` creates a Board) and this door has no confirm-and-apply step, so they\n * are not offered either (A1, 2026-09-17). This runner offers, and dispatches, `read` only.\n *\n * The gate answers from the same list the definitions are built from, so what is offered and what\n * may be dispatched cannot drift apart.\n */\nimport type { AIClient, AIMessage } from '../types.js'\nimport { narrowToolScope, resolveToolScope, toolScopeGuidance, type ToolAccessCheck } from './tool-scope.js'\nimport { assistantChat, type AssistantChatResult } from './assistant-chat.js'\nimport { summarizeToolResult } from './summarize.js'\nimport type { GroundingSources } from '../utils/index.js'\nimport type { ToolAttachment, ToolCallContext } from '../tool-registry/types.js'\nimport { defaultValueTranslate } from '../tool-registry/translate.js'\nimport { recordToolCall } from '../tool-registry/call-recorder.js'\n\n\n/** Lookups a registry conversation may take before it answers from what it has (operato-twin's assistant takes 16 too). */\nexport const REGISTRY_MAX_ITERATIONS = 16\n\nexport interface RegistryAssistantInput {\n /** Provider client. Must implement `chatWithTools`. */\n client: AIClient\n /** The conversation so far, newest last. The caller owns history and truncation. */\n messages: AIMessage[]\n /** Who the assistant is and what this surface is. The host owns this prose. */\n systemPrompt: string\n /**\n * Categories this conversation may use. **Omitting it allows nothing** — the same as an empty\n * list, a plain conversation with no tools (A1, 2026-09-17). It used to mean every registered\n * category, which is how a Board-creating tool reached any conversation.\n */\n toolCategories?: string[]\n /**\n * Answers whether the caller holds a tool's declared privilege. Without it, every tool that\n * declares a privilege stays closed. The host builds it from the request — this package does\n * not know auth.\n */\n toolAccess?: ToolAccessCheck\n /** Passed to tools as `context.host`. Client-supplied — never used for authorization. */\n hostContext?: any\n /**\n * Which conversation this is — `{ kind, target }` from the registered mode (ADR-0061 decision 3).\n *\n * Tools see it as `context.mode`. It is the server's value, resolved from the registry before the\n * turn runs, so a tool that behaves differently in two conversations reads it here and not from\n * `host`, which the client fills.\n */\n mode?: { kind: string; target?: string }\n /** Caller's koa state (domain, user, tx). Tools that read the database need it. */\n state: any\n model?: string\n temperature?: number\n maxTokens?: number\n /** Make the model call a tool before it may answer. For surfaces that report live state. */\n requireToolOnFirstTurn?: boolean\n /**\n * The request's translator — the stop notice is then in the reader's language, and tools write what a person\n * reads with it (`context.t`). Without it tools get `defaultValueTranslate`: the English default values.\n */\n t?: (key: string, params?: Record<string, any>) => string\n /**\n * The files sent with this turn, after the caller's server checked them — stored with this conversation, the\n * conversation the caller's to write, the mode's policy met (ADR-0061, 2026-10-16 decision 2). Tools see them as\n * `context.attachments`. Never built from the client's request: what a client puts in `hostContext` stays there.\n */\n attachments?: ToolAttachment[]\n /** Texts the answer is allowed to draw identifiers from, besides the tool results. */\n groundingSources?: Omit<GroundingSources, 'toolResults'>\n}\n\nexport interface RegistryAssistantResult extends AssistantChatResult<unknown> {\n /** Tool names actually offered to the model this turn. The caller may log or return it. */\n offeredTools: string[]\n}\n\n/**\n * Run one turn of a conversation whose only tools come from the registry.\n *\n * Throws when the client cannot use tools at all — a surface built on registered tools that\n * silently ran without them would answer from nothing and look like it had checked.\n */\nexport async function registryAssistantChat(input: RegistryAssistantInput): Promise<RegistryAssistantResult> {\n const { client, messages, systemPrompt, toolCategories, hostContext, state } = input\n\n if (!client.chatWithTools) {\n throw new Error('[registry-assistant] the configured AI client cannot call tools, so this conversation cannot run')\n }\n\n /*\n * Only `read` is offered here. `external` means the tool changes something outside the\n * conversation (board-import's `materializeImportSession` creates a Board); this door answers\n * questions and has no confirm-and-apply step, so a tool with an effect does not go through it\n * even when the caller holds the privilege.\n */\n const scope = narrowToolScope(\n await resolveToolScope(toolCategories, input.toolAccess),\n spec => spec.kind === 'read'\n )\n const specs = scope.specs\n const byName = new Map(specs.map(spec => [spec.name, spec]))\n const categoryOf = new Map(scope.categories.flatMap(category => category.specs.map(spec => [spec.name, category.name] as [string, string])))\n\n const tools = specs.map(spec => ({\n name: spec.name,\n description: spec.themeNote ? `${spec.description} ${spec.themeNote}` : spec.description,\n parameters: spec.schema\n }))\n\n /*\n * The rules for using a tool belong to whoever registered it. Collect only the exposed ones, so\n * a surface never reads instructions for a category it cannot call.\n */\n const guidance = toolScopeGuidance(scope)\n const prompt = guidance.length > 0 ? `${systemPrompt}\\n\\n${guidance.join('\\n\\n')}` : systemPrompt\n\n /*\n * `mode` 는 서버의 값이다 — 등록된 모드에서 왔고, `host` 와 달리 클라이언트가 못 고친다. A2 가 서기\n * 전에는 채울 것이 없어 비워 두었다(ADR-0061 결정 3 의 주석). 이제 부르는 쪽이 들고 있다.\n */\n const toolContext: ToolCallContext = {\n state,\n host: hostContext ?? {},\n ...(input.mode ? { mode: input.mode } : {}),\n /* 번역기와 첨부는 서버의 값이다 — `host` 에서 받지 않는다(ADR-0061 2026-10-16 판정 1·2). */\n t: input.t ?? defaultValueTranslate,\n attachments: input.attachments ?? []\n }\n\n const result = await assistantChat<ToolCallContext, unknown>({\n initialConversation: messages,\n tools,\n options: {\n systemPrompt: prompt,\n model: input.model,\n temperature: input.temperature,\n maxTokens: input.maxTokens,\n requireToolOnFirstTurn: input.requireToolOnFirstTurn,\n /*\n * A question across a product's records takes many lookups. At the limit the model answers from\n * what it read and says what is missing — the person is never told to split the question.\n */\n maxIterations: REGISTRY_MAX_ITERATIONS,\n salvageOnMaxIterations: true\n },\n chat: (msgs, toolDefs, opts) => client.chatWithTools!(msgs, toolDefs, opts),\n context: toolContext,\n dispatch: {\n isReadTool: name => byName.has(name),\n /*\n * This surface has no document, so nothing here can write. Saying so plainly makes the loop\n * report an unknown name back to the model instead of accepting a call and dropping it.\n */\n isWriteTool: () => false,\n isActionTool: () => false,\n /* Every call is handed to the recorders - an application's audit sees the tools other packages declared too. */\n executeReadTool: async (call, context) => {\n const spec = byName.get(call.name)\n if (!spec) return undefined\n const args = call.arguments ?? {}\n const started = Date.now()\n const record = { tool: spec.name, category: categoryOf.get(spec.name) ?? '', args }\n try {\n const result = await (spec.builder as any)(args, context)\n await recordToolCall({ ...record, result, durationMs: Date.now() - started }, context)\n return result\n } catch (e) {\n await recordToolCall({ ...record, error: String((e as Error)?.message ?? e), durationMs: Date.now() - started }, context)\n throw e\n }\n },\n validateWriteToolCall: () => ({ valid: false, errors: ['this conversation has no document to write to'] }),\n toolCallToOp: () => null,\n summarizeToolResult\n },\n groundingSources: input.groundingSources,\n t: input.t\n })\n\n return { ...result, offeredTools: tools.map(tool => tool.name) }\n}\n"]}
1
+ {"version":3,"file":"registry-assistant.js","sourceRoot":"","sources":["../../../server/service/assistant/registry-assistant.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A conversation for a product that has no editor of its own.\n *\n * ── What this is for ───────────────────────────────────────────────────────────\n * `board-ai` assembles its conversation from two sources: its own board editing tools, and\n * whatever other packages registered in the tool registry. The second half is the same work for\n * every product — read the specs, build the definitions, dispatch by name — but it only existed\n * inside the board assistant, so a product without a board had nowhere to ask a question from.\n * operato-figure hit exactly that: the dock was mounted, the tools were registered, and the only\n * mutation that could run them lived in a package figure does not load.\n *\n * ── Read only ──────────────────────────────────────────────────────────────────\n * A registry category may also carry `write` and `action` tools. Those produce edit ops for a\n * document the host owns, and a host with no document cannot apply them. Offering them here would\n * declare a door with no road behind it — the model would call one and the call would go nowhere.\n * `external` tools change something outside the conversation (board-import's\n * `materializeImportSession` creates a Board) and this door has no confirm-and-apply step, so they\n * are not offered either (A1, 2026-09-17). This runner offers, and dispatches, `read` only.\n *\n * The gate answers from the same list the definitions are built from, so what is offered and what\n * may be dispatched cannot drift apart.\n */\nimport type { AIClient, AIMessage } from '../types.js'\nimport { narrowToolScope, resolveToolScope, toolScopeGuidance, type ToolAccessCheck } from './tool-scope.js'\nimport { assistantChat, type AssistantChatResult } from './assistant-chat.js'\nimport { resultShapeOf } from './summarize.js'\nimport type { GroundingSources } from '../utils/index.js'\nimport type { ToolAttachment, ToolCallContext } from '../tool-registry/types.js'\nimport { defaultValueTranslate } from '../tool-registry/translate.js'\nimport { recordToolCall } from '../tool-registry/call-recorder.js'\n\n\n/** Lookups a registry conversation may take before it answers from what it has (operato-twin's assistant takes 16 too). */\nexport const REGISTRY_MAX_ITERATIONS = 16\n\nexport interface RegistryAssistantInput {\n /** Provider client. Must implement `chatWithTools`. */\n client: AIClient\n /** The conversation so far, newest last. The caller owns history and truncation. */\n messages: AIMessage[]\n /** Who the assistant is and what this surface is. The host owns this prose. */\n systemPrompt: string\n /**\n * Categories this conversation may use. **Omitting it allows nothing** — the same as an empty\n * list, a plain conversation with no tools (A1, 2026-09-17). It used to mean every registered\n * category, which is how a Board-creating tool reached any conversation.\n */\n toolCategories?: string[]\n /**\n * Answers whether the caller holds a tool's declared privilege. Without it, every tool that\n * declares a privilege stays closed. The host builds it from the request — this package does\n * not know auth.\n */\n toolAccess?: ToolAccessCheck\n /** Passed to tools as `context.host`. Client-supplied — never used for authorization. */\n hostContext?: any\n /**\n * Which conversation this is — `{ kind, target }` from the registered mode (ADR-0061 decision 3).\n *\n * Tools see it as `context.mode`. It is the server's value, resolved from the registry before the\n * turn runs, so a tool that behaves differently in two conversations reads it here and not from\n * `host`, which the client fills.\n */\n mode?: { kind: string; target?: string }\n /** Caller's koa state (domain, user, tx). Tools that read the database need it. */\n state: any\n model?: string\n temperature?: number\n maxTokens?: number\n /** Make the model call a tool before it may answer. For surfaces that report live state. */\n requireToolOnFirstTurn?: boolean\n /**\n * The request's translator — the stop notice is then in the reader's language, and tools write what a person\n * reads with it (`context.t`). Without it tools get `defaultValueTranslate`: the English default values.\n */\n t?: (key: string, params?: Record<string, any>) => string\n /**\n * The files sent with this turn, after the caller's server checked them — stored with this conversation, the\n * conversation the caller's to write, the mode's policy met (ADR-0061, 2026-10-16 decision 2). Tools see them as\n * `context.attachments`. Never built from the client's request: what a client puts in `hostContext` stays there.\n */\n attachments?: ToolAttachment[]\n /** Texts the answer is allowed to draw identifiers from, besides the tool results. */\n groundingSources?: Omit<GroundingSources, 'toolResults'>\n}\n\nexport interface RegistryAssistantResult extends AssistantChatResult<unknown> {\n /** Tool names actually offered to the model this turn. The caller may log or return it. */\n offeredTools: string[]\n}\n\n/**\n * Run one turn of a conversation whose only tools come from the registry.\n *\n * Throws when the client cannot use tools at all — a surface built on registered tools that\n * silently ran without them would answer from nothing and look like it had checked.\n */\nexport async function registryAssistantChat(input: RegistryAssistantInput): Promise<RegistryAssistantResult> {\n const { client, messages, systemPrompt, toolCategories, hostContext, state } = input\n\n if (!client.chatWithTools) {\n throw new Error('[registry-assistant] the configured AI client cannot call tools, so this conversation cannot run')\n }\n\n /*\n * Only `read` is offered here. `external` means the tool changes something outside the\n * conversation (board-import's `materializeImportSession` creates a Board); this door answers\n * questions and has no confirm-and-apply step, so a tool with an effect does not go through it\n * even when the caller holds the privilege.\n */\n const scope = narrowToolScope(\n await resolveToolScope(toolCategories, input.toolAccess),\n spec => spec.kind === 'read'\n )\n const specs = scope.specs\n const byName = new Map(specs.map(spec => [spec.name, spec]))\n const categoryOf = new Map(scope.categories.flatMap(category => category.specs.map(spec => [spec.name, category.name] as [string, string])))\n\n const tools = specs.map(spec => ({\n name: spec.name,\n description: spec.themeNote ? `${spec.description} ${spec.themeNote}` : spec.description,\n parameters: spec.schema\n }))\n\n /*\n * The rules for using a tool belong to whoever registered it. Collect only the exposed ones, so\n * a surface never reads instructions for a category it cannot call.\n */\n const guidance = toolScopeGuidance(scope)\n const prompt = guidance.length > 0 ? `${systemPrompt}\\n\\n${guidance.join('\\n\\n')}` : systemPrompt\n\n /*\n * `mode` 는 서버의 값이다 — 등록된 모드에서 왔고, `host` 와 달리 클라이언트가 못 고친다. A2 가 서기\n * 전에는 채울 것이 없어 비워 두었다(ADR-0061 결정 3 의 주석). 이제 부르는 쪽이 들고 있다.\n */\n const toolContext: ToolCallContext = {\n state,\n host: hostContext ?? {},\n ...(input.mode ? { mode: input.mode } : {}),\n /* 번역기와 첨부는 서버의 값이다 — `host` 에서 받지 않는다(ADR-0061 2026-10-16 판정 1·2). */\n t: input.t ?? defaultValueTranslate,\n attachments: input.attachments ?? []\n }\n\n const result = await assistantChat<ToolCallContext, unknown>({\n initialConversation: messages,\n tools,\n options: {\n systemPrompt: prompt,\n model: input.model,\n temperature: input.temperature,\n maxTokens: input.maxTokens,\n requireToolOnFirstTurn: input.requireToolOnFirstTurn,\n /*\n * A question across a product's records takes many lookups. At the limit the model answers from\n * what it read and says what is missing — the person is never told to split the question.\n */\n maxIterations: REGISTRY_MAX_ITERATIONS,\n salvageOnMaxIterations: true\n },\n chat: (msgs, toolDefs, opts) => client.chatWithTools!(msgs, toolDefs, opts),\n context: toolContext,\n dispatch: {\n isReadTool: name => byName.has(name),\n /*\n * This surface has no document, so nothing here can write. Saying so plainly makes the loop\n * report an unknown name back to the model instead of accepting a call and dropping it.\n */\n isWriteTool: () => false,\n isActionTool: () => false,\n /* Every call is handed to the recorders - an application's audit sees the tools other packages declared too. */\n executeReadTool: async (call, context) => {\n const spec = byName.get(call.name)\n if (!spec) return undefined\n const args = call.arguments ?? {}\n const started = Date.now()\n const record = { tool: spec.name, category: categoryOf.get(spec.name) ?? '', args }\n try {\n const result = await (spec.builder as any)(args, context)\n await recordToolCall({ ...record, result, durationMs: Date.now() - started }, context)\n return result\n } catch (e) {\n await recordToolCall({ ...record, error: e, durationMs: Date.now() - started }, context)\n throw e\n }\n },\n validateWriteToolCall: () => ({ valid: false, errors: ['this conversation has no document to write to'] }),\n toolCallToOp: () => null,\n /* The conversation row keeps the shape of each answer, as the audit does - never its values (ADR-0061 decision 8). */\n summarizeToolResult: resultShapeOf\n },\n groundingSources: input.groundingSources,\n t: input.t\n })\n\n return { ...result, offeredTools: tools.map(tool => tool.name) }\n}\n"]}
@@ -1,7 +1,13 @@
1
- /**
2
- * 도구 결과를 화면·영속에 쓸 크기로 줄인다.
3
- *
4
- * 줄인 자리마다 **줄였다는 표시**를 남긴다(`…[+N chars]` · `…[+N items]` · `[Array(N)]`).
5
- * 표시가 없으면 읽는 사람이 그것을 원본으로 믿는다.
6
- */
7
- export declare function summarizeToolResult(value: any, depth?: number): any;
1
+ export interface ResultShape {
2
+ /** Characters of the answer as JSON. */
3
+ size: number;
4
+ /** Rows it carried - `read` of a list read, else the length of the answer or of its first list. */
5
+ count?: number;
6
+ /** The field names of an object answer - names only. */
7
+ keys?: string[];
8
+ /** More field names than `keys` holds. */
9
+ moreKeys?: number;
10
+ /** The code it failed with - a refusal's `error`, or the `error` code of a failure. */
11
+ errorCode?: string;
12
+ }
13
+ export declare function resultShapeOf(value: unknown): ResultShape;
@@ -1,67 +1,38 @@
1
1
  "use strict";
2
2
  /*
3
- * Tool results, cut down to what a screen can hold.
3
+ * A tool result as it is kept - its shape, never its values (ADR-0061 decision 8, ruled 2026-10-17).
4
4
  *
5
- * ── Why this is here and not in a product (2026-09-10) ───────────────────────────
6
- * This came out of `board-ai`, and measuring the other consumer is what settled it: `twin-ai`
7
- * passes `summarizeToolResult: v => v` — the identity function. So one product compressed its
8
- * tool results before persisting them and the other did not, for no stated reason. That is the
9
- * shape of a layer living in the wrong package: the second product does not decide differently,
10
- * it just never gets the decision.
5
+ * Two records keep what a tool answered: the conversation row (`toolUsages`, the dock's tool panel) and the AI audit
6
+ * (the call recorder's digest). Both keep the same thing, through this one function: how big the answer was, how many
7
+ * rows it carried, the names of its fields, and the code it failed with. The values - pay, shareholdings, a person's
8
+ * name - stay in their own tables; copying them into a conversation row puts them in its backups and exports too,
9
+ * whoever may read the row.
11
10
  *
12
- * ── ⚠ This is for the screen, never for a judgement ─────────────────────────────
13
- * The output is depth- and length-limited, so it is **not evidence of what the model received**.
14
- * A check that reads these summaries can pass while the model was handed `{}` — an un-awaited
15
- * async builder did exactly that, and the screen still showed a successful call.
16
- *
17
- * What the model actually received is `AgenticLoopResult.groundedTexts`. Grounding already uses
18
- * it, and anything that judges correctness must use it too.
11
+ * ── ⚠ This is for the panel and the audit, never for a judgement ─────────────────
12
+ * What the model actually received is `AgenticLoopResult.groundedTexts`. Grounding uses it, and anything that judges
13
+ * correctness must use it too.
19
14
  */
20
15
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.summarizeToolResult = summarizeToolResult;
22
- /** 문자열은 이 길이에서 자른다. 자른 사실과 잘린 양을 남긴다 — 조용히 짧아지지 않는다. */
23
- const MAX_STRING = 200;
24
- /** 배열은 이 개수까지 보이고 나머지는 수로 말한다. */
25
- const MAX_ITEMS = 5;
26
- /** 배열 안의 배열은 이 깊이에서 모양만 남긴다. */
27
- const MAX_ARRAY_DEPTH = 2;
28
- /** 객체는 이 깊이에서 키 이름만 남긴다. */
29
- const MAX_OBJECT_DEPTH = 3;
30
- /** 깊이 한계에서 남길 키 개수. */
31
- const MAX_KEYS = 6;
32
- /**
33
- * 도구 결과를 화면·영속에 쓸 크기로 줄인다.
34
- *
35
- * 줄인 자리마다 **줄였다는 표시**를 남긴다(`…[+N chars]` · `…[+N items]` · `[Array(N)]`).
36
- * 표시가 없으면 읽는 사람이 그것을 원본으로 믿는다.
37
- */
38
- function summarizeToolResult(value, depth = 0) {
39
- if (value === null || value === undefined)
40
- return value;
41
- if (typeof value === 'string') {
42
- return value.length > MAX_STRING ? `${value.slice(0, MAX_STRING)}…[+${value.length - MAX_STRING} chars]` : value;
43
- }
44
- if (typeof value === 'number' || typeof value === 'boolean')
45
- return value;
46
- if (Array.isArray(value)) {
47
- if (depth >= MAX_ARRAY_DEPTH)
48
- return `[Array(${value.length})]`;
49
- const out = value.slice(0, MAX_ITEMS).map(v => summarizeToolResult(v, depth + 1));
50
- if (value.length > MAX_ITEMS)
51
- out.push(`…[+${value.length - MAX_ITEMS} items]`);
52
- return out;
53
- }
54
- if (typeof value === 'object') {
55
- if (depth >= MAX_OBJECT_DEPTH) {
56
- const keys = Object.keys(value).slice(0, MAX_KEYS).join(',');
57
- return `{${keys}${Object.keys(value).length > MAX_KEYS ? ',…' : ''}}`;
58
- }
59
- const out = {};
60
- for (const key of Object.keys(value)) {
61
- out[key] = summarizeToolResult(value[key], depth + 1);
62
- }
63
- return out;
64
- }
65
- return undefined;
16
+ exports.resultShapeOf = resultShapeOf;
17
+ /** Field names kept of an object answer - its first ones; an answer with more says how many. */
18
+ const MAX_KEYS = 20;
19
+ function resultShapeOf(value) {
20
+ const size = JSON.stringify(value ?? null).length;
21
+ if (Array.isArray(value))
22
+ return { size, count: value.length };
23
+ if (!value || typeof value !== 'object')
24
+ return { size };
25
+ const answer = value;
26
+ const names = Object.keys(answer);
27
+ const lists = Object.values(answer).filter(Array.isArray);
28
+ const count = typeof answer.read === 'number' ? answer.read : lists.length ? lists[0].length : undefined;
29
+ const errorCode = answer.rejected === true ? String(answer.error ?? 'rejected') : typeof answer.error === 'string' ? answer.error : undefined;
30
+ return {
31
+ size,
32
+ ...(count === undefined ? {} : { count }),
33
+ keys: names.slice(0, MAX_KEYS),
34
+ ...(names.length > MAX_KEYS ? { moreKeys: names.length - MAX_KEYS } : {}),
35
+ ...(errorCode ? { errorCode } : {})
36
+ };
66
37
  }
67
38
  //# sourceMappingURL=summarize.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"summarize.js","sourceRoot":"","sources":["../../../server/service/assistant/summarize.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;AAmBH,kDAwBC;AAzCD,wDAAwD;AACxD,MAAM,UAAU,GAAG,GAAG,CAAA;AACtB,kCAAkC;AAClC,MAAM,SAAS,GAAG,CAAC,CAAA;AACnB,gCAAgC;AAChC,MAAM,eAAe,GAAG,CAAC,CAAA;AACzB,4BAA4B;AAC5B,MAAM,gBAAgB,GAAG,CAAC,CAAA;AAC1B,uBAAuB;AACvB,MAAM,QAAQ,GAAG,CAAC,CAAA;AAElB;;;;;GAKG;AACH,SAAgB,mBAAmB,CAAC,KAAU,EAAE,KAAK,GAAG,CAAC;IACvD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IACvD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,MAAM,KAAK,CAAC,MAAM,GAAG,UAAU,SAAS,CAAC,CAAC,CAAC,KAAK,CAAA;IAClH,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IACzE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,IAAI,KAAK,IAAI,eAAe;YAAE,OAAO,UAAU,KAAK,CAAC,MAAM,IAAI,CAAA;QAC/D,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,mBAAmB,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAA;QACjF,IAAI,KAAK,CAAC,MAAM,GAAG,SAAS;YAAE,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,MAAM,GAAG,SAAS,SAAS,CAAC,CAAA;QAC/E,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,KAAK,IAAI,gBAAgB,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YAC5D,OAAO,IAAI,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAA;QACvE,CAAC;QACD,MAAM,GAAG,GAAQ,EAAE,CAAA;QACnB,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACrC,GAAG,CAAC,GAAG,CAAC,GAAG,mBAAmB,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAA;QACvD,CAAC;QACD,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC","sourcesContent":["/*\n * Tool results, cut down to what a screen can hold.\n *\n * ── Why this is here and not in a product (2026-09-10) ───────────────────────────\n * This came out of `board-ai`, and measuring the other consumer is what settled it: `twin-ai`\n * passes `summarizeToolResult: v => v` — the identity function. So one product compressed its\n * tool results before persisting them and the other did not, for no stated reason. That is the\n * shape of a layer living in the wrong package: the second product does not decide differently,\n * it just never gets the decision.\n *\n * ── ⚠ This is for the screen, never for a judgement ─────────────────────────────\n * The output is depth- and length-limited, so it is **not evidence of what the model received**.\n * A check that reads these summaries can pass while the model was handed `{}` — an un-awaited\n * async builder did exactly that, and the screen still showed a successful call.\n *\n * What the model actually received is `AgenticLoopResult.groundedTexts`. Grounding already uses\n * it, and anything that judges correctness must use it too.\n */\n\n/** 문자열은 이 길이에서 자른다. 자른 사실과 잘린 양을 남긴다 — 조용히 짧아지지 않는다. */\nconst MAX_STRING = 200\n/** 배열은 이 개수까지 보이고 나머지는 수로 말한다. */\nconst MAX_ITEMS = 5\n/** 배열 안의 배열은 이 깊이에서 모양만 남긴다. */\nconst MAX_ARRAY_DEPTH = 2\n/** 객체는 이 깊이에서 키 이름만 남긴다. */\nconst MAX_OBJECT_DEPTH = 3\n/** 깊이 한계에서 남길 키 개수. */\nconst MAX_KEYS = 6\n\n/**\n * 도구 결과를 화면·영속에 쓸 크기로 줄인다.\n *\n * 줄인 자리마다 **줄였다는 표시**를 남긴다(`…[+N chars]` · `…[+N items]` · `[Array(N)]`).\n * 표시가 없으면 읽는 사람이 그것을 원본으로 믿는다.\n */\nexport function summarizeToolResult(value: any, depth = 0): any {\n if (value === null || value === undefined) return value\n if (typeof value === 'string') {\n return value.length > MAX_STRING ? `${value.slice(0, MAX_STRING)}…[+${value.length - MAX_STRING} chars]` : value\n }\n if (typeof value === 'number' || typeof value === 'boolean') return value\n if (Array.isArray(value)) {\n if (depth >= MAX_ARRAY_DEPTH) return `[Array(${value.length})]`\n const out = value.slice(0, MAX_ITEMS).map(v => summarizeToolResult(v, depth + 1))\n if (value.length > MAX_ITEMS) out.push(`…[+${value.length - MAX_ITEMS} items]`)\n return out\n }\n if (typeof value === 'object') {\n if (depth >= MAX_OBJECT_DEPTH) {\n const keys = Object.keys(value).slice(0, MAX_KEYS).join(',')\n return `{${keys}${Object.keys(value).length > MAX_KEYS ? ',…' : ''}}`\n }\n const out: any = {}\n for (const key of Object.keys(value)) {\n out[key] = summarizeToolResult(value[key], depth + 1)\n }\n return out\n }\n return undefined\n}\n"]}
1
+ {"version":3,"file":"summarize.js","sourceRoot":"","sources":["../../../server/service/assistant/summarize.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;AAkBH,sCAgBC;AAhCD,gGAAgG;AAChG,MAAM,QAAQ,GAAG,EAAE,CAAA;AAenB,SAAgB,aAAa,CAAC,KAAc;IAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,CAAA;IACjD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,CAAA;IAC9D,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,IAAI,EAAE,CAAA;IACxD,MAAM,MAAM,GAAG,KAA4B,CAAA;IAC3C,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACjC,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IACzD,MAAM,KAAK,GAAG,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;IACxG,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;IAC7I,OAAO;QACL,IAAI;QACJ,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;QACzC,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC;QAC9B,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,MAAM,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpC,CAAA;AACH,CAAC","sourcesContent":["/*\n * A tool result as it is kept - its shape, never its values (ADR-0061 decision 8, ruled 2026-10-17).\n *\n * Two records keep what a tool answered: the conversation row (`toolUsages`, the dock's tool panel) and the AI audit\n * (the call recorder's digest). Both keep the same thing, through this one function: how big the answer was, how many\n * rows it carried, the names of its fields, and the code it failed with. The values - pay, shareholdings, a person's\n * name - stay in their own tables; copying them into a conversation row puts them in its backups and exports too,\n * whoever may read the row.\n *\n * ── ⚠ This is for the panel and the audit, never for a judgement ─────────────────\n * What the model actually received is `AgenticLoopResult.groundedTexts`. Grounding uses it, and anything that judges\n * correctness must use it too.\n */\n\n/** Field names kept of an object answer - its first ones; an answer with more says how many. */\nconst MAX_KEYS = 20\n\nexport interface ResultShape {\n /** Characters of the answer as JSON. */\n size: number\n /** Rows it carried - `read` of a list read, else the length of the answer or of its first list. */\n count?: number\n /** The field names of an object answer - names only. */\n keys?: string[]\n /** More field names than `keys` holds. */\n moreKeys?: number\n /** The code it failed with - a refusal's `error`, or the `error` code of a failure. */\n errorCode?: string\n}\n\nexport function resultShapeOf(value: unknown): ResultShape {\n const size = JSON.stringify(value ?? null).length\n if (Array.isArray(value)) return { size, count: value.length }\n if (!value || typeof value !== 'object') return { size }\n const answer = value as Record<string, any>\n const names = Object.keys(answer)\n const lists = Object.values(answer).filter(Array.isArray)\n const count = typeof answer.read === 'number' ? answer.read : lists.length ? lists[0].length : undefined\n const errorCode = answer.rejected === true ? String(answer.error ?? 'rejected') : typeof answer.error === 'string' ? answer.error : undefined\n return {\n size,\n ...(count === undefined ? {} : { count }),\n keys: names.slice(0, MAX_KEYS),\n ...(names.length > MAX_KEYS ? { moreKeys: names.length - MAX_KEYS } : {}),\n ...(errorCode ? { errorCode } : {})\n }\n}\n"]}
@@ -3,14 +3,42 @@ export interface ToolCallRecord {
3
3
  tool: string;
4
4
  /** The category the tool was offered from. */
5
5
  category: string;
6
+ /** The ids the arguments name - `id`, `…Id`, `ids`, `…Ids` - and nothing else of them. */
7
+ targetIds: string[];
8
+ /** sha256 of the arguments - the same call is the same hash. */
9
+ argsHash: string;
10
+ /** Characters of the result as JSON; 0 when the tool threw. */
11
+ resultSize: number;
12
+ /** How many rows it carried - `read` of a list read, else the length of its first list; absent when it says none. */
13
+ resultCount?: number;
14
+ resultHash?: string;
15
+ /** Why it failed: the code a refusal returned (`{ rejected: true, error }`), the thrown error's code, or `tool-threw`. */
16
+ errorCode?: string;
17
+ ok: boolean;
18
+ durationMs: number;
19
+ /** The arguments and the result as they were - only for a category the recorder named in `raw`. */
20
+ raw?: {
21
+ args: unknown;
22
+ result?: unknown;
23
+ error?: string;
24
+ };
25
+ }
26
+ export type ToolCallRecorder = (record: ToolCallRecord, context: ToolCallContext) => void | Promise<void>;
27
+ export interface ToolCallRecorderOptions {
28
+ /** Categories whose calls the recorder is handed with their arguments and result as they were. */
29
+ raw?: string[];
30
+ }
31
+ /** A call as it ran - the registry door's side, before it is cut down for a recorder. */
32
+ export interface ToolCallRun {
33
+ tool: string;
34
+ category: string;
6
35
  args: unknown;
7
- /** What the tool returned - absent when it threw. A refusal it returned (`{ rejected: true }`) is here. */
8
36
  result?: unknown;
9
- /** Why it threw. */
10
- error?: string;
37
+ error?: unknown;
11
38
  durationMs: number;
12
39
  }
13
- export type ToolCallRecorder = (record: ToolCallRecord, context: ToolCallContext) => void | Promise<void>;
14
- export declare function registerToolCallRecorder(recorder: ToolCallRecorder): void;
40
+ export declare function registerToolCallRecorder(recorder: ToolCallRecorder, options?: ToolCallRecorderOptions): void;
15
41
  export declare function clearToolCallRecorders(): void;
16
- export declare function recordToolCall(record: ToolCallRecord, context: ToolCallContext): Promise<void>;
42
+ /** A run as a recorder sees it - the digest, and the raw text when its category was asked for. */
43
+ export declare function digestOf(run: ToolCallRun, raw?: boolean): ToolCallRecord;
44
+ export declare function recordToolCall(run: ToolCallRun, context: ToolCallContext): Promise<void>;