@things-factory/ai-client-base 10.1.3 → 10.1.7

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 (44) hide show
  1. package/dist-server/service/assistant/abort-notice.d.ts +7 -0
  2. package/dist-server/service/assistant/abort-notice.js +29 -0
  3. package/dist-server/service/assistant/abort-notice.js.map +1 -0
  4. package/dist-server/service/assistant/assistant-chat.d.ts +41 -0
  5. package/dist-server/service/assistant/assistant-chat.js +63 -0
  6. package/dist-server/service/assistant/assistant-chat.js.map +1 -0
  7. package/dist-server/service/assistant/index.d.ts +5 -0
  8. package/dist-server/service/assistant/index.js +14 -0
  9. package/dist-server/service/assistant/index.js.map +1 -0
  10. package/dist-server/service/assistant/registry-assistant.d.ts +46 -0
  11. package/dist-server/service/assistant/registry-assistant.js +71 -0
  12. package/dist-server/service/assistant/registry-assistant.js.map +1 -0
  13. package/dist-server/service/assistant/registry-gate.d.ts +31 -0
  14. package/dist-server/service/assistant/registry-gate.js +61 -0
  15. package/dist-server/service/assistant/registry-gate.js.map +1 -0
  16. package/dist-server/service/assistant/summarize.d.ts +7 -0
  17. package/dist-server/service/assistant/summarize.js +67 -0
  18. package/dist-server/service/assistant/summarize.js.map +1 -0
  19. package/dist-server/service/index.d.ts +1 -0
  20. package/dist-server/service/index.js +1 -0
  21. package/dist-server/service/index.js.map +1 -1
  22. package/dist-server/service/tool-registry/registry.js +19 -0
  23. package/dist-server/service/tool-registry/registry.js.map +1 -1
  24. package/dist-server/service/tool-registry/types.d.ts +11 -1
  25. package/dist-server/service/tool-registry/types.js.map +1 -1
  26. package/dist-server/service/utils/grounding.d.ts +25 -2
  27. package/dist-server/service/utils/grounding.js +44 -8
  28. package/dist-server/service/utils/grounding.js.map +1 -1
  29. package/dist-server/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +2 -2
  31. package/server/service/assistant/abort-notice.ts +44 -0
  32. package/server/service/assistant/assistant-chat.test.ts +218 -0
  33. package/server/service/assistant/assistant-chat.ts +99 -0
  34. package/server/service/assistant/index.ts +10 -0
  35. package/server/service/assistant/registry-assistant.test.ts +182 -0
  36. package/server/service/assistant/registry-assistant.ts +137 -0
  37. package/server/service/assistant/registry-gate.ts +76 -0
  38. package/server/service/assistant/summarize.ts +61 -0
  39. package/server/service/index.ts +1 -0
  40. package/server/service/tool-registry/registry.test.ts +59 -4
  41. package/server/service/tool-registry/registry.ts +22 -0
  42. package/server/service/tool-registry/types.ts +11 -1
  43. package/server/service/utils/grounding.test.ts +49 -0
  44. package/server/service/utils/grounding.ts +74 -8
@@ -0,0 +1,7 @@
1
+ import type { AgenticLoopResult } from '../agentic-loop/index.js';
2
+ /**
3
+ * 중단 이유 → 답에 덧붙일 한 줄. 정상 종료면 빈 문자열이다.
4
+ *
5
+ * 답을 고치지 않고 **뒤에 붙인다** — 모델이 한 말을 우리가 다시 쓰지 않는다.
6
+ */
7
+ export declare function formatAbortNotice(abortReason: AgenticLoopResult<any>['abortReason']): string;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatAbortNotice = formatAbortNotice;
4
+ const index_js_1 = require("../utils/index.js");
5
+ /**
6
+ * 중단 이유 → 답에 덧붙일 한 줄. 정상 종료면 빈 문자열이다.
7
+ *
8
+ * 답을 고치지 않고 **뒤에 붙인다** — 모델이 한 말을 우리가 다시 쓰지 않는다.
9
+ */
10
+ function formatAbortNotice(abortReason) {
11
+ if (!abortReason)
12
+ return '';
13
+ switch (abortReason.type) {
14
+ case 'aborted':
15
+ return '\n\n_(요청이 중단되어 일부만 처리됐어요.)_';
16
+ case 'provider_error':
17
+ /* 남의 말(SDK 원문)을 사용자에게 보여주지 않는다 — "Error fetching from undefined: [503 …]" 은
18
+ * 사용자가 할 수 있는 일을 알려주지 않는다. 일시 장애는 이미 몇 번 다시 시도한 뒤이므로,
19
+ * 지금 필요한 안내는 "잠시 후 다시" 하나다. 원문은 서버 로그에 남는다. */
20
+ return (0, index_js_1.isTransientAIError)({ message: abortReason.message })
21
+ ? '\n\n_(AI 서비스가 일시적으로 응답하지 않았어요. 잠시 후 다시 시도해 주세요.)_'
22
+ : '\n\n_(처리 중 오류가 발생했어요. 같은 요청이 계속 실패하면 관리자에게 알려 주세요.)_';
23
+ case 'max_iterations':
24
+ return `\n\n_(작업이 너무 길어 ${abortReason.iter}회에서 중단했어요. 더 작게 나눠 요청해보세요.)_`;
25
+ case 'repeated_validation_failure':
26
+ return `\n\n_(\`${abortReason.toolName}\` 호출이 ${abortReason.consecutive}회 연속 실패해 중단했어요. 요청을 다시 표현해보세요.)_`;
27
+ }
28
+ }
29
+ //# sourceMappingURL=abort-notice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abort-notice.js","sourceRoot":"","sources":["../../../server/service/assistant/abort-notice.ts"],"names":[],"mappings":";;AA0BA,8CAiBC;AAxBD,gDAAsD;AAEtD;;;;GAIG;AACH,SAAgB,iBAAiB,CAAC,WAAkD;IAClF,IAAI,CAAC,WAAW;QAAE,OAAO,EAAE,CAAA;IAC3B,QAAQ,WAAW,CAAC,IAAI,EAAE,CAAC;QACzB,KAAK,SAAS;YACZ,OAAO,6BAA6B,CAAA;QACtC,KAAK,gBAAgB;YACnB;;2DAE+C;YAC/C,OAAO,IAAA,6BAAkB,EAAC,EAAE,OAAO,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC;gBACzD,CAAC,CAAC,mDAAmD;gBACrD,CAAC,CAAC,sDAAsD,CAAA;QAC5D,KAAK,gBAAgB;YACnB,OAAO,mBAAmB,WAAW,CAAC,IAAI,8BAA8B,CAAA;QAC1E,KAAK,6BAA6B;YAChC,OAAO,WAAW,WAAW,CAAC,QAAQ,UAAU,WAAW,CAAC,WAAW,kCAAkC,CAAA;IAC7G,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 * ── Known debt: the strings are Korean ─────────────────────────────────────────\n * They were Korean in board-ai and they still are. Moving them does not make that worse, but a\n * framework package is the wrong home for untranslated user-facing text. When this needs a second\n * locale, the shape is a caller-supplied translator, not a table in here — the notice is one line\n * appended to a reply the product already owns.\n */\nimport type { AgenticLoopResult } from '../agentic-loop/index.js'\nimport { isTransientAIError } from '../utils/index.js'\n\n/**\n * 중단 이유 → 답에 덧붙일 한 줄. 정상 종료면 빈 문자열이다.\n *\n * 답을 고치지 않고 **뒤에 붙인다** — 모델이 한 말을 우리가 다시 쓰지 않는다.\n */\nexport function formatAbortNotice(abortReason: AgenticLoopResult<any>['abortReason']): string {\n if (!abortReason) return ''\n switch (abortReason.type) {\n case 'aborted':\n return '\\n\\n_(요청이 중단되어 일부만 처리됐어요.)_'\n case 'provider_error':\n /* 남의 말(SDK 원문)을 사용자에게 보여주지 않는다 — \"Error fetching from undefined: [503 …]\" 은\n * 사용자가 할 수 있는 일을 알려주지 않는다. 일시 장애는 이미 몇 번 다시 시도한 뒤이므로,\n * 지금 필요한 안내는 \"잠시 후 다시\" 하나다. 원문은 서버 로그에 남는다. */\n return isTransientAIError({ message: abortReason.message })\n ? '\\n\\n_(AI 서비스가 일시적으로 응답하지 않았어요. 잠시 후 다시 시도해 주세요.)_'\n : '\\n\\n_(처리 중 오류가 발생했어요. 같은 요청이 계속 실패하면 관리자에게 알려 주세요.)_'\n case 'max_iterations':\n return `\\n\\n_(작업이 너무 길어 ${abortReason.iter}회에서 중단했어요. 더 작게 나눠 요청해보세요.)_`\n case 'repeated_validation_failure':\n return `\\n\\n_(\\`${abortReason.toolName}\\` 호출이 ${abortReason.consecutive}회 연속 실패해 중단했어요. 요청을 다시 표현해보세요.)_`\n }\n}\n"]}
@@ -0,0 +1,41 @@
1
+ import { type AgenticLoopInput, type AgenticLoopResult } from '../agentic-loop/index.js';
2
+ import { type GroundingSources } from '../utils/index.js';
3
+ export interface AssistantChatInput<TCtx = unknown, TOp = unknown> extends AgenticLoopInput<TCtx, TOp> {
4
+ /**
5
+ * 이 턴에 모델이 본 것 — **도구 결과 말고** 전부. 도구 결과는 이 함수가 루프에서 받아 더한다.
6
+ *
7
+ * 빠뜨리면 정상 답이 경고를 받는다(오탐). 시스템 프롬프트에 적어 보낸 타입 이름을 모델이 말하는
8
+ * 것은 근거 있는 발언이다.
9
+ */
10
+ groundingSources?: Omit<GroundingSources, 'toolResults'>;
11
+ /**
12
+ * 접지 검사를 끈다 — **기본은 켜짐.**
13
+ *
14
+ * 끄는 것이 맞는 자리가 있다: 식별자를 다루지 않는 대화(문서 상담·개념 질문)는 근거집합에 식별자가
15
+ * 없어 모든 고유명사가 경고로 나온다. 그때는 끄는 것이 정직하고, **켠 채로 경고를 버리는 것**이
16
+ * 정직하지 않다.
17
+ */
18
+ skipGrounding?: boolean;
19
+ }
20
+ export interface AssistantChatResult<TOp = unknown> {
21
+ /**
22
+ * 사용자에게 보일 답 — 모델의 마지막 말 + 중단 통지.
23
+ *
24
+ * 모델의 말을 고치지 않는다. 통지는 **뒤에 붙는다.**
25
+ */
26
+ reply: string;
27
+ /** 통지가 붙기 전의 답. 접지 검사가 본 것이고, 호스트가 다시 판정할 때 쓴다. */
28
+ baseText: string;
29
+ /** 근거 밖의 식별자를 지목한 자리들. 없으면 undefined — 빈 배열을 주지 않는다. */
30
+ groundingWarnings?: string[];
31
+ /** 루프 결과 원본 — 쓰기 호출·조치·제안·도구 흔적. 호스트가 자기 뜻으로 해석한다. */
32
+ loop: AgenticLoopResult<TOp>;
33
+ }
34
+ /**
35
+ * 한 턴을 돌린다.
36
+ *
37
+ * `lastText` 가 없을 수 있다(도구만 부르고 말을 안 한 턴). 그 경우 `baseText` 는 빈 문자열이고,
38
+ * 무엇을 대신 말할지는 호스트가 정한다 — 여기서 「변경했습니다」 같은 말을 지어내지 않는다.
39
+ * 도메인을 모르는 자리에서 결과를 서술하면 그것이 곧 지어낸 원인이 된다.
40
+ */
41
+ export declare function assistantChat<TCtx = unknown, TOp = unknown>(input: AssistantChatInput<TCtx, TOp>): Promise<AssistantChatResult<TOp>>;
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assistantChat = assistantChat;
4
+ /*
5
+ * One turn of an assistant — the loop **with the guards that must not be optional.**
6
+ *
7
+ * ── Why this exists (2026-09-10) ────────────────────────────────────────────────
8
+ * `runAgenticLoop` is neutral and has been since 2026-09-05, when the same loop running in two
9
+ * places was consolidated. The note on that change is the reason this file exists: it was "not a
10
+ * duplication, a defect" — the two copies had **their safety measures set opposite ways**.
11
+ *
12
+ * The loop came out. The guards around it did not. They stayed in `board-ai`, wrapped around
13
+ * `DefaultBoardAIAssistant.chat`, which meant a product with no board of its own could only get
14
+ * them by going through the board editor. The plant lane read that correctly — it uses none of
15
+ * board's session machinery — and asked whether it could call `runAgenticLoop` directly instead.
16
+ *
17
+ * The answer was no, and the reason is written in `board-ai`'s own grounding file: the twin's
18
+ * header route had a grounding guard and its dock route did not, so **the same question was
19
+ * verified on one side and not the other.** A per-product door makes that three ways. This file is
20
+ * the door instead.
21
+ *
22
+ * ── What is guaranteed here, and what the host still owns ───────────────────────
23
+ * Guaranteed, because a caller cannot skip it:
24
+ * · the loop stops for a reason and the reason reaches the reply (`formatAbortNotice`)
25
+ * · the reply is checked against what the model was actually given (`groundingWarnings`)
26
+ *
27
+ * Still the host's, because only the host knows the domain:
28
+ * · which tools exist and what a write call means → `gate`, `dispatch`
29
+ * · what counts as evidence beyond the tool results → `groundingSources`
30
+ *
31
+ * ── Grounding sources: leaving one out produces false warnings ──────────────────
32
+ * The corpus is everything the model saw this turn. The tool results are added here, from
33
+ * `groundedTexts` — the raw strings sent back as `tool_result`, never `toolUsages[].result`, which
34
+ * is depth-limited for display. A check reading the display copy can pass while the model was
35
+ * handed `{}`; an un-awaited async builder did exactly that.
36
+ *
37
+ * Anything else the host put in front of the model — a system prompt, a state summary, the user's
38
+ * own words — has to be declared, or a correct answer quoting it gets warned about.
39
+ */
40
+ const index_js_1 = require("../agentic-loop/index.js");
41
+ const index_js_2 = require("../utils/index.js");
42
+ const abort_notice_js_1 = require("./abort-notice.js");
43
+ /**
44
+ * 한 턴을 돌린다.
45
+ *
46
+ * `lastText` 가 없을 수 있다(도구만 부르고 말을 안 한 턴). 그 경우 `baseText` 는 빈 문자열이고,
47
+ * 무엇을 대신 말할지는 호스트가 정한다 — 여기서 「변경했습니다」 같은 말을 지어내지 않는다.
48
+ * 도메인을 모르는 자리에서 결과를 서술하면 그것이 곧 지어낸 원인이 된다.
49
+ */
50
+ async function assistantChat(input) {
51
+ const { groundingSources, skipGrounding, ...loopInput } = input;
52
+ const loop = await (0, index_js_1.runAgenticLoop)(loopInput);
53
+ const baseText = loop.lastText ?? '';
54
+ const reply = baseText + (0, abort_notice_js_1.formatAbortNotice)(loop.abortReason);
55
+ let warnings;
56
+ if (!skipGrounding && baseText) {
57
+ const corpus = (0, index_js_2.buildGroundedCorpus)({ ...(groundingSources ?? {}), toolResults: loop.groundedTexts });
58
+ const found = (0, index_js_2.groundingWarnings)(baseText, corpus);
59
+ warnings = found.length > 0 ? found : undefined;
60
+ }
61
+ return { reply, baseText, groundingWarnings: warnings, loop };
62
+ }
63
+ //# sourceMappingURL=assistant-chat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assistant-chat.js","sourceRoot":"","sources":["../../../server/service/assistant/assistant-chat.ts"],"names":[],"mappings":";;AAgFA,sCAkBC;AAlGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,uDAAwG;AACxG,gDAAiG;AACjG,uDAAqD;AAmCrD;;;;;;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,CAAC,CAAA;IAE5D,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 } from './abort-notice.js'\n\nexport interface AssistantChatInput<TCtx = unknown, TOp = unknown> extends AgenticLoopInput<TCtx, TOp> {\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)\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"]}
@@ -0,0 +1,5 @@
1
+ export * from './abort-notice.js';
2
+ export * from './summarize.js';
3
+ export * from './registry-gate.js';
4
+ export * from './assistant-chat.js';
5
+ export * from './registry-assistant.js';
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ /*
5
+ * The neutral assistant layer — the agentic loop plus the guards that must not be optional.
6
+ *
7
+ * See `assistant-chat.ts` for why this is not in a product package.
8
+ */
9
+ tslib_1.__exportStar(require("./abort-notice.js"), exports);
10
+ tslib_1.__exportStar(require("./summarize.js"), exports);
11
+ tslib_1.__exportStar(require("./registry-gate.js"), exports);
12
+ tslib_1.__exportStar(require("./assistant-chat.js"), exports);
13
+ tslib_1.__exportStar(require("./registry-assistant.js"), exports);
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/assistant/index.ts"],"names":[],"mappings":";;;AAAA;;;;GAIG;AACH,4DAAiC;AACjC,yDAA8B;AAC9B,6DAAkC;AAClC,8DAAmC;AACnC,kEAAuC","sourcesContent":["/*\n * The neutral assistant layer — the agentic loop plus the guards that must not be optional.\n *\n * See `assistant-chat.ts` for why this is not in a product package.\n */\nexport * from './abort-notice.js'\nexport * from './summarize.js'\nexport * from './registry-gate.js'\nexport * from './assistant-chat.js'\nexport * from './registry-assistant.js'\n"]}
@@ -0,0 +1,46 @@
1
+ import type { AIClient, AIMessage } from '../types.js';
2
+ import { type AssistantChatResult } from './assistant-chat.js';
3
+ import type { GroundingSources } from '../utils/index.js';
4
+ /** What a registry tool is handed when it runs. `state` is the caller's koa state. */
5
+ export interface RegistryToolCallContext {
6
+ state: any;
7
+ /** What this surface is looking at — the host passes it through; tools read it as `host`. */
8
+ host?: any;
9
+ }
10
+ export interface RegistryAssistantInput {
11
+ /** Provider client. Must implement `chatWithTools`. */
12
+ client: AIClient;
13
+ /** The conversation so far, newest last. The caller owns history and truncation. */
14
+ messages: AIMessage[];
15
+ /** Who the assistant is and what this surface is. The host owns this prose. */
16
+ systemPrompt: string;
17
+ /**
18
+ * Categories this conversation may use. Omit to allow every registered category.
19
+ *
20
+ * An empty array is not the same as omitting: it allows nothing, and the conversation runs with
21
+ * no tools at all. That is a legitimate surface (plain chat), so it is not corrected here.
22
+ */
23
+ toolCategories?: string[];
24
+ /** Passed to tools as `context.host`. Client-supplied — never used for authorization. */
25
+ hostContext?: any;
26
+ /** Caller's koa state (domain, user, tx). Tools that read the database need it. */
27
+ state: any;
28
+ model?: string;
29
+ temperature?: number;
30
+ maxTokens?: number;
31
+ /** Make the model call a tool before it may answer. For surfaces that report live state. */
32
+ requireToolOnFirstTurn?: boolean;
33
+ /** Texts the answer is allowed to draw identifiers from, besides the tool results. */
34
+ groundingSources?: Omit<GroundingSources, 'toolResults'>;
35
+ }
36
+ export interface RegistryAssistantResult extends AssistantChatResult<unknown> {
37
+ /** Tool names actually offered to the model this turn. The caller may log or return it. */
38
+ offeredTools: string[];
39
+ }
40
+ /**
41
+ * Run one turn of a conversation whose only tools come from the registry.
42
+ *
43
+ * Throws when the client cannot use tools at all — a surface built on registered tools that
44
+ * silently ran without them would answer from nothing and look like it had checked.
45
+ */
46
+ export declare function registryAssistantChat(input: RegistryAssistantInput): Promise<RegistryAssistantResult>;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registryAssistantChat = registryAssistantChat;
4
+ const index_js_1 = require("../tool-registry/index.js");
5
+ const assistant_chat_js_1 = require("./assistant-chat.js");
6
+ const summarize_js_1 = require("./summarize.js");
7
+ /** `read` and `external` are the two kinds that answer a question without changing a document. */
8
+ function answeringSpecs(filter) {
9
+ return (0, index_js_1.getToolSpecsFor)(filter).filter(spec => spec.kind === 'read' || spec.kind === 'external');
10
+ }
11
+ /**
12
+ * Run one turn of a conversation whose only tools come from the registry.
13
+ *
14
+ * Throws when the client cannot use tools at all — a surface built on registered tools that
15
+ * silently ran without them would answer from nothing and look like it had checked.
16
+ */
17
+ async function registryAssistantChat(input) {
18
+ const { client, messages, systemPrompt, toolCategories, hostContext, state } = input;
19
+ if (!client.chatWithTools) {
20
+ throw new Error('[registry-assistant] the configured AI client cannot call tools, so this conversation cannot run');
21
+ }
22
+ const filter = toolCategories;
23
+ const specs = answeringSpecs(filter);
24
+ const byName = new Map(specs.map(spec => [spec.name, spec]));
25
+ const tools = specs.map(spec => ({
26
+ name: spec.name,
27
+ description: spec.themeNote ? `${spec.description} ${spec.themeNote}` : spec.description,
28
+ parameters: spec.schema
29
+ }));
30
+ /*
31
+ * The rules for using a tool belong to whoever registered it. Collect only the exposed ones, so
32
+ * a surface never reads instructions for a category it cannot call.
33
+ */
34
+ const guidance = (0, index_js_1.getCategoryGuidance)(filter);
35
+ const prompt = guidance.length > 0 ? `${systemPrompt}\n\n${guidance.join('\n\n')}` : systemPrompt;
36
+ const toolContext = { state, host: hostContext };
37
+ const result = await (0, assistant_chat_js_1.assistantChat)({
38
+ initialConversation: messages,
39
+ tools,
40
+ options: {
41
+ systemPrompt: prompt,
42
+ model: input.model,
43
+ temperature: input.temperature,
44
+ maxTokens: input.maxTokens,
45
+ requireToolOnFirstTurn: input.requireToolOnFirstTurn
46
+ },
47
+ chat: (msgs, toolDefs, opts) => client.chatWithTools(msgs, toolDefs, opts),
48
+ context: toolContext,
49
+ dispatch: {
50
+ isReadTool: name => byName.has(name),
51
+ /*
52
+ * This surface has no document, so nothing here can write. Saying so plainly makes the loop
53
+ * report an unknown name back to the model instead of accepting a call and dropping it.
54
+ */
55
+ isWriteTool: () => false,
56
+ isActionTool: () => false,
57
+ executeReadTool: (call, context) => {
58
+ const spec = byName.get(call.name);
59
+ if (!spec)
60
+ return undefined;
61
+ return spec.builder(call.arguments ?? {}, context);
62
+ },
63
+ validateWriteToolCall: () => ({ valid: false, errors: ['this conversation has no document to write to'] }),
64
+ toolCallToOp: () => null,
65
+ summarizeToolResult: summarize_js_1.summarizeToolResult
66
+ },
67
+ groundingSources: input.groundingSources
68
+ });
69
+ return { ...result, offeredTools: tools.map(tool => tool.name) };
70
+ }
71
+ //# sourceMappingURL=registry-assistant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry-assistant.js","sourceRoot":"","sources":["../../../server/service/assistant/registry-assistant.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A conversation for a product that has no editor of its own.\n *\n * ── What this is for ───────────────────────────────────────────────────────────\n * `board-ai` assembles its conversation from two sources: its own board editing tools, and\n * whatever other packages registered in the tool registry. The second half is the same work for\n * every product — read the specs, build the definitions, dispatch by name — but it only existed\n * inside the board assistant, so a product without a board had nowhere to ask a question from.\n * operato-figure hit exactly that: the dock was mounted, the tools were registered, and the only\n * mutation that could run them lived in a package figure does not load.\n *\n * ── Read and external 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 * So this runner offers, and dispatches, `read` and `external` only. A host that owns a document\n * keeps assembling its own conversation, as board-ai does.\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 { getCategoryGuidance, getToolSpecsFor, type ToolCategoryFilter, type ToolSpec } from '../tool-registry/index.js'\nimport { assistantChat, type AssistantChatResult } from './assistant-chat.js'\nimport { summarizeToolResult } from './summarize.js'\nimport type { GroundingSources } from '../utils/index.js'\n\n/** What a registry tool is handed when it runs. `state` is the caller's koa state. */\nexport interface RegistryToolCallContext {\n state: any\n /** What this surface is looking at — the host passes it through; tools read it as `host`. */\n host?: any\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. Omit to allow every registered category.\n *\n * An empty array is not the same as omitting: it allows nothing, and the conversation runs with\n * no tools at all. That is a legitimate surface (plain chat), so it is not corrected here.\n */\n toolCategories?: string[]\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 /** 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/** `read` and `external` are the two kinds that answer a question without changing a document. */\nfunction answeringSpecs(filter: ToolCategoryFilter): ToolSpec[] {\n return getToolSpecsFor(filter).filter(spec => spec.kind === 'read' || spec.kind === 'external')\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 const filter: ToolCategoryFilter = toolCategories\n const specs = answeringSpecs(filter)\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 = getCategoryGuidance(filter)\n const prompt = guidance.length > 0 ? `${systemPrompt}\\n\\n${guidance.join('\\n\\n')}` : systemPrompt\n\n const toolContext: RegistryToolCallContext = { state, host: hostContext }\n\n const result = await assistantChat<RegistryToolCallContext, 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 })\n\n return { ...result, offeredTools: tools.map(tool => tool.name) }\n}\n"]}
@@ -0,0 +1,31 @@
1
+ import { type ToolCategoryFilter, type ToolSpec } from '../tool-registry/index.js';
2
+ /** 이름 하나에 대한 판정. 정의 조립과 dispatch 가 **같은 것**을 쓴다. */
3
+ export interface ToolGate {
4
+ /** 이 대화에 허용된 카테고리 목록. 없음 = 전부. */
5
+ filter: ToolCategoryFilter;
6
+ isRead(name: string): boolean;
7
+ isWrite(name: string): boolean;
8
+ isAction(name: string): boolean;
9
+ spec(name: string): ToolSpec | undefined;
10
+ }
11
+ /** 호스트가 자기 도구를 더할 때 주는 것. 준 것만 더해지고, 안 준 축은 레지스트리 판정 그대로다. */
12
+ export interface ToolGateExtension {
13
+ isRead?(name: string): boolean;
14
+ isWrite?(name: string): boolean;
15
+ isAction?(name: string): boolean;
16
+ spec?(name: string): ToolSpec | undefined;
17
+ }
18
+ /**
19
+ * 레지스트리만 보고 답하는 게이트.
20
+ *
21
+ * `external` 은 읽기로 본다 — 등록한 쪽이 「밖에서 값을 가져온다」고 말한 것이고, 그것은 쓰지 않는다.
22
+ */
23
+ export declare function registryToolGate(filter?: ToolCategoryFilter): ToolGate;
24
+ /**
25
+ * 호스트의 판정을 **더한다.** 겹치면 어느 쪽이든 참이면 참이다 — 게이트는 「이 이름을 다룰 수 있나」를
26
+ * 묻고, 두 출처 중 하나가 안다고 하면 다룰 수 있다.
27
+ *
28
+ * 한쪽으로 덮지 않는 이유: 덮으면 호스트가 자기 이름집합에 없는 등록 도구를 **조용히 잠근다.** 그러면
29
+ * 대화면이 카테고리를 허용했는데 호출이 미등록으로 회신되고, 원인이 두 자리에 나뉘어 있어 못 찾는다.
30
+ */
31
+ export declare function extendToolGate(base: ToolGate, extension: ToolGateExtension): ToolGate;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registryToolGate = registryToolGate;
4
+ exports.extendToolGate = extendToolGate;
5
+ /*
6
+ * Which tools this conversation may call — the registry half.
7
+ *
8
+ * ── Why a gate at all ───────────────────────────────────────────────────────────
9
+ * One product grows several conversation surfaces (a board authoring dock, an operations dock, a
10
+ * header assistant). Board edits can be reverted; an operations command reaches a real factory.
11
+ * If both sets hang in one conversation, the boundary is a label on the screen.
12
+ *
13
+ * **Hiding the definitions is not enough.** A model can guess a name, and an older conversation
14
+ * can be replayed with tool calls still in it. So the same gate has to decide *both* what is
15
+ * offered and what may be dispatched. A name that fails the gate is reported back to the model as
16
+ * unregistered — never a silent no-op, which the model would read as success.
17
+ *
18
+ * ── Why the registry half is here and the rest is not (2026-09-10) ──────────────
19
+ * This came out of `board-ai`, where the gate answered from two sources at once: the tool registry
20
+ * *and* board's own core tool-name sets. The first is neutral — any host asking "what kind of tool
21
+ * is this name, and may this conversation see it" wants the same answer. The second is board's.
22
+ *
23
+ * So the registry half lives here and a host **extends** it with whatever it owns
24
+ * (`extendToolGate`). Splitting it this way is what lets a product with no board of its own get
25
+ * the same dispatch discipline, instead of each product writing its own gate and diverging — which
26
+ * is what happened with the grounding guard: the twin's header route had it and the dock route did
27
+ * not, so the same question was verified on one side and not the other.
28
+ */
29
+ const index_js_1 = require("../tool-registry/index.js");
30
+ /**
31
+ * 레지스트리만 보고 답하는 게이트.
32
+ *
33
+ * `external` 은 읽기로 본다 — 등록한 쪽이 「밖에서 값을 가져온다」고 말한 것이고, 그것은 쓰지 않는다.
34
+ */
35
+ function registryToolGate(filter) {
36
+ const kind = (name) => (0, index_js_1.getToolKind)(name, filter);
37
+ return {
38
+ filter,
39
+ isRead: name => kind(name) === 'read' || kind(name) === 'external',
40
+ isWrite: name => kind(name) === 'write',
41
+ isAction: name => kind(name) === 'action',
42
+ spec: name => (0, index_js_1.findToolSpec)(name, filter)
43
+ };
44
+ }
45
+ /**
46
+ * 호스트의 판정을 **더한다.** 겹치면 어느 쪽이든 참이면 참이다 — 게이트는 「이 이름을 다룰 수 있나」를
47
+ * 묻고, 두 출처 중 하나가 안다고 하면 다룰 수 있다.
48
+ *
49
+ * 한쪽으로 덮지 않는 이유: 덮으면 호스트가 자기 이름집합에 없는 등록 도구를 **조용히 잠근다.** 그러면
50
+ * 대화면이 카테고리를 허용했는데 호출이 미등록으로 회신되고, 원인이 두 자리에 나뉘어 있어 못 찾는다.
51
+ */
52
+ function extendToolGate(base, extension) {
53
+ return {
54
+ filter: base.filter,
55
+ isRead: name => base.isRead(name) || !!extension.isRead?.(name),
56
+ isWrite: name => base.isWrite(name) || !!extension.isWrite?.(name),
57
+ isAction: name => base.isAction(name) || !!extension.isAction?.(name),
58
+ spec: name => base.spec(name) ?? extension.spec?.(name)
59
+ };
60
+ }
61
+ //# sourceMappingURL=registry-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry-gate.js","sourceRoot":"","sources":["../../../server/service/assistant/registry-gate.ts"],"names":[],"mappings":";;AAiDA,4CASC;AASD,wCAQC;AA3ED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wDAA6G;AAoB7G;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,MAA2B;IAC1D,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,IAAA,sBAAW,EAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACxD,OAAO;QACL,MAAM;QACN,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,UAAU;QAClE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,OAAO;QACvC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,QAAQ;QACzC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAA,uBAAY,EAAC,IAAI,EAAE,MAAM,CAAC;KACzC,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,cAAc,CAAC,IAAc,EAAE,SAA4B;IACzE,OAAO;QACL,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC;QAC/D,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC;QAClE,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC;QACrE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC;KACxD,CAAA;AACH,CAAC","sourcesContent":["/*\n * Which tools this conversation may call — the registry half.\n *\n * ── Why a gate at all ───────────────────────────────────────────────────────────\n * One product grows several conversation surfaces (a board authoring dock, an operations dock, a\n * header assistant). Board edits can be reverted; an operations command reaches a real factory.\n * If both sets hang in one conversation, the boundary is a label on the screen.\n *\n * **Hiding the definitions is not enough.** A model can guess a name, and an older conversation\n * can be replayed with tool calls still in it. So the same gate has to decide *both* what is\n * offered and what may be dispatched. A name that fails the gate is reported back to the model as\n * unregistered — never a silent no-op, which the model would read as success.\n *\n * ── Why the registry half is here and the rest is not (2026-09-10) ──────────────\n * This came out of `board-ai`, where the gate answered from two sources at once: the tool registry\n * *and* board's own core tool-name sets. The first is neutral — any host asking \"what kind of tool\n * is this name, and may this conversation see it\" wants the same answer. The second is board's.\n *\n * So the registry half lives here and a host **extends** it with whatever it owns\n * (`extendToolGate`). Splitting it this way is what lets a product with no board of its own get\n * the same dispatch discipline, instead of each product writing its own gate and diverging — which\n * is what happened with the grounding guard: the twin's header route had it and the dock route did\n * not, so the same question was verified on one side and not the other.\n */\nimport { findToolSpec, getToolKind, type ToolCategoryFilter, type ToolSpec } from '../tool-registry/index.js'\n\n/** 이름 하나에 대한 판정. 정의 조립과 dispatch 가 **같은 것**을 쓴다. */\nexport interface ToolGate {\n /** 이 대화에 허용된 카테고리 목록. 없음 = 전부. */\n filter: ToolCategoryFilter\n isRead(name: string): boolean\n isWrite(name: string): boolean\n isAction(name: string): boolean\n spec(name: string): ToolSpec | undefined\n}\n\n/** 호스트가 자기 도구를 더할 때 주는 것. 준 것만 더해지고, 안 준 축은 레지스트리 판정 그대로다. */\nexport interface ToolGateExtension {\n isRead?(name: string): boolean\n isWrite?(name: string): boolean\n isAction?(name: string): boolean\n spec?(name: string): ToolSpec | undefined\n}\n\n/**\n * 레지스트리만 보고 답하는 게이트.\n *\n * `external` 은 읽기로 본다 — 등록한 쪽이 「밖에서 값을 가져온다」고 말한 것이고, 그것은 쓰지 않는다.\n */\nexport function registryToolGate(filter?: ToolCategoryFilter): ToolGate {\n const kind = (name: string) => getToolKind(name, filter)\n return {\n filter,\n isRead: name => kind(name) === 'read' || kind(name) === 'external',\n isWrite: name => kind(name) === 'write',\n isAction: name => kind(name) === 'action',\n spec: name => findToolSpec(name, filter)\n }\n}\n\n/**\n * 호스트의 판정을 **더한다.** 겹치면 어느 쪽이든 참이면 참이다 — 게이트는 「이 이름을 다룰 수 있나」를\n * 묻고, 두 출처 중 하나가 안다고 하면 다룰 수 있다.\n *\n * 한쪽으로 덮지 않는 이유: 덮으면 호스트가 자기 이름집합에 없는 등록 도구를 **조용히 잠근다.** 그러면\n * 대화면이 카테고리를 허용했는데 호출이 미등록으로 회신되고, 원인이 두 자리에 나뉘어 있어 못 찾는다.\n */\nexport function extendToolGate(base: ToolGate, extension: ToolGateExtension): ToolGate {\n return {\n filter: base.filter,\n isRead: name => base.isRead(name) || !!extension.isRead?.(name),\n isWrite: name => base.isWrite(name) || !!extension.isWrite?.(name),\n isAction: name => base.isAction(name) || !!extension.isAction?.(name),\n spec: name => base.spec(name) ?? extension.spec?.(name)\n }\n}\n"]}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 도구 결과를 화면·영속에 쓸 크기로 줄인다.
3
+ *
4
+ * 줄인 자리마다 **줄였다는 표시**를 남긴다(`…[+N chars]` · `…[+N items]` · `[Array(N)]`).
5
+ * 표시가 없으면 읽는 사람이 그것을 원본으로 믿는다.
6
+ */
7
+ export declare function summarizeToolResult(value: any, depth?: number): any;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ /*
3
+ * Tool results, cut down to what a screen can hold.
4
+ *
5
+ * ── Why this is here and not in a product (2026-09-10) ───────────────────────────
6
+ * This came out of `board-ai`, and measuring the other consumer is what settled it: `twin-ai`
7
+ * passes `summarizeToolResult: v => v` — the identity function. So one product compressed its
8
+ * tool results before persisting them and the other did not, for no stated reason. That is the
9
+ * shape of a layer living in the wrong package: the second product does not decide differently,
10
+ * it just never gets the decision.
11
+ *
12
+ * ── ⚠ This is for the screen, never for a judgement ─────────────────────────────
13
+ * The output is depth- and length-limited, so it is **not evidence of what the model received**.
14
+ * A check that reads these summaries can pass while the model was handed `{}` — an un-awaited
15
+ * async builder did exactly that, and the screen still showed a successful call.
16
+ *
17
+ * What the model actually received is `AgenticLoopResult.groundedTexts`. Grounding already uses
18
+ * it, and anything that judges correctness must use it too.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.summarizeToolResult = summarizeToolResult;
22
+ /** 문자열은 이 길이에서 자른다. 자른 사실과 잘린 양을 남긴다 — 조용히 짧아지지 않는다. */
23
+ const MAX_STRING = 200;
24
+ /** 배열은 이 개수까지 보이고 나머지는 수로 말한다. */
25
+ const MAX_ITEMS = 5;
26
+ /** 배열 안의 배열은 이 깊이에서 모양만 남긴다. */
27
+ const MAX_ARRAY_DEPTH = 2;
28
+ /** 객체는 이 깊이에서 키 이름만 남긴다. */
29
+ const MAX_OBJECT_DEPTH = 3;
30
+ /** 깊이 한계에서 남길 키 개수. */
31
+ const MAX_KEYS = 6;
32
+ /**
33
+ * 도구 결과를 화면·영속에 쓸 크기로 줄인다.
34
+ *
35
+ * 줄인 자리마다 **줄였다는 표시**를 남긴다(`…[+N chars]` · `…[+N items]` · `[Array(N)]`).
36
+ * 표시가 없으면 읽는 사람이 그것을 원본으로 믿는다.
37
+ */
38
+ function summarizeToolResult(value, depth = 0) {
39
+ if (value === null || value === undefined)
40
+ return value;
41
+ if (typeof value === 'string') {
42
+ return value.length > MAX_STRING ? `${value.slice(0, MAX_STRING)}…[+${value.length - MAX_STRING} chars]` : value;
43
+ }
44
+ if (typeof value === 'number' || typeof value === 'boolean')
45
+ return value;
46
+ if (Array.isArray(value)) {
47
+ if (depth >= MAX_ARRAY_DEPTH)
48
+ return `[Array(${value.length})]`;
49
+ const out = value.slice(0, MAX_ITEMS).map(v => summarizeToolResult(v, depth + 1));
50
+ if (value.length > MAX_ITEMS)
51
+ out.push(`…[+${value.length - MAX_ITEMS} items]`);
52
+ return out;
53
+ }
54
+ if (typeof value === 'object') {
55
+ if (depth >= MAX_OBJECT_DEPTH) {
56
+ const keys = Object.keys(value).slice(0, MAX_KEYS).join(',');
57
+ return `{${keys}${Object.keys(value).length > MAX_KEYS ? ',…' : ''}}`;
58
+ }
59
+ const out = {};
60
+ for (const key of Object.keys(value)) {
61
+ out[key] = summarizeToolResult(value[key], depth + 1);
62
+ }
63
+ return out;
64
+ }
65
+ return undefined;
66
+ }
67
+ //# sourceMappingURL=summarize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summarize.js","sourceRoot":"","sources":["../../../server/service/assistant/summarize.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;AAmBH,kDAwBC;AAzCD,wDAAwD;AACxD,MAAM,UAAU,GAAG,GAAG,CAAA;AACtB,kCAAkC;AAClC,MAAM,SAAS,GAAG,CAAC,CAAA;AACnB,gCAAgC;AAChC,MAAM,eAAe,GAAG,CAAC,CAAA;AACzB,4BAA4B;AAC5B,MAAM,gBAAgB,GAAG,CAAC,CAAA;AAC1B,uBAAuB;AACvB,MAAM,QAAQ,GAAG,CAAC,CAAA;AAElB;;;;;GAKG;AACH,SAAgB,mBAAmB,CAAC,KAAU,EAAE,KAAK,GAAG,CAAC;IACvD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IACvD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,MAAM,KAAK,CAAC,MAAM,GAAG,UAAU,SAAS,CAAC,CAAC,CAAC,KAAK,CAAA;IAClH,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IACzE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,IAAI,KAAK,IAAI,eAAe;YAAE,OAAO,UAAU,KAAK,CAAC,MAAM,IAAI,CAAA;QAC/D,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,mBAAmB,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAA;QACjF,IAAI,KAAK,CAAC,MAAM,GAAG,SAAS;YAAE,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,MAAM,GAAG,SAAS,SAAS,CAAC,CAAA;QAC/E,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,KAAK,IAAI,gBAAgB,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YAC5D,OAAO,IAAI,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAA;QACvE,CAAC;QACD,MAAM,GAAG,GAAQ,EAAE,CAAA;QACnB,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACrC,GAAG,CAAC,GAAG,CAAC,GAAG,mBAAmB,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAA;QACvD,CAAC;QACD,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC","sourcesContent":["/*\n * Tool results, cut down to what a screen can hold.\n *\n * ── Why this is here and not in a product (2026-09-10) ───────────────────────────\n * This came out of `board-ai`, and measuring the other consumer is what settled it: `twin-ai`\n * passes `summarizeToolResult: v => v` — the identity function. So one product compressed its\n * tool results before persisting them and the other did not, for no stated reason. That is the\n * shape of a layer living in the wrong package: the second product does not decide differently,\n * it just never gets the decision.\n *\n * ── ⚠ This is for the screen, never for a judgement ─────────────────────────────\n * The output is depth- and length-limited, so it is **not evidence of what the model received**.\n * A check that reads these summaries can pass while the model was handed `{}` — an un-awaited\n * async builder did exactly that, and the screen still showed a successful call.\n *\n * What the model actually received is `AgenticLoopResult.groundedTexts`. Grounding already uses\n * it, and anything that judges correctness must use it too.\n */\n\n/** 문자열은 이 길이에서 자른다. 자른 사실과 잘린 양을 남긴다 — 조용히 짧아지지 않는다. */\nconst MAX_STRING = 200\n/** 배열은 이 개수까지 보이고 나머지는 수로 말한다. */\nconst MAX_ITEMS = 5\n/** 배열 안의 배열은 이 깊이에서 모양만 남긴다. */\nconst MAX_ARRAY_DEPTH = 2\n/** 객체는 이 깊이에서 키 이름만 남긴다. */\nconst MAX_OBJECT_DEPTH = 3\n/** 깊이 한계에서 남길 키 개수. */\nconst MAX_KEYS = 6\n\n/**\n * 도구 결과를 화면·영속에 쓸 크기로 줄인다.\n *\n * 줄인 자리마다 **줄였다는 표시**를 남긴다(`…[+N chars]` · `…[+N items]` · `[Array(N)]`).\n * 표시가 없으면 읽는 사람이 그것을 원본으로 믿는다.\n */\nexport function summarizeToolResult(value: any, depth = 0): any {\n if (value === null || value === undefined) return value\n if (typeof value === 'string') {\n return value.length > MAX_STRING ? `${value.slice(0, MAX_STRING)}…[+${value.length - MAX_STRING} chars]` : value\n }\n if (typeof value === 'number' || typeof value === 'boolean') return value\n if (Array.isArray(value)) {\n if (depth >= MAX_ARRAY_DEPTH) return `[Array(${value.length})]`\n const out = value.slice(0, MAX_ITEMS).map(v => summarizeToolResult(v, depth + 1))\n if (value.length > MAX_ITEMS) out.push(`…[+${value.length - MAX_ITEMS} items]`)\n return out\n }\n if (typeof value === 'object') {\n if (depth >= MAX_OBJECT_DEPTH) {\n const keys = Object.keys(value).slice(0, MAX_KEYS).join(',')\n return `{${keys}${Object.keys(value).length > MAX_KEYS ? ',…' : ''}}`\n }\n const out: any = {}\n for (const key of Object.keys(value)) {\n out[key] = summarizeToolResult(value[key], depth + 1)\n }\n return out\n }\n return undefined\n}\n"]}
@@ -4,3 +4,4 @@ export * from './providers/index.js';
4
4
  export * from './utils/index.js';
5
5
  export * from './tool-registry/index.js';
6
6
  export * from './agentic-loop/index.js';
7
+ export * from './assistant/index.js';
@@ -7,4 +7,5 @@ tslib_1.__exportStar(require("./providers/index.js"), exports);
7
7
  tslib_1.__exportStar(require("./utils/index.js"), exports);
8
8
  tslib_1.__exportStar(require("./tool-registry/index.js"), exports);
9
9
  tslib_1.__exportStar(require("./agentic-loop/index.js"), exports);
10
+ tslib_1.__exportStar(require("./assistant/index.js"), exports);
10
11
  //# 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","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'\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","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'\n"]}
@@ -15,6 +15,25 @@ const categories = new Map();
15
15
  * 카테고리 등록. tool name 충돌 시 throw, 이름 중복 등록 시 덮어쓰기 + warn.
16
16
  */
17
17
  function registerToolCategory(category) {
18
+ /*
19
+ * 지침 없는 카테고리는 **등록하지 않는다.**
20
+ *
21
+ * 도구 목록만 보낸 모델이 「불러야 할 때 부른다」를 맞히지 못한 사고가 있었다 — 조치 요청에
22
+ * `proposeAction` 을 부르지 않고 「제안해 두었습니다」라고 답해, 사용자에게 없는 버튼을 누르라고
23
+ * 안내했다. 화면에는 아무 일도 없었고 오류도 나지 않았다(§`ToolCategory.guidance`).
24
+ *
25
+ * 그 뒤로 규율을 적는 자리를 만들었지만 **선택이었다.** 선택인 규율은 그 사고를 아는 사람만
26
+ * 채운다. 여기서 거절하는 이유가 그것이다 — 빠진 것을 부팅에서 만든 사람이 보고, 모델이 헛말을
27
+ * 한 뒤에 사용자가 보지 않는다.
28
+ *
29
+ * 빈 문자열·공백도 거절한다. 통과시키면 이 검사가 형식이 되고, 형식이 된 검사는 없는 것과 같다.
30
+ */
31
+ if (!String(category.guidance ?? '').trim()) {
32
+ throw new Error(`[ai-client-base/tool-registry] category '${category.name}' has no guidance. ` +
33
+ 'Tool descriptions say what a tool does; guidance says when the model must call one. ' +
34
+ 'Without it a model answers as though it called a tool it did not, and nothing errors. ' +
35
+ 'Write the rules for these tools in `guidance` — the side that registers them owns them.');
36
+ }
18
37
  if (categories.has(category.name)) {
19
38
  console.warn(`[board-ai/tool-registry] category '${category.name}' already registered — overwriting. ` +
20
39
  `(dev hot-reload 시 정상이지만 운영 환경에서 발생하면 buildup bug)`);