@things-factory/ai-client-base 10.1.26 → 10.1.28
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.
- package/dist-server/service/agentic-loop/agentic-loop.js +22 -8
- package/dist-server/service/agentic-loop/agentic-loop.js.map +1 -1
- package/dist-server/service/assistant/abort-notice.d.ts +3 -1
- package/dist-server/service/assistant/abort-notice.js +20 -6
- package/dist-server/service/assistant/abort-notice.js.map +1 -1
- package/dist-server/service/assistant/assistant-chat.d.ts +6 -0
- package/dist-server/service/assistant/assistant-chat.js +1 -1
- package/dist-server/service/assistant/assistant-chat.js.map +1 -1
- package/dist-server/service/assistant/registry-assistant.d.ts +2 -0
- package/dist-server/service/assistant/registry-assistant.js +2 -1
- package/dist-server/service/assistant/registry-assistant.js.map +1 -1
- package/dist-server/service/capability.d.ts +17 -0
- package/dist-server/service/capability.js +49 -0
- package/dist-server/service/capability.js.map +1 -0
- package/dist-server/service/index.d.ts +1 -0
- package/dist-server/service/index.js +2 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/tool-registry/registry.d.ts +8 -1
- package/dist-server/service/tool-registry/registry.js +27 -6
- package/dist-server/service/tool-registry/registry.js.map +1 -1
- package/dist-server/service/tool-registry/types.d.ts +26 -2
- package/dist-server/service/tool-registry/types.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/service/agentic-loop/agentic-loop.ts +21 -8
- package/server/service/agentic-loop/proposal-key.test.ts +52 -0
- package/server/service/assistant/abort-notice.ts +33 -11
- package/server/service/assistant/assistant-chat.test.ts +24 -2
- package/server/service/assistant/assistant-chat.ts +7 -2
- package/server/service/assistant/registry-assistant.ts +4 -1
- package/server/service/capability.test.ts +52 -0
- package/server/service/capability.ts +58 -0
- package/server/service/index.ts +2 -0
- package/server/service/tool-registry/registry.test.ts +43 -2
- package/server/service/tool-registry/registry.ts +37 -8
- package/server/service/tool-registry/types.ts +27 -2
|
@@ -46,9 +46,12 @@ const index_js_1 = require("../utils/index.js");
|
|
|
46
46
|
const log = (0, debug_1.default)('things-factory:ai:agentic-loop');
|
|
47
47
|
// ── Loop ─────────────────────────────────────────────────────────
|
|
48
48
|
const DEFAULT_MAX_ITERATIONS = 8;
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
/**
|
|
50
|
+
* The wrap-up call's default instruction — domain neutral, and in English because the reader is the model.
|
|
51
|
+
* The reply it produces is in the language the person wrote in; that is the model's job, not this string's.
|
|
52
|
+
*/
|
|
53
|
+
const DEFAULT_SALVAGE_PROMPT = '[wrap up] No more lookups are possible. Answer from the tool output you already have. ' +
|
|
54
|
+
'State what you found first, then say in one line what is still missing to answer the rest.';
|
|
52
55
|
/**
|
|
53
56
|
* multi-turn agentic loop.
|
|
54
57
|
*
|
|
@@ -84,7 +87,7 @@ async function runAgenticLoop(input) {
|
|
|
84
87
|
abortReason = { type: 'aborted', iter };
|
|
85
88
|
break;
|
|
86
89
|
}
|
|
87
|
-
log('iter %d: chat()
|
|
90
|
+
log('iter %d: calling chat()', iter);
|
|
88
91
|
let result;
|
|
89
92
|
try {
|
|
90
93
|
/* (2) Error boundary — provider 예외 시 graceful 종료.
|
|
@@ -102,7 +105,7 @@ async function runAgenticLoop(input) {
|
|
|
102
105
|
allowParallelToolCalls: true,
|
|
103
106
|
signal
|
|
104
107
|
}), {
|
|
105
|
-
onRetry: ({ attempt, delayMs, error }) => log('iter %d:
|
|
108
|
+
onRetry: ({ attempt, delayMs, error }) => log('iter %d: transient failure, retry %d in %dms: %s', iter, attempt, delayMs, error?.message)
|
|
106
109
|
});
|
|
107
110
|
}
|
|
108
111
|
catch (e) {
|
|
@@ -218,7 +221,17 @@ async function runAgenticLoop(input) {
|
|
|
218
221
|
* 보이면 카드가 둘이 되고, 사용자가 같은 명령을 두 번 실행할 수 있다.
|
|
219
222
|
*/
|
|
220
223
|
function proposalEffectKey(p) {
|
|
221
|
-
|
|
224
|
+
/*
|
|
225
|
+
* 카드에만 있는 것은 효과가 아니다 — 문구가 다르다고 다른 조치가 되지 않는다. `key` 도 뺀다:
|
|
226
|
+
* 이 함수가 만든 값이 다음 계산의 입력이 되면 키가 키를 낳는다.
|
|
227
|
+
*/
|
|
228
|
+
const presentation = new Set(['label', 'reason', 'icon', 'choices', 'key', 'proposed', 'source', 'autoStage']);
|
|
229
|
+
const effect = {};
|
|
230
|
+
for (const [name, value] of Object.entries(p ?? {})) {
|
|
231
|
+
if (!presentation.has(name))
|
|
232
|
+
effect[name] = value;
|
|
233
|
+
}
|
|
234
|
+
return `${p?.tool ?? ''}|${stableStringify(effect)}`;
|
|
222
235
|
}
|
|
223
236
|
/** 키 정렬 직렬화 — 순서에 흔들리지 않는 비교를 위해. */
|
|
224
237
|
function stableStringify(value) {
|
|
@@ -291,11 +304,12 @@ iter) {
|
|
|
291
304
|
* 판정은 **효과**(도구·명령·대상·인자)로 한다 — 설명 문구(label·reason)가 달라도 같은 조치다. */
|
|
292
305
|
const candidate = { tool: tc.name, ...value };
|
|
293
306
|
const key = proposalEffectKey(candidate);
|
|
294
|
-
if (proposals.some(p =>
|
|
307
|
+
if (proposals.some(p => p.key === key)) {
|
|
295
308
|
outcome = 'folded'; // 접힌 사실을 남긴다 — "왜 카드가 하나뿐인가" 가 추적 가능해야 한다
|
|
296
309
|
}
|
|
297
310
|
else {
|
|
298
|
-
|
|
311
|
+
/* 키를 함께 실어 보낸다 — 카드가 같은 규칙으로 다시 세지 않게(원본은 하나). */
|
|
312
|
+
proposals.push({ ...candidate, key });
|
|
299
313
|
outcome = 'proposed';
|
|
300
314
|
}
|
|
301
315
|
}
|
|
@@ -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 debug from 'debug'\nimport type { 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 }\n | { type: 'repeated_validation_failure'; toolName: string; consecutive: number; iter: number }\n}\n\n// ── Loop ─────────────────────────────────────────────────────────\n\nconst DEFAULT_MAX_ITERATIONS = 8\n\n/** 마무리 호출의 기본 지시 — 도메인 중립. */\nconst DEFAULT_SALVAGE_PROMPT =\n '[마무리] 더 조회할 수 없다. **지금까지 받은 도구 출력만으로** 답하라. ' +\n '알아낸 사실을 먼저 말하고, 그것으로 답할 수 없는 부분은 **무엇이 더 필요한지** 한 줄로 밝혀라.'\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: 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: 일시 장애 재시도 %d회 (%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 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 return [p?.tool ?? '', p?.command ?? '', p?.instanceId ?? '', stableStringify(p?.args)].join('|')\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 */\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<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\n for (const tc of toolCalls) {\n if (dispatch.isReadTool(tc.name)) {\n /* await 필수 — registry 에 등록된 read/external builder 는 async 일 수 있다(DB·외부 호출).\n * await 없이 JSON.stringify 하면 Promise 가 '{}' 로 직렬화되어 LLM 이 **빈 결과**를 받는다\n * (문서 가져오기·트윈 구조 조회 등 — 무엇인지는 부르는 쪽이 안다). */\n const value = await dispatch.executeReadTool(tc, context)\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) }\n const key = proposalEffectKey(candidate)\n if (proposals.some(p => proposalEffectKey(p) === key)) {\n outcome = 'folded' // 접힌 사실을 남긴다 — \"왜 카드가 하나뿐인가\" 가 추적 가능해야 한다\n } else {\n proposals.push(candidate)\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 return toolResultParts\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 debug from 'debug'\nimport type { 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 }\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 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 return `${p?.tool ?? ''}|${stableStringify(effect)}`\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 */\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<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\n for (const tc of toolCalls) {\n if (dispatch.isReadTool(tc.name)) {\n /* await 필수 — registry 에 등록된 read/external builder 는 async 일 수 있다(DB·외부 호출).\n * await 없이 JSON.stringify 하면 Promise 가 '{}' 로 직렬화되어 LLM 이 **빈 결과**를 받는다\n * (문서 가져오기·트윈 구조 조회 등 — 무엇인지는 부르는 쪽이 안다). */\n const value = await dispatch.executeReadTool(tc, context)\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) }\n const key = proposalEffectKey(candidate)\n if (proposals.some(p => p.key === key)) {\n outcome = 'folded' // 접힌 사실을 남긴다 — \"왜 카드가 하나뿐인가\" 가 추적 가능해야 한다\n } else {\n /* 키를 함께 실어 보낸다 — 카드가 같은 규칙으로 다시 세지 않게(원본은 하나). */\n proposals.push({ ...candidate, key })\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 return toolResultParts\n}\n"]}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { AgenticLoopResult } from '../agentic-loop/index.js';
|
|
2
|
+
/** Turns a key and its values into the reader's sentence. `ai-assistant` hands in the request's `context.t`. */
|
|
3
|
+
export type TranslateNotice = (key: string, params?: Record<string, any>) => string;
|
|
2
4
|
/**
|
|
3
5
|
* 중단 이유 → 답에 덧붙일 한 줄. 정상 종료면 빈 문자열이다.
|
|
4
6
|
*
|
|
5
7
|
* 답을 고치지 않고 **뒤에 붙인다** — 모델이 한 말을 우리가 다시 쓰지 않는다.
|
|
6
8
|
*/
|
|
7
|
-
export declare function formatAbortNotice(abortReason: AgenticLoopResult<any>['abortReason']): string;
|
|
9
|
+
export declare function formatAbortNotice(abortReason: AgenticLoopResult<any>['abortReason'], t?: TranslateNotice): string;
|
|
@@ -2,28 +2,42 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.formatAbortNotice = formatAbortNotice;
|
|
4
4
|
const index_js_1 = require("../utils/index.js");
|
|
5
|
+
/** The English sentence for each reason, used when the caller has no translator. */
|
|
6
|
+
const NOTICE_DEFAULTS = {
|
|
7
|
+
'ai-assistant.notice.stopped': 'the request stopped partway, so only part of it was done.',
|
|
8
|
+
'ai-assistant.notice.provider-busy': 'the ai service did not answer just now. try again in a moment.',
|
|
9
|
+
'ai-assistant.notice.provider-error': 'something went wrong while handling this. tell an administrator if it keeps happening.',
|
|
10
|
+
'ai-assistant.notice.too-long': 'this ran long and stopped at step {iter}. try asking in smaller pieces.',
|
|
11
|
+
'ai-assistant.notice.tool-kept-failing': '{tool} failed {count} times in a row, so it stopped. try saying it differently.'
|
|
12
|
+
};
|
|
13
|
+
function line(key, params, t) {
|
|
14
|
+
const said = t
|
|
15
|
+
? t(key, { defaultValue: NOTICE_DEFAULTS[key], ...params })
|
|
16
|
+
: Object.entries(params).reduce((text, [k, v]) => text.replaceAll(`{${k}}`, String(v)), NOTICE_DEFAULTS[key]);
|
|
17
|
+
return `\n\n_(${said})_`;
|
|
18
|
+
}
|
|
5
19
|
/**
|
|
6
20
|
* 중단 이유 → 답에 덧붙일 한 줄. 정상 종료면 빈 문자열이다.
|
|
7
21
|
*
|
|
8
22
|
* 답을 고치지 않고 **뒤에 붙인다** — 모델이 한 말을 우리가 다시 쓰지 않는다.
|
|
9
23
|
*/
|
|
10
|
-
function formatAbortNotice(abortReason) {
|
|
24
|
+
function formatAbortNotice(abortReason, t) {
|
|
11
25
|
if (!abortReason)
|
|
12
26
|
return '';
|
|
13
27
|
switch (abortReason.type) {
|
|
14
28
|
case 'aborted':
|
|
15
|
-
return '
|
|
29
|
+
return line('ai-assistant.notice.stopped', {}, t);
|
|
16
30
|
case 'provider_error':
|
|
17
31
|
/* 남의 말(SDK 원문)을 사용자에게 보여주지 않는다 — "Error fetching from undefined: [503 …]" 은
|
|
18
32
|
* 사용자가 할 수 있는 일을 알려주지 않는다. 일시 장애는 이미 몇 번 다시 시도한 뒤이므로,
|
|
19
33
|
* 지금 필요한 안내는 "잠시 후 다시" 하나다. 원문은 서버 로그에 남는다. */
|
|
20
34
|
return (0, index_js_1.isTransientAIError)({ message: abortReason.message })
|
|
21
|
-
? '
|
|
22
|
-
: '
|
|
35
|
+
? line('ai-assistant.notice.provider-busy', {}, t)
|
|
36
|
+
: line('ai-assistant.notice.provider-error', {}, t);
|
|
23
37
|
case 'max_iterations':
|
|
24
|
-
return
|
|
38
|
+
return line('ai-assistant.notice.too-long', { iter: abortReason.iter }, t);
|
|
25
39
|
case 'repeated_validation_failure':
|
|
26
|
-
return
|
|
40
|
+
return line('ai-assistant.notice.tool-kept-failing', { tool: abortReason.toolName, count: abortReason.consecutive }, t);
|
|
27
41
|
}
|
|
28
42
|
}
|
|
29
43
|
//# sourceMappingURL=abort-notice.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"abort-notice.js","sourceRoot":"","sources":["../../../server/service/assistant/abort-notice.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"abort-notice.js","sourceRoot":"","sources":["../../../server/service/assistant/abort-notice.ts"],"names":[],"mappings":";;AA6CA,8CAoBC;AA9CD,gDAAsD;AAKtD,oFAAoF;AACpF,MAAM,eAAe,GAA2B;IAC9C,6BAA6B,EAAE,2DAA2D;IAC1F,mCAAmC,EAAE,gEAAgE;IACrG,oCAAoC,EAAE,wFAAwF;IAC9H,8BAA8B,EAAE,yEAAyE;IACzG,uCAAuC,EAAE,iFAAiF;CAC3H,CAAA;AAED,SAAS,IAAI,CAAC,GAAW,EAAE,MAA2B,EAAE,CAAmB;IACzE,MAAM,IAAI,GAAG,CAAC;QACZ,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,YAAY,EAAE,eAAe,CAAC,GAAG,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC;QAC3D,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,eAAe,CAAC,GAAG,CAAC,CAAC,CAAA;IAC/G,OAAO,SAAS,IAAI,IAAI,CAAA;AAC1B,CAAC;AAED;;;;GAIG;AACH,SAAgB,iBAAiB,CAC/B,WAAkD,EAClD,CAAmB;IAEnB,IAAI,CAAC,WAAW;QAAE,OAAO,EAAE,CAAA;IAC3B,QAAQ,WAAW,CAAC,IAAI,EAAE,CAAC;QACzB,KAAK,SAAS;YACZ,OAAO,IAAI,CAAC,6BAA6B,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;QACnD,KAAK,gBAAgB;YACnB;;2DAE+C;YAC/C,OAAO,IAAA,6BAAkB,EAAC,EAAE,OAAO,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC;gBACzD,CAAC,CAAC,IAAI,CAAC,mCAAmC,EAAE,EAAE,EAAE,CAAC,CAAC;gBAClD,CAAC,CAAC,IAAI,CAAC,oCAAoC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;QACvD,KAAK,gBAAgB;YACnB,OAAO,IAAI,CAAC,8BAA8B,EAAE,EAAE,IAAI,EAAE,WAAW,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAA;QAC5E,KAAK,6BAA6B;YAChC,OAAO,IAAI,CAAC,uCAAuC,EAAE,EAAE,IAAI,EAAE,WAAW,CAAC,QAAQ,EAAE,KAAK,EAAE,WAAW,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAA;IAC3H,CAAC;AACH,CAAC","sourcesContent":["/*\n * Why the loop stopped, said to the person who asked.\n *\n * ── Why this is here and not in a product (2026-09-10) ───────────────────────────\n * This came out of `board-ai`. It is not board's: every product that runs the agentic loop can\n * have it stop early, and a reply that was cut off looks exactly like a reply that finished.\n *\n * The loop already reports why it stopped — `AgenticLoopResult.abortReason`. Nothing forces the\n * caller to say so. A product that forgets prints a truncated answer as though it were complete,\n * and the person acts on half of it. That failure is silent by construction, which is why the\n * notice belongs beside the loop rather than beside each product's prose.\n *\n * ── The words are the reader's, and the caller owns the reader (2026-09-18) ────────\n * These lines were Korean literals in a framework package, so every product said them in Korean\n * whatever language the person was reading in (ADR-0049 ② · ADR-0054). The notice now names a key\n * and carries an English default; a caller that has the request's translator hands it in and the\n * person reads the line in their own language.\n */\nimport type { AgenticLoopResult } from '../agentic-loop/index.js'\nimport { isTransientAIError } from '../utils/index.js'\n\n/** Turns a key and its values into the reader's sentence. `ai-assistant` hands in the request's `context.t`. */\nexport type TranslateNotice = (key: string, params?: Record<string, any>) => string\n\n/** The English sentence for each reason, used when the caller has no translator. */\nconst NOTICE_DEFAULTS: Record<string, string> = {\n 'ai-assistant.notice.stopped': 'the request stopped partway, so only part of it was done.',\n 'ai-assistant.notice.provider-busy': 'the ai service did not answer just now. try again in a moment.',\n 'ai-assistant.notice.provider-error': 'something went wrong while handling this. tell an administrator if it keeps happening.',\n 'ai-assistant.notice.too-long': 'this ran long and stopped at step {iter}. try asking in smaller pieces.',\n 'ai-assistant.notice.tool-kept-failing': '{tool} failed {count} times in a row, so it stopped. try saying it differently.'\n}\n\nfunction line(key: string, params: Record<string, any>, t?: TranslateNotice): string {\n const said = t\n ? t(key, { defaultValue: NOTICE_DEFAULTS[key], ...params })\n : Object.entries(params).reduce((text, [k, v]) => text.replaceAll(`{${k}}`, String(v)), NOTICE_DEFAULTS[key])\n return `\\n\\n_(${said})_`\n}\n\n/**\n * 중단 이유 → 답에 덧붙일 한 줄. 정상 종료면 빈 문자열이다.\n *\n * 답을 고치지 않고 **뒤에 붙인다** — 모델이 한 말을 우리가 다시 쓰지 않는다.\n */\nexport function formatAbortNotice(\n abortReason: AgenticLoopResult<any>['abortReason'],\n t?: TranslateNotice\n): string {\n if (!abortReason) return ''\n switch (abortReason.type) {\n case 'aborted':\n return line('ai-assistant.notice.stopped', {}, t)\n case 'provider_error':\n /* 남의 말(SDK 원문)을 사용자에게 보여주지 않는다 — \"Error fetching from undefined: [503 …]\" 은\n * 사용자가 할 수 있는 일을 알려주지 않는다. 일시 장애는 이미 몇 번 다시 시도한 뒤이므로,\n * 지금 필요한 안내는 \"잠시 후 다시\" 하나다. 원문은 서버 로그에 남는다. */\n return isTransientAIError({ message: abortReason.message })\n ? line('ai-assistant.notice.provider-busy', {}, t)\n : line('ai-assistant.notice.provider-error', {}, t)\n case 'max_iterations':\n return line('ai-assistant.notice.too-long', { iter: abortReason.iter }, t)\n case 'repeated_validation_failure':\n return line('ai-assistant.notice.tool-kept-failing', { tool: abortReason.toolName, count: abortReason.consecutive }, t)\n }\n}\n"]}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { type AgenticLoopInput, type AgenticLoopResult } from '../agentic-loop/index.js';
|
|
2
2
|
import { type GroundingSources } from '../utils/index.js';
|
|
3
|
+
import { type TranslateNotice } from './abort-notice.js';
|
|
3
4
|
export interface AssistantChatInput<TCtx = unknown, TOp = unknown> extends AgenticLoopInput<TCtx, TOp> {
|
|
5
|
+
/**
|
|
6
|
+
* The request's translator, so a notice this function appends is in the reader's language
|
|
7
|
+
* (ADR-0054). Without it the notice falls back to English.
|
|
8
|
+
*/
|
|
9
|
+
t?: TranslateNotice;
|
|
4
10
|
/**
|
|
5
11
|
* 이 턴에 모델이 본 것 — **도구 결과 말고** 전부. 도구 결과는 이 함수가 루프에서 받아 더한다.
|
|
6
12
|
*
|
|
@@ -51,7 +51,7 @@ async function assistantChat(input) {
|
|
|
51
51
|
const { groundingSources, skipGrounding, ...loopInput } = input;
|
|
52
52
|
const loop = await (0, index_js_1.runAgenticLoop)(loopInput);
|
|
53
53
|
const baseText = loop.lastText ?? '';
|
|
54
|
-
const reply = baseText + (0, abort_notice_js_1.formatAbortNotice)(loop.abortReason);
|
|
54
|
+
const reply = baseText + (0, abort_notice_js_1.formatAbortNotice)(loop.abortReason, input.t);
|
|
55
55
|
let warnings;
|
|
56
56
|
if (!skipGrounding && baseText) {
|
|
57
57
|
const corpus = (0, index_js_2.buildGroundedCorpus)({ ...(groundingSources ?? {}), toolResults: loop.groundedTexts });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant-chat.js","sourceRoot":"","sources":["../../../server/service/assistant/assistant-chat.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"assistant-chat.js","sourceRoot":"","sources":["../../../server/service/assistant/assistant-chat.ts"],"names":[],"mappings":";;AAqFA,sCAkBC;AAvGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,uDAAwG;AACxG,gDAAiG;AACjG,uDAA2E;AAwC3E;;;;;;GAMG;AACI,KAAK,UAAU,aAAa,CACjC,KAAoC;IAEpC,MAAM,EAAE,gBAAgB,EAAE,aAAa,EAAE,GAAG,SAAS,EAAE,GAAG,KAAK,CAAA;IAE/D,MAAM,IAAI,GAAG,MAAM,IAAA,yBAAc,EAAY,SAAS,CAAC,CAAA;IAEvD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAA;IACpC,MAAM,KAAK,GAAG,QAAQ,GAAG,IAAA,mCAAiB,EAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;IAErE,IAAI,QAA8B,CAAA;IAClC,IAAI,CAAC,aAAa,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,IAAA,8BAAmB,EAAC,EAAE,GAAG,CAAC,gBAAgB,IAAI,EAAE,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,CAAA;QACpG,MAAM,KAAK,GAAG,IAAA,4BAAiB,EAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;QACjD,QAAQ,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;IACjD,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;AAC/D,CAAC","sourcesContent":["/*\n * One turn of an assistant — the loop **with the guards that must not be optional.**\n *\n * ── Why this exists (2026-09-10) ────────────────────────────────────────────────\n * `runAgenticLoop` is neutral and has been since 2026-09-05, when the same loop running in two\n * places was consolidated. The note on that change is the reason this file exists: it was \"not a\n * duplication, a defect\" — the two copies had **their safety measures set opposite ways**.\n *\n * The loop came out. The guards around it did not. They stayed in `board-ai`, wrapped around\n * `DefaultBoardAIAssistant.chat`, which meant a product with no board of its own could only get\n * them by going through the board editor. The plant lane read that correctly — it uses none of\n * board's session machinery — and asked whether it could call `runAgenticLoop` directly instead.\n *\n * The answer was no, and the reason is written in `board-ai`'s own grounding file: the twin's\n * header route had a grounding guard and its dock route did not, so **the same question was\n * verified on one side and not the other.** A per-product door makes that three ways. This file is\n * the door instead.\n *\n * ── What is guaranteed here, and what the host still owns ───────────────────────\n * Guaranteed, because a caller cannot skip it:\n * · the loop stops for a reason and the reason reaches the reply (`formatAbortNotice`)\n * · the reply is checked against what the model was actually given (`groundingWarnings`)\n *\n * Still the host's, because only the host knows the domain:\n * · which tools exist and what a write call means → `gate`, `dispatch`\n * · what counts as evidence beyond the tool results → `groundingSources`\n *\n * ── Grounding sources: leaving one out produces false warnings ──────────────────\n * The corpus is everything the model saw this turn. The tool results are added here, from\n * `groundedTexts` — the raw strings sent back as `tool_result`, never `toolUsages[].result`, which\n * is depth-limited for display. A check reading the display copy can pass while the model was\n * handed `{}`; an un-awaited async builder did exactly that.\n *\n * Anything else the host put in front of the model — a system prompt, a state summary, the user's\n * own words — has to be declared, or a correct answer quoting it gets warned about.\n */\nimport { runAgenticLoop, type AgenticLoopInput, type AgenticLoopResult } from '../agentic-loop/index.js'\nimport { buildGroundedCorpus, groundingWarnings, type GroundingSources } from '../utils/index.js'\nimport { formatAbortNotice, type TranslateNotice } from './abort-notice.js'\n\nexport interface AssistantChatInput<TCtx = unknown, TOp = unknown> extends AgenticLoopInput<TCtx, TOp> {\n /**\n * The request's translator, so a notice this function appends is in the reader's language\n * (ADR-0054). Without it the notice falls back to English.\n */\n t?: TranslateNotice\n /**\n * 이 턴에 모델이 본 것 — **도구 결과 말고** 전부. 도구 결과는 이 함수가 루프에서 받아 더한다.\n *\n * 빠뜨리면 정상 답이 경고를 받는다(오탐). 시스템 프롬프트에 적어 보낸 타입 이름을 모델이 말하는\n * 것은 근거 있는 발언이다.\n */\n groundingSources?: Omit<GroundingSources, 'toolResults'>\n /**\n * 접지 검사를 끈다 — **기본은 켜짐.**\n *\n * 끄는 것이 맞는 자리가 있다: 식별자를 다루지 않는 대화(문서 상담·개념 질문)는 근거집합에 식별자가\n * 없어 모든 고유명사가 경고로 나온다. 그때는 끄는 것이 정직하고, **켠 채로 경고를 버리는 것**이\n * 정직하지 않다.\n */\n skipGrounding?: boolean\n}\n\nexport interface AssistantChatResult<TOp = unknown> {\n /**\n * 사용자에게 보일 답 — 모델의 마지막 말 + 중단 통지.\n *\n * 모델의 말을 고치지 않는다. 통지는 **뒤에 붙는다.**\n */\n reply: string\n /** 통지가 붙기 전의 답. 접지 검사가 본 것이고, 호스트가 다시 판정할 때 쓴다. */\n baseText: string\n /** 근거 밖의 식별자를 지목한 자리들. 없으면 undefined — 빈 배열을 주지 않는다. */\n groundingWarnings?: string[]\n /** 루프 결과 원본 — 쓰기 호출·조치·제안·도구 흔적. 호스트가 자기 뜻으로 해석한다. */\n loop: AgenticLoopResult<TOp>\n}\n\n/**\n * 한 턴을 돌린다.\n *\n * `lastText` 가 없을 수 있다(도구만 부르고 말을 안 한 턴). 그 경우 `baseText` 는 빈 문자열이고,\n * 무엇을 대신 말할지는 호스트가 정한다 — 여기서 「변경했습니다」 같은 말을 지어내지 않는다.\n * 도메인을 모르는 자리에서 결과를 서술하면 그것이 곧 지어낸 원인이 된다.\n */\nexport async function assistantChat<TCtx = unknown, TOp = unknown>(\n input: AssistantChatInput<TCtx, TOp>\n): Promise<AssistantChatResult<TOp>> {\n const { groundingSources, skipGrounding, ...loopInput } = input\n\n const loop = await runAgenticLoop<TCtx, TOp>(loopInput)\n\n const baseText = loop.lastText ?? ''\n const reply = baseText + formatAbortNotice(loop.abortReason, input.t)\n\n let warnings: string[] | undefined\n if (!skipGrounding && baseText) {\n const corpus = buildGroundedCorpus({ ...(groundingSources ?? {}), toolResults: loop.groundedTexts })\n const found = groundingWarnings(baseText, corpus)\n warnings = found.length > 0 ? found : undefined\n }\n\n return { reply, baseText, groundingWarnings: warnings, loop }\n}\n"]}
|
|
@@ -30,6 +30,8 @@ export interface RegistryAssistantInput {
|
|
|
30
30
|
maxTokens?: number;
|
|
31
31
|
/** Make the model call a tool before it may answer. For surfaces that report live state. */
|
|
32
32
|
requireToolOnFirstTurn?: boolean;
|
|
33
|
+
/** The request's translator — the stop notice is then in the reader's language. */
|
|
34
|
+
t?: (key: string, params?: Record<string, any>) => string;
|
|
33
35
|
/** Texts the answer is allowed to draw identifiers from, besides the tool results. */
|
|
34
36
|
groundingSources?: Omit<GroundingSources, 'toolResults'>;
|
|
35
37
|
}
|
|
@@ -66,7 +66,8 @@ async function registryAssistantChat(input) {
|
|
|
66
66
|
toolCallToOp: () => null,
|
|
67
67
|
summarizeToolResult: summarize_js_1.summarizeToolResult
|
|
68
68
|
},
|
|
69
|
-
groundingSources: input.groundingSources
|
|
69
|
+
groundingSources: input.groundingSources,
|
|
70
|
+
t: input.t
|
|
70
71
|
});
|
|
71
72
|
return { ...result, offeredTools: tools.map(tool => tool.name) };
|
|
72
73
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry-assistant.js","sourceRoot":"","sources":["../../../server/service/assistant/registry-assistant.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"registry-assistant.js","sourceRoot":"","sources":["../../../server/service/assistant/registry-assistant.ts"],"names":[],"mappings":";;AA2EA,sDAqEC;AAzHD,mDAA4G;AAC5G,2DAA6E;AAC7E,iDAAoD;AA4CpD;;;;;GAKG;AACI,KAAK,UAAU,qBAAqB,CAAC,KAA6B;IACvE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,KAAK,CAAA;IAEpF,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,kGAAkG,CAAC,CAAA;IACrH,CAAC;IAED;;;;;OAKG;IACH,MAAM,KAAK,GAAG,IAAA,+BAAe,EAC3B,MAAM,IAAA,gCAAgB,EAAC,cAAc,EAAE,KAAK,CAAC,UAAU,CAAC,EACxD,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CAC7B,CAAA;IACD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAA;IACzB,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;IAE5D,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC/B,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW;QACxF,UAAU,EAAE,IAAI,CAAC,MAAM;KACxB,CAAC,CAAC,CAAA;IAEH;;;OAGG;IACH,MAAM,QAAQ,GAAG,IAAA,iCAAiB,EAAC,KAAK,CAAC,CAAA;IACzC,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,YAAY,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,CAAA;IAEjG,MAAM,WAAW,GAAoB,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,IAAI,EAAE,EAAE,CAAA;IAEvE,MAAM,MAAM,GAAG,MAAM,IAAA,iCAAa,EAA2B;QAC3D,mBAAmB,EAAE,QAAQ;QAC7B,KAAK;QACL,OAAO,EAAE;YACP,YAAY,EAAE,MAAM;YACpB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,sBAAsB,EAAE,KAAK,CAAC,sBAAsB;SACrD;QACD,IAAI,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,aAAc,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC;QAC3E,OAAO,EAAE,WAAW;QACpB,QAAQ,EAAE;YACR,UAAU,EAAE,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;YACpC;;;eAGG;YACH,WAAW,EAAE,GAAG,EAAE,CAAC,KAAK;YACxB,YAAY,EAAE,GAAG,EAAE,CAAC,KAAK;YACzB,eAAe,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE;gBACjC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBAClC,IAAI,CAAC,IAAI;oBAAE,OAAO,SAAS,CAAA;gBAC3B,OAAQ,IAAI,CAAC,OAAe,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,EAAE,OAAO,CAAC,CAAA;YAC7D,CAAC;YACD,qBAAqB,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,+CAA+C,CAAC,EAAE,CAAC;YAC1G,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI;YACxB,mBAAmB,EAAnB,kCAAmB;SACpB;QACD,gBAAgB,EAAE,KAAK,CAAC,gBAAgB;QACxC,CAAC,EAAE,KAAK,CAAC,CAAC;KACX,CAAC,CAAA;IAEF,OAAO,EAAE,GAAG,MAAM,EAAE,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAA;AAClE,CAAC","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 { ToolCallContext } from '../tool-registry/types.js'\n\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 /** 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 /** The request's translator — the stop notice is then in the reader's language. */\n t?: (key: string, params?: Record<string, any>) => string\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\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 const toolContext: ToolCallContext = { state, host: hostContext ?? {} }\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 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 executeReadTool: (call, context) => {\n const spec = byName.get(call.name)\n if (!spec) return undefined\n return (spec.builder as any)(call.arguments ?? {}, context)\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"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type AiCapabilityState = 'configured' | 'not-configured';
|
|
2
|
+
export interface AiCapability {
|
|
3
|
+
state: AiCapabilityState;
|
|
4
|
+
/** Which provider answers, when one does — for the person reading a boot log, not for a screen. */
|
|
5
|
+
provider?: string;
|
|
6
|
+
/** The default model this deployment would use. */
|
|
7
|
+
model?: string;
|
|
8
|
+
/** Why it is not configured, as a code: no-config · no-provider · unknown-provider · no-adapter. */
|
|
9
|
+
reason?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* What this server can do, as of now.
|
|
13
|
+
*
|
|
14
|
+
* Creating the client is what proves a provider adapter is loaded and the configuration parses, so
|
|
15
|
+
* this asks for it. It never throws: a deployment with no AI is a normal deployment.
|
|
16
|
+
*/
|
|
17
|
+
export declare function aiCapability(): AiCapability;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.aiCapability = aiCapability;
|
|
4
|
+
/**
|
|
5
|
+
* Is there an AI on this deployment, and what is missing — asked before anything is tried.
|
|
6
|
+
*
|
|
7
|
+
* The twin had a button that asked the assistant, and whether to show it was decided by whether a
|
|
8
|
+
* now-dead mutation had failed once. When the dock moved to the neutral door nothing called that
|
|
9
|
+
* mutation any more, so the answer was "configured" forever: the button appeared on deployments
|
|
10
|
+
* with no provider at all and failed when pressed (standard A7-a).
|
|
11
|
+
*
|
|
12
|
+
* A capability is read, not discovered by failing (ADR-0042 decision ②). It lives here because the
|
|
13
|
+
* provider configuration does, and the answer has three states — the third is the one a screen
|
|
14
|
+
* must not fold away: **not asked yet** is not "configured".
|
|
15
|
+
*/
|
|
16
|
+
const env_1 = require("@things-factory/env");
|
|
17
|
+
const factory_js_1 = require("./factory.js");
|
|
18
|
+
/**
|
|
19
|
+
* What this server can do, as of now.
|
|
20
|
+
*
|
|
21
|
+
* Creating the client is what proves a provider adapter is loaded and the configuration parses, so
|
|
22
|
+
* this asks for it. It never throws: a deployment with no AI is a normal deployment.
|
|
23
|
+
*/
|
|
24
|
+
function aiCapability() {
|
|
25
|
+
const client = safeDefaultClient();
|
|
26
|
+
if (client) {
|
|
27
|
+
const [provider, model] = String(client.id ?? '').split(':');
|
|
28
|
+
return { state: 'configured', provider: provider || undefined, model: model || undefined };
|
|
29
|
+
}
|
|
30
|
+
const cfg = env_1.config.get('aiClient', null);
|
|
31
|
+
if (!cfg)
|
|
32
|
+
return { state: 'not-configured', reason: 'no-config' };
|
|
33
|
+
if (!cfg.provider)
|
|
34
|
+
return { state: 'not-configured', reason: 'no-provider' };
|
|
35
|
+
if (!(0, factory_js_1.listRegisteredProviders)().includes(cfg.provider)) {
|
|
36
|
+
return { state: 'not-configured', provider: cfg.provider, reason: 'unknown-provider' };
|
|
37
|
+
}
|
|
38
|
+
return { state: 'not-configured', provider: cfg.provider, model: cfg.model, reason: 'no-adapter' };
|
|
39
|
+
}
|
|
40
|
+
function safeDefaultClient() {
|
|
41
|
+
try {
|
|
42
|
+
return (0, factory_js_1.getDefaultAIClient)();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* A broken configuration is "not configured", and it is the reason below that says which. */
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=capability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability.js","sourceRoot":"","sources":["../../server/service/capability.ts"],"names":[],"mappings":";;AAkCA,oCAcC;AAhDD;;;;;;;;;;;GAWG;AACH,6CAAyD;AAEzD,6CAA0E;AAc1E;;;;;GAKG;AACH,SAAgB,YAAY;IAC1B,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAC5D,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,IAAI,SAAS,EAAE,KAAK,EAAE,KAAK,IAAI,SAAS,EAAE,CAAA;IAC5F,CAAC;IAED,MAAM,GAAG,GAAG,YAAS,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAiD,CAAA;IAC3F,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,WAAW,EAAE,CAAA;IACjE,IAAI,CAAC,GAAG,CAAC,QAAQ;QAAE,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,aAAa,EAAE,CAAA;IAC5E,IAAI,CAAC,IAAA,oCAAuB,GAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtD,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAA;IACxF,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,CAAA;AACpG,CAAC;AAED,SAAS,iBAAiB;IACxB,IAAI,CAAC;QACH,OAAO,IAAA,+BAAkB,GAAE,CAAA;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,6FAA6F;QAC7F,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC","sourcesContent":["/**\n * Is there an AI on this deployment, and what is missing — asked before anything is tried.\n *\n * The twin had a button that asked the assistant, and whether to show it was decided by whether a\n * now-dead mutation had failed once. When the dock moved to the neutral door nothing called that\n * mutation any more, so the answer was \"configured\" forever: the button appeared on deployments\n * with no provider at all and failed when pressed (standard A7-a).\n *\n * A capability is read, not discovered by failing (ADR-0042 decision ②). It lives here because the\n * provider configuration does, and the answer has three states — the third is the one a screen\n * must not fold away: **not asked yet** is not \"configured\".\n */\nimport { config as envConfig } from '@things-factory/env'\n\nimport { getDefaultAIClient, listRegisteredProviders } from './factory.js'\n\nexport type AiCapabilityState = 'configured' | 'not-configured'\n\nexport interface AiCapability {\n state: AiCapabilityState\n /** Which provider answers, when one does — for the person reading a boot log, not for a screen. */\n provider?: string\n /** The default model this deployment would use. */\n model?: string\n /** Why it is not configured, as a code: no-config · no-provider · unknown-provider · no-adapter. */\n reason?: string\n}\n\n/**\n * What this server can do, as of now.\n *\n * Creating the client is what proves a provider adapter is loaded and the configuration parses, so\n * this asks for it. It never throws: a deployment with no AI is a normal deployment.\n */\nexport function aiCapability(): AiCapability {\n const client = safeDefaultClient()\n if (client) {\n const [provider, model] = String(client.id ?? '').split(':')\n return { state: 'configured', provider: provider || undefined, model: model || undefined }\n }\n\n const cfg = envConfig.get('aiClient', null) as { provider?: string; model?: string } | null\n if (!cfg) return { state: 'not-configured', reason: 'no-config' }\n if (!cfg.provider) return { state: 'not-configured', reason: 'no-provider' }\n if (!listRegisteredProviders().includes(cfg.provider)) {\n return { state: 'not-configured', provider: cfg.provider, reason: 'unknown-provider' }\n }\n return { state: 'not-configured', provider: cfg.provider, model: cfg.model, reason: 'no-adapter' }\n}\n\nfunction safeDefaultClient() {\n try {\n return getDefaultAIClient()\n } catch {\n /* A broken configuration is \"not configured\", and it is the reason below that says which. */\n return undefined\n }\n}\n"]}
|
|
@@ -9,5 +9,7 @@ tslib_1.__exportStar(require("./tool-registry/index.js"), exports);
|
|
|
9
9
|
tslib_1.__exportStar(require("./agentic-loop/index.js"), exports);
|
|
10
10
|
tslib_1.__exportStar(require("./assistant/index.js"), exports);
|
|
11
11
|
tslib_1.__exportStar(require("./analysis-contract.js"), exports);
|
|
12
|
+
/* 「이 배포에 AI 가 있나」 — 시도해서 알아내지 않고 먼저 읽는다(표준 A7-a). */
|
|
13
|
+
tslib_1.__exportStar(require("./capability.js"), exports);
|
|
12
14
|
tslib_1.__exportStar(require("./testing/tool-call-context.js"), exports);
|
|
13
15
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;AAAA,qDAA0B;AAC1B,uDAA4B;AAC5B,+DAAoC;AACpC,2DAAgC;AAChC,mEAAwC;AACxC,kEAAuC;AACvC,+DAAoC;AACpC,iEAAsC;AACtC,yEAA8C","sourcesContent":["export * from './types.js'\nexport * from './factory.js'\nexport * from './providers/index.js'\nexport * from './utils/index.js'\nexport * from './tool-registry/index.js'\nexport * from './agentic-loop/index.js'\nexport * from './assistant/index.js'\nexport * from './analysis-contract.js'\nexport * from './testing/tool-call-context.js'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;AAAA,qDAA0B;AAC1B,uDAA4B;AAC5B,+DAAoC;AACpC,2DAAgC;AAChC,mEAAwC;AACxC,kEAAuC;AACvC,+DAAoC;AACpC,iEAAsC;AACtC,qDAAqD;AACrD,0DAA+B;AAC/B,yEAA8C","sourcesContent":["export * from './types.js'\nexport * from './factory.js'\nexport * from './providers/index.js'\nexport * from './utils/index.js'\nexport * from './tool-registry/index.js'\nexport * from './agentic-loop/index.js'\nexport * from './assistant/index.js'\nexport * from './analysis-contract.js'\n/* 「이 배포에 AI 가 있나」 — 시도해서 알아내지 않고 먼저 읽는다(표준 A7-a). */\nexport * from './capability.js'\nexport * from './testing/tool-call-context.js'\n"]}
|