@things-factory/ai-client-base 10.1.53 → 10.1.114

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.
Files changed (48) hide show
  1. package/config/config.development.js +17 -0
  2. package/config/config.production.js +9 -0
  3. package/dist-server/service/agentic-loop/agentic-loop.d.ts +24 -1
  4. package/dist-server/service/agentic-loop/agentic-loop.js +67 -7
  5. package/dist-server/service/agentic-loop/agentic-loop.js.map +1 -1
  6. package/dist-server/service/decision/config.d.ts +23 -0
  7. package/dist-server/service/decision/config.js +39 -0
  8. package/dist-server/service/decision/config.js.map +1 -0
  9. package/dist-server/service/decision/decision.d.ts +37 -0
  10. package/dist-server/service/decision/decision.js +98 -0
  11. package/dist-server/service/decision/decision.js.map +1 -0
  12. package/dist-server/service/decision/index.d.ts +3 -0
  13. package/dist-server/service/decision/index.js +7 -0
  14. package/dist-server/service/decision/index.js.map +1 -0
  15. package/dist-server/service/decision/jev.d.ts +5 -0
  16. package/dist-server/service/decision/jev.js +46 -0
  17. package/dist-server/service/decision/jev.js.map +1 -0
  18. package/dist-server/service/index.d.ts +1 -0
  19. package/dist-server/service/index.js +1 -0
  20. package/dist-server/service/index.js.map +1 -1
  21. package/dist-server/service/providers/openai.d.ts +5 -0
  22. package/dist-server/service/providers/openai.js +5 -1
  23. package/dist-server/service/providers/openai.js.map +1 -1
  24. package/dist-server/tsconfig.tsbuildinfo +1 -1
  25. package/experiments/entity360/README.md +224 -0
  26. package/experiments/entity360/apps.cjs +103 -0
  27. package/experiments/entity360/apps.test.cjs +24 -0
  28. package/experiments/entity360/baseline-apps-results.json +3357 -0
  29. package/experiments/entity360/baseline-expanded-results.json +5952 -0
  30. package/experiments/entity360/baseline-results.json +1507 -0
  31. package/experiments/entity360/expanded.cjs +65 -0
  32. package/experiments/entity360/fixtures.json +100 -0
  33. package/experiments/entity360/index.html +44 -0
  34. package/experiments/entity360/jev-apps-results.json +3440 -0
  35. package/experiments/entity360/jev-expanded-results.json +6095 -0
  36. package/experiments/entity360/jev-results.json +1542 -0
  37. package/experiments/entity360/run.cjs +100 -0
  38. package/package.json +2 -2
  39. package/server/service/agentic-loop/agentic-loop.test.ts +123 -0
  40. package/server/service/agentic-loop/agentic-loop.ts +66 -8
  41. package/server/service/decision/config.test.ts +55 -0
  42. package/server/service/decision/config.ts +49 -0
  43. package/server/service/decision/decision.test.ts +94 -0
  44. package/server/service/decision/decision.ts +112 -0
  45. package/server/service/decision/index.ts +3 -0
  46. package/server/service/decision/jev.ts +40 -0
  47. package/server/service/index.ts +1 -0
  48. package/server/service/providers/openai.ts +5 -2
@@ -1,4 +1,21 @@
1
1
  module.exports = {
2
+ /*
3
+ Server-only decision model, independent of the generative aiClient below.
4
+ Enable in the application's config with aiDecision.enabled: true and provide
5
+ TYPESAFE_API_KEY in the server environment. No requests are made on startup.
6
+ Read with getDefaultDecisionClient(); call client.recommend(...) explicitly.
7
+ Missing configuration or enabled:false returns no client. Invalid enabled
8
+ configuration throws a field-only error (never credentials).
9
+ The pinned model is the version verified by the adapter; aliases are rejected.
10
+ timeoutMs covers authorization and the provider call, with no UI-path retries.
11
+ */
12
+ aiDecision: {
13
+ enabled: false,
14
+ provider: 'typesafe',
15
+ model: 'jev-1.13.0',
16
+ apiKey: process.env.TYPESAFE_API_KEY,
17
+ timeoutMs: 300
18
+ },
2
19
  /*
3
20
  AI Client Configuration
4
21
 
@@ -1,4 +1,13 @@
1
1
  module.exports = {
2
+ // Server-only. Enable explicitly in app config; inject the key through deployment secrets.
3
+ // See config.development.js for the decision-client contract and limits.
4
+ aiDecision: {
5
+ enabled: false,
6
+ provider: 'typesafe',
7
+ model: 'jev-1.13.0',
8
+ apiKey: process.env.TYPESAFE_API_KEY,
9
+ timeoutMs: 300
10
+ },
2
11
  /*
3
12
  AI Client Configuration — Production
4
13
 
@@ -1,4 +1,4 @@
1
- import type { AIMessage, AIToolCall, AIToolChatResult } from '../types.js';
1
+ import type { AIImagePart, AIMessage, AIToolCall, AIToolChatResult } from '../types.js';
2
2
  /**
3
3
  * AI 가 응답을 만드는 동안 호출한 도구의 시간순 trace.
4
4
  *
@@ -179,3 +179,26 @@ export declare function runAgenticLoop<TCtx = unknown, TOp = unknown>(input: Age
179
179
  * 보이면 카드가 둘이 되고, 사용자가 같은 명령을 두 번 실행할 수 있다.
180
180
  */
181
181
  export declare function proposalEffectKey(p: any): string;
182
+ /**
183
+ * A proposal's reserved `forScreen` field, taken out of what the model reads and kept for the card.
184
+ *
185
+ * A read tool that has queried many rows puts them on the card (the AI 작업판 table) but tells the model only the
186
+ * count, the totals and a few rows - the model does not copy a table it never received, so the card's numbers are
187
+ * the query's own (operato-application/design/ai/read-targets.md §4). Only a proposal (`proposed: true`) carries it.
188
+ */
189
+ export declare function takeForScreen(result: any): {
190
+ value: any;
191
+ forScreen: Record<string, unknown>;
192
+ };
193
+ /**
194
+ * The pictures a read tool returned in its reserved `images` field, taken out of the result.
195
+ *
196
+ * A tool result reaches the model as JSON text; a picture cannot. So a read tool that must show one - a picture
197
+ * sent in an earlier turn of the conversation - returns `images: [{ data, mediaType }]`, and the loop sends each as an
198
+ * image part of the same turn. The bytes never stay in the JSON, the grounding texts or the usage record. A picture
199
+ * of a type the providers do not all take is left out and said in the result.
200
+ */
201
+ export declare function takeToolImages(result: any): {
202
+ value: any;
203
+ images: AIImagePart[];
204
+ };
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runAgenticLoop = runAgenticLoop;
4
4
  exports.proposalEffectKey = proposalEffectKey;
5
+ exports.takeForScreen = takeForScreen;
6
+ exports.takeToolImages = takeToolImages;
5
7
  const tslib_1 = require("tslib");
6
8
  /**
7
9
  * agentic-loop — multi-turn LLM ↔ tool 루프. **도메인 중립.**
@@ -300,12 +302,17 @@ async function processToolCalls(toolCalls, context, dispatch, accumulatedWriteCa
300
302
  /** 몇 번째 LLM 턴인가 — 기록에 남겨 화면이 단계로 읽을 수 있게 한다. */
301
303
  iter) {
302
304
  const toolResultParts = [];
305
+ const toolResultImages = [];
303
306
  for (const tc of toolCalls) {
304
307
  if (dispatch.isReadTool(tc.name)) {
305
308
  /* await 필수 — registry 에 등록된 read/external builder 는 async 일 수 있다(DB·외부 호출).
306
309
  * await 없이 JSON.stringify 하면 Promise 가 '{}' 로 직렬화되어 LLM 이 **빈 결과**를 받는다
307
310
  * (문서 가져오기·트윈 구조 조회 등 — 무엇인지는 부르는 쪽이 안다). */
308
- const value = await dispatch.executeReadTool(tc, context);
311
+ /* Pictures a read tool hands back ride as image parts after the results - never inside the JSON. */
312
+ const { value: given, images } = takeToolImages(await dispatch.executeReadTool(tc, context));
313
+ toolResultImages.push(...images);
314
+ /* What only the card needs (a table of a thousand rows) goes to the screen, never to the model. */
315
+ const { value, forScreen } = takeForScreen(given);
309
316
  /* 제안 규약 — 결과가 `proposed: true` 면 조치 제안이다(도메인은 모른 채 그대로 실어 올린다).
310
317
  * 이 채널이 없으면 대화면은 도구 추적(trace)을 뒤져야 하고, 실행 버튼을 만들 근거가 없다. */
311
318
  /* 판정(outcome) — 화면이 단계별 배지로 보여줄 값. 조용한 무동작을 남기지 않는다. */
@@ -314,14 +321,23 @@ iter) {
314
321
  /* **같은 조치는 한 번만** — 모델이 같은 것을 두 번 제안하는 일이 실제로 있었다(하나는 인자가
315
322
  * 비어 있었다). 카드가 둘이면 사용자가 같은 명령을 두 번 실행할 수 있고, 그건 되돌릴 수 없다.
316
323
  * 판정은 **효과**(도구·명령·대상·인자)로 한다 — 설명 문구(label·reason)가 달라도 같은 조치다. */
317
- const candidate = { tool: tc.name, ...value };
318
- const key = proposalEffectKey(candidate);
319
- if (proposals.some(p => p.key === key)) {
324
+ const candidate = { tool: tc.name, ...value, ...forScreen };
325
+ const effectKey = proposalEffectKey(candidate);
326
+ if (proposals.some(p => p.effectKey === effectKey)) {
320
327
  outcome = 'folded'; // 접힌 사실을 남긴다 — "왜 카드가 하나뿐인가" 가 추적 가능해야 한다
321
328
  }
322
329
  else {
323
- /* 키를 함께 실어 보낸다 — 카드가 같은 규칙으로 다시 세지 않게(원본은 하나). */
324
- proposals.push({ ...candidate, key });
330
+ /*
331
+ * 키를 함께 실어 보낸다 — 카드가 같은 규칙으로 다시 세지 않게(원본은 하나).
332
+ *
333
+ * **도구가 이름을 준 제안은 그 이름을 쓴다**(2026-10-02, Figure 레인 · AI 레인 합의). figure-service 의
334
+ * 도구는 제안에 UUID `key` 를 붙이고, 모델러는 그 UUID 로 새 도형을 만든다(ADR-0097 5.6절 5항). 효과 키로
335
+ * 덮어쓰면 「제안 식별자가 올바르지 않습니다」로 실행이 막혔다. 같은 조치인지는 효과 키(`effectKey`)로
336
+ * 따로 판정하므로 접기는 그대로다. 도구 `key` 는 **한 대화 안에서 유일해야 한다** — 겹치면 입력안의 「새
337
+ * 카드가 옛 카드를 대신함」 판정과 결말 줄이 섞인다. 이름을 주지 않는 도구(ai-assistant 의 제안 도구들)는
338
+ * 지금처럼 효과 키를 받는다.
339
+ */
340
+ proposals.push({ ...candidate, key: typeof candidate.key === 'string' && candidate.key ? candidate.key : effectKey, effectKey });
325
341
  outcome = 'proposed';
326
342
  }
327
343
  }
@@ -427,6 +443,50 @@ iter) {
427
443
  });
428
444
  }
429
445
  }
430
- return toolResultParts;
446
+ /* Every provider takes a picture after the results of the same turn (design/ai/shared-dock.md §9-4). */
447
+ return [...toolResultParts, ...toolResultImages];
448
+ }
449
+ /**
450
+ * A proposal's reserved `forScreen` field, taken out of what the model reads and kept for the card.
451
+ *
452
+ * A read tool that has queried many rows puts them on the card (the AI 작업판 table) but tells the model only the
453
+ * count, the totals and a few rows - the model does not copy a table it never received, so the card's numbers are
454
+ * the query's own (operato-application/design/ai/read-targets.md §4). Only a proposal (`proposed: true`) carries it.
455
+ */
456
+ function takeForScreen(result) {
457
+ if (!result || typeof result !== 'object' || Array.isArray(result) || result.proposed !== true || !result.forScreen || typeof result.forScreen !== 'object') {
458
+ return { value: result, forScreen: {} };
459
+ }
460
+ const { forScreen, ...value } = result;
461
+ return { value, forScreen };
462
+ }
463
+ const TOOL_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
464
+ /**
465
+ * The pictures a read tool returned in its reserved `images` field, taken out of the result.
466
+ *
467
+ * A tool result reaches the model as JSON text; a picture cannot. So a read tool that must show one - a picture
468
+ * sent in an earlier turn of the conversation - returns `images: [{ data, mediaType }]`, and the loop sends each as an
469
+ * image part of the same turn. The bytes never stay in the JSON, the grounding texts or the usage record. A picture
470
+ * of a type the providers do not all take is left out and said in the result.
471
+ */
472
+ function takeToolImages(result) {
473
+ if (!result || typeof result !== 'object' || Array.isArray(result) || !Array.isArray(result.images))
474
+ return { value: result, images: [] };
475
+ const { images: given, ...value } = result;
476
+ const images = [];
477
+ let left = 0;
478
+ for (const one of given) {
479
+ const data = typeof one?.data === 'string' ? Buffer.from(one.data, 'base64') : one?.data;
480
+ const mediaType = String(one?.mediaType ?? '').toLowerCase();
481
+ if ((Buffer.isBuffer(data) || data instanceof Uint8Array) && data.length && TOOL_IMAGE_TYPES.includes(mediaType)) {
482
+ images.push({ type: 'image', source: { kind: 'base64', data, mediaType: mediaType } });
483
+ }
484
+ else
485
+ left++;
486
+ }
487
+ return {
488
+ value: { ...value, imagesAttached: images.length, ...(left ? { imagesLeftOut: left } : {}) },
489
+ images
490
+ };
431
491
  }
432
492
  //# sourceMappingURL=agentic-loop.js.map
@@ -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 { 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 /*\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 */\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
+ {"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 }\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 /*\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 */\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 const toolResultImages: AIImagePart[] = []\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 /* Pictures a read tool hands back ride as image parts after the results - never inside the JSON. */\n const { value: given, images } = takeToolImages(await dispatch.executeReadTool(tc, context))\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"]}
@@ -0,0 +1,23 @@
1
+ import { recommendCandidate, type DecisionResult } from './decision';
2
+ import { type JevDecisionPurpose } from './jev';
3
+ export interface AIDecisionConfig {
4
+ enabled: boolean;
5
+ provider?: 'typesafe';
6
+ model?: string;
7
+ apiKey?: string;
8
+ timeoutMs?: number;
9
+ }
10
+ export type DecisionRequest = Omit<Parameters<typeof recommendCandidate>[0], 'provider' | 'timeoutMs'> & {
11
+ /** Trusted host contract, not a model-selected mode. Defaults to entity selection. */
12
+ purpose?: JevDecisionPurpose;
13
+ };
14
+ export interface DecisionClient {
15
+ readonly provider: 'typesafe';
16
+ readonly model: string;
17
+ readonly timeoutMs: number;
18
+ recommend(request: DecisionRequest): Promise<DecisionResult>;
19
+ }
20
+ /** Validate once on construction. Errors never include supplied values or secrets. */
21
+ export declare function createConfiguredDecisionClient(config: AIDecisionConfig | null | undefined): DecisionClient | undefined;
22
+ /** Server configuration only; constructing this client performs no network request and does not cache credentials. */
23
+ export declare function getDefaultDecisionClient(): DecisionClient | undefined;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createConfiguredDecisionClient = createConfiguredDecisionClient;
4
+ exports.getDefaultDecisionClient = getDefaultDecisionClient;
5
+ const env_1 = require("@things-factory/env");
6
+ const decision_1 = require("./decision");
7
+ const jev_1 = require("./jev");
8
+ /** Validate once on construction. Errors never include supplied values or secrets. */
9
+ function createConfiguredDecisionClient(config) {
10
+ if (!config || config.enabled === false)
11
+ return undefined;
12
+ if (config.enabled !== true)
13
+ throw new Error('aiDecision.enabled must be a boolean');
14
+ if (config.provider !== 'typesafe')
15
+ throw new Error('aiDecision.provider is not supported');
16
+ const model = config.model ?? jev_1.JEV_DECISION_MODEL;
17
+ if (model !== jev_1.JEV_DECISION_MODEL)
18
+ throw new Error('aiDecision.model must use the supported pinned version');
19
+ const timeoutMs = config.timeoutMs ?? 300;
20
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > 10000)
21
+ throw new Error('aiDecision.timeoutMs must be between 1 and 10000');
22
+ if (typeof config.apiKey !== 'string' || !config.apiKey.trim())
23
+ throw new Error('aiDecision.apiKey is required when enabled');
24
+ const provider = (0, jev_1.createJevDecisionProvider)(config.apiKey.trim());
25
+ const actionProvider = (0, jev_1.createJevDecisionProvider)(config.apiKey.trim(), fetch, 'action');
26
+ return Object.freeze({
27
+ provider: 'typesafe', model, timeoutMs,
28
+ recommend: async ({ purpose = 'entity', ...request }) => {
29
+ if (purpose !== 'entity' && purpose !== 'action')
30
+ throw new Error('Unsupported decision purpose');
31
+ return (0, decision_1.recommendCandidate)({ ...request, provider: purpose === 'action' ? actionProvider : provider, timeoutMs });
32
+ }
33
+ });
34
+ }
35
+ /** Server configuration only; constructing this client performs no network request and does not cache credentials. */
36
+ function getDefaultDecisionClient() {
37
+ return createConfiguredDecisionClient(env_1.config.get('aiDecision', null));
38
+ }
39
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../server/service/decision/config.ts"],"names":[],"mappings":";;AAyBA,wEAkBC;AAGD,4DAEC;AAhDD,6CAAyD;AACzD,yCAAoE;AACpE,+BAA8F;AAsB9F,sFAAsF;AACtF,SAAgB,8BAA8B,CAAC,MAA2C;IACxF,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,OAAO,KAAK,KAAK;QAAE,OAAO,SAAS,CAAA;IACzD,IAAI,MAAM,CAAC,OAAO,KAAK,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAA;IACpF,IAAI,MAAM,CAAC,QAAQ,KAAK,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAA;IAC3F,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,wBAAkB,CAAA;IAChD,IAAI,KAAK,KAAK,wBAAkB;QAAE,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAA;IAC3G,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,IAAI,GAAG,CAAA;IACzC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,CAAC,IAAI,SAAS,GAAG,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAA;IAC1I,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAA;IAC7H,MAAM,QAAQ,GAAG,IAAA,+BAAyB,EAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA;IAChE,MAAM,cAAc,GAAG,IAAA,+BAAyB,EAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAA;IACvF,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,QAAQ,EAAE,UAAmB,EAAE,KAAK,EAAE,SAAS;QAC/C,SAAS,EAAE,KAAK,EAAE,EAAE,OAAO,GAAG,QAAQ,EAAE,GAAG,OAAO,EAAmB,EAAE,EAAE;YACvE,IAAI,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,QAAQ;gBAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAA;YACjG,OAAO,IAAA,6BAAkB,EAAC,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAA;QAClH,CAAC;KACF,CAAC,CAAA;AACJ,CAAC;AAED,sHAAsH;AACtH,SAAgB,wBAAwB;IACtC,OAAO,8BAA8B,CAAC,YAAS,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,CAA4B,CAAC,CAAA;AACrG,CAAC","sourcesContent":["import { config as envConfig } from '@things-factory/env'\nimport { recommendCandidate, type DecisionResult } from './decision'\nimport { createJevDecisionProvider, JEV_DECISION_MODEL, type JevDecisionPurpose } from './jev'\n\nexport interface AIDecisionConfig {\n enabled: boolean\n provider?: 'typesafe'\n model?: string\n apiKey?: string\n timeoutMs?: number\n}\n\nexport type DecisionRequest = Omit<Parameters<typeof recommendCandidate>[0], 'provider' | 'timeoutMs'> & {\n /** Trusted host contract, not a model-selected mode. Defaults to entity selection. */\n purpose?: JevDecisionPurpose\n}\n\nexport interface DecisionClient {\n readonly provider: 'typesafe'\n readonly model: string\n readonly timeoutMs: number\n recommend(request: DecisionRequest): Promise<DecisionResult>\n}\n\n/** Validate once on construction. Errors never include supplied values or secrets. */\nexport function createConfiguredDecisionClient(config: AIDecisionConfig | null | undefined): DecisionClient | undefined {\n if (!config || config.enabled === false) return undefined\n if (config.enabled !== true) throw new Error('aiDecision.enabled must be a boolean')\n if (config.provider !== 'typesafe') throw new Error('aiDecision.provider is not supported')\n const model = config.model ?? JEV_DECISION_MODEL\n if (model !== JEV_DECISION_MODEL) throw new Error('aiDecision.model must use the supported pinned version')\n const timeoutMs = config.timeoutMs ?? 300\n if (!Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > 10000) throw new Error('aiDecision.timeoutMs must be between 1 and 10000')\n if (typeof config.apiKey !== 'string' || !config.apiKey.trim()) throw new Error('aiDecision.apiKey is required when enabled')\n const provider = createJevDecisionProvider(config.apiKey.trim())\n const actionProvider = createJevDecisionProvider(config.apiKey.trim(), fetch, 'action')\n return Object.freeze({\n provider: 'typesafe' as const, model, timeoutMs,\n recommend: async ({ purpose = 'entity', ...request }: DecisionRequest) => {\n if (purpose !== 'entity' && purpose !== 'action') throw new Error('Unsupported decision purpose')\n return recommendCandidate({ ...request, provider: purpose === 'action' ? actionProvider : provider, timeoutMs })\n }\n })\n}\n\n/** Server configuration only; constructing this client performs no network request and does not cache credentials. */\nexport function getDefaultDecisionClient(): DecisionClient | undefined {\n return createConfiguredDecisionClient(envConfig.get('aiDecision', null) as AIDecisionConfig | null)\n}\n"]}
@@ -0,0 +1,37 @@
1
+ /** Bounded, read-only recommendations. The host owns retrieval, permissions and opening a 360. */
2
+ export interface DecisionCandidate {
3
+ key: string;
4
+ label: string;
5
+ description: string;
6
+ }
7
+ export interface DecisionProvider {
8
+ name: string;
9
+ choose(query: string, candidates: readonly DecisionCandidate[], signal: AbortSignal): Promise<unknown>;
10
+ }
11
+ export interface DecisionReply {
12
+ choice: string;
13
+ probabilities: Record<string, number>;
14
+ confidence: number;
15
+ }
16
+ export interface DecisionResult {
17
+ status: 'suggested' | 'abstained';
18
+ reason: string;
19
+ candidateKey?: string;
20
+ provider: string;
21
+ elapsedMs: number;
22
+ total: number;
23
+ eligible: number;
24
+ }
25
+ export declare const NO_MATCH = "__none__";
26
+ /** No fuzzy certainty: the baseline recommends only one exact label or key. */
27
+ export declare const exactDecisionProvider: DecisionProvider;
28
+ export declare function recommendCandidate(input: {
29
+ query: string;
30
+ candidates: readonly DecisionCandidate[];
31
+ /** Required, fail-closed, called before provider access and again on the selected candidate. */
32
+ mayRead(candidate: DecisionCandidate): Promise<boolean>;
33
+ /** Includes user, tenant and screen revision. A late answer must not apply to another context. */
34
+ isCurrent(): boolean;
35
+ provider?: DecisionProvider;
36
+ timeoutMs?: number;
37
+ }): Promise<DecisionResult>;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.exactDecisionProvider = exports.NO_MATCH = void 0;
4
+ exports.recommendCandidate = recommendCandidate;
5
+ exports.NO_MATCH = '__none__';
6
+ const normalize = (text) => text.normalize('NFKC').trim().toLocaleLowerCase();
7
+ /** No fuzzy certainty: the baseline recommends only one exact label or key. */
8
+ exports.exactDecisionProvider = {
9
+ name: 'exact-v1',
10
+ async choose(query, candidates) {
11
+ const hits = candidates.filter(c => [c.key, c.label].some(v => normalize(v) === normalize(query)));
12
+ const choice = hits.length === 1 ? hits[0].key : exports.NO_MATCH;
13
+ return { choice, confidence: 1, probabilities: Object.fromEntries([...candidates.map(c => c.key), exports.NO_MATCH].map(k => [k, k === choice ? 1 : 0])) };
14
+ }
15
+ };
16
+ function validReply(value, keys) {
17
+ const r = value;
18
+ if (!r || !keys.includes(r.choice) || !Number.isFinite(r.confidence) || r.confidence < 0 || r.confidence > 1)
19
+ return false;
20
+ if (!r.probabilities || typeof r.probabilities !== 'object' || Array.isArray(r.probabilities))
21
+ return false;
22
+ const entries = Object.entries(r.probabilities);
23
+ if (entries.length !== keys.length || entries.some(([k, p]) => !keys.includes(k) || !Number.isFinite(p) || p < 0 || p > 1))
24
+ return false;
25
+ if (Math.abs(entries.reduce((sum, [, p]) => sum + p, 0) - 1) > 0.001)
26
+ return false;
27
+ return entries.every(([, p]) => p <= r.probabilities[r.choice]);
28
+ }
29
+ async function recommendCandidate(input) {
30
+ const started = Date.now();
31
+ const provider = input.provider ?? exports.exactDecisionProvider;
32
+ let eligible = 0;
33
+ const result = (reason, candidateKey) => ({
34
+ status: candidateKey ? 'suggested' : 'abstained', reason, candidateKey,
35
+ provider: provider.name, elapsedMs: Date.now() - started, total: input.candidates.length, eligible
36
+ });
37
+ // An oversized shortlist needs better retrieval, not silent truncation.
38
+ if (!input.query.trim() || input.query.length > 2000 || input.candidates.length > 12 ||
39
+ input.candidates.some(c => !c.key || c.key === exports.NO_MATCH || c.key.length > 100 || !c.label || c.label.length > 200 || c.description.length > 1000) ||
40
+ new Set(input.candidates.map(c => c.key)).size !== input.candidates.length)
41
+ return result('invalid-input');
42
+ const timeoutMs = input.timeoutMs ?? 300;
43
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > 10000)
44
+ return result('invalid-input');
45
+ const snapshot = input.candidates.map(c => ({ key: c.key, label: c.label, description: c.description }));
46
+ const controller = new AbortController();
47
+ let timer;
48
+ let expired = false;
49
+ const current = () => !expired && input.isCurrent();
50
+ const allowed = async (c) => {
51
+ try {
52
+ return await input.mayRead(c) === true;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ };
58
+ const work = async () => {
59
+ if (!current())
60
+ return result('stale-context');
61
+ const access = await Promise.all(snapshot.map(allowed));
62
+ const candidates = snapshot.filter((_, i) => access[i]);
63
+ eligible = candidates.length;
64
+ if (!current())
65
+ return result('stale-context');
66
+ if (!eligible)
67
+ return result('no-candidates');
68
+ const reply = await provider.choose(input.query, candidates, controller.signal);
69
+ if (!current())
70
+ return result('stale-context');
71
+ if (!validReply(reply, [...candidates.map(c => c.key), exports.NO_MATCH]))
72
+ return result('invalid-reply');
73
+ if (reply.choice === exports.NO_MATCH)
74
+ return result('no-match');
75
+ const ordered = Object.values(reply.probabilities).sort((a, b) => b - a);
76
+ // Experimental recommendation thresholds, not a calibrated correctness guarantee.
77
+ if (reply.confidence < 0.8 || ordered[0] < 0.8 || ordered[0] - ordered[1] < 0.2)
78
+ return result('uncertain');
79
+ const selected = candidates.find(c => c.key === reply.choice);
80
+ if (!await allowed(selected))
81
+ return result('access-changed');
82
+ if (!current())
83
+ return result('stale-context');
84
+ return result('candidate-match', selected.key);
85
+ };
86
+ try {
87
+ return await Promise.race([
88
+ work().catch(() => result('provider-error')),
89
+ new Promise(resolve => {
90
+ timer = setTimeout(() => { expired = true; controller.abort(); resolve(result('timeout')); }, timeoutMs);
91
+ })
92
+ ]);
93
+ }
94
+ finally {
95
+ clearTimeout(timer);
96
+ }
97
+ }
98
+ //# sourceMappingURL=decision.js.map