@things-factory/ai-client-base 10.1.203 → 10.1.205

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 (34) hide show
  1. package/dist-server/service/assistant/registry-assistant.d.ts +15 -1
  2. package/dist-server/service/assistant/registry-assistant.js +18 -5
  3. package/dist-server/service/assistant/registry-assistant.js.map +1 -1
  4. package/dist-server/service/providers/anthropic.js +3 -2
  5. package/dist-server/service/providers/anthropic.js.map +1 -1
  6. package/dist-server/service/providers/gemini.js +4 -3
  7. package/dist-server/service/providers/gemini.js.map +1 -1
  8. package/dist-server/service/providers/openai.js +4 -3
  9. package/dist-server/service/providers/openai.js.map +1 -1
  10. package/dist-server/service/providers/request-options.d.ts +4 -0
  11. package/dist-server/service/providers/request-options.js +7 -0
  12. package/dist-server/service/providers/request-options.js.map +1 -0
  13. package/dist-server/service/testing/tool-call-context.d.ts +3 -1
  14. package/dist-server/service/testing/tool-call-context.js +2 -0
  15. package/dist-server/service/testing/tool-call-context.js.map +1 -1
  16. package/dist-server/service/tool-registry/index.d.ts +1 -1
  17. package/dist-server/service/tool-registry/index.js.map +1 -1
  18. package/dist-server/service/tool-registry/types.d.ts +16 -0
  19. package/dist-server/service/tool-registry/types.js.map +1 -1
  20. package/dist-server/service/types.d.ts +5 -0
  21. package/dist-server/service/types.js.map +1 -1
  22. package/dist-server/tsconfig.tsbuildinfo +1 -1
  23. package/package.json +5 -5
  24. package/server/service/assistant/registry-assistant.test.ts +80 -0
  25. package/server/service/assistant/registry-assistant.ts +33 -7
  26. package/server/service/providers/anthropic.ts +3 -2
  27. package/server/service/providers/gemini.ts +4 -3
  28. package/server/service/providers/openai.ts +4 -3
  29. package/server/service/providers/request-options.ts +12 -0
  30. package/server/service/providers/request-signal.test.ts +77 -0
  31. package/server/service/testing/tool-call-context.ts +4 -1
  32. package/server/service/tool-registry/index.ts +1 -0
  33. package/server/service/tool-registry/types.ts +17 -0
  34. package/server/service/types.ts +5 -0
@@ -1,4 +1,4 @@
1
- import type { ToolAttachment, ToolCallContext, ToolCallMode, ToolTranslate } from '../tool-registry/types.js';
1
+ import type { ToolAttachment, ToolCallContext, ToolCallMode, ToolTranslate, UnattendedRunRef } from '../tool-registry/types.js';
2
2
  export interface ToolCallContextFixtureInput {
3
3
  domainId?: string;
4
4
  userId?: string;
@@ -10,5 +10,7 @@ export interface ToolCallContextFixtureInput {
10
10
  t?: ToolTranslate;
11
11
  /** The checked files of the turn. Absent: none. */
12
12
  attachments?: ToolAttachment[];
13
+ /** The unattended run the call belongs to. Absent: a turn of a conversation. */
14
+ run?: UnattendedRunRef;
13
15
  }
14
16
  export declare function toolCallContextFixture(input?: ToolCallContextFixtureInput): ToolCallContext;
@@ -11,6 +11,8 @@ function toolCallContextFixture(input = {}) {
11
11
  };
12
12
  if (input.mode)
13
13
  context.mode = input.mode;
14
+ if (input.run)
15
+ context.run = input.run;
14
16
  return context;
15
17
  }
16
18
  //# sourceMappingURL=tool-call-context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tool-call-context.js","sourceRoot":"","sources":["../../../server/service/testing/tool-call-context.ts"],"names":[],"mappings":";;AAyBA,wDASC;AAxBD,gEAAqE;AAerE,SAAgB,sBAAsB,CAAC,QAAqC,EAAE;IAC5E,MAAM,OAAO,GAAoB;QAC/B,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,QAAQ,IAAI,aAAa,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,MAAM,IAAI,WAAW,EAAE,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE;QAC7H,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE;QACtB,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,oCAAqB;QACnC,WAAW,EAAE,KAAK,CAAC,WAAW,IAAI,EAAE;KACrC,CAAA;IACD,IAAI,KAAK,CAAC,IAAI;QAAE,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAA;IACzC,OAAO,OAAO,CAAA;AAChB,CAAC","sourcesContent":["/*\n * The one way a test builds what a registered tool is handed (A3, 2026-09-17).\n *\n * The twin's space tools read `ctx.state.host` while the door passed `ctx.host`, and their tests\n * stayed green because each test wrote its own context in the shape the tool expected. A fixture\n * owned by each test agrees with its tool by construction. This one is owned by the package that\n * defines the neutral door, and that door's tests check it hands tools exactly this shape — so a tool\n * test built on it fails when the tool reads a field no door fills.\n */\nimport type { ToolAttachment, ToolCallContext, ToolCallMode, ToolTranslate } from '../tool-registry/types.js'\nimport { defaultValueTranslate } from '../tool-registry/translate.js'\n\nexport interface ToolCallContextFixtureInput {\n domainId?: string\n userId?: string\n /** Extra request-state fields a tool reads (e.g. `tx`). Merged after domain and user. */\n state?: Record<string, any>\n host?: Record<string, any>\n mode?: ToolCallMode\n /** The request translator. Absent: the one the door hands when it was given none. */\n t?: ToolTranslate\n /** The checked files of the turn. Absent: none. */\n attachments?: ToolAttachment[]\n}\n\nexport function toolCallContextFixture(input: ToolCallContextFixtureInput = {}): ToolCallContext {\n const context: ToolCallContext = {\n state: { domain: { id: input.domainId ?? 'domain-test' }, user: { id: input.userId ?? 'user-test' }, ...(input.state ?? {}) },\n host: input.host ?? {},\n t: input.t ?? defaultValueTranslate,\n attachments: input.attachments ?? []\n }\n if (input.mode) context.mode = input.mode\n return context\n}\n"]}
1
+ {"version":3,"file":"tool-call-context.js","sourceRoot":"","sources":["../../../server/service/testing/tool-call-context.ts"],"names":[],"mappings":";;AA2BA,wDAUC;AA3BD,gEAAqE;AAiBrE,SAAgB,sBAAsB,CAAC,QAAqC,EAAE;IAC5E,MAAM,OAAO,GAAoB;QAC/B,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,QAAQ,IAAI,aAAa,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,MAAM,IAAI,WAAW,EAAE,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE;QAC7H,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE;QACtB,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,oCAAqB;QACnC,WAAW,EAAE,KAAK,CAAC,WAAW,IAAI,EAAE;KACrC,CAAA;IACD,IAAI,KAAK,CAAC,IAAI;QAAE,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAA;IACzC,IAAI,KAAK,CAAC,GAAG;QAAE,OAAO,CAAC,GAAG,GAAG,KAAK,CAAC,GAAG,CAAA;IACtC,OAAO,OAAO,CAAA;AAChB,CAAC","sourcesContent":["/*\n * The one way a test builds what a registered tool is handed (A3, 2026-09-17).\n *\n * The twin's space tools read `ctx.state.host` while the door passed `ctx.host`, and their tests\n * stayed green because each test wrote its own context in the shape the tool expected. A fixture\n * owned by each test agrees with its tool by construction. This one is owned by the package that\n * defines the neutral door, and that door's tests check it hands tools exactly this shape — so a tool\n * test built on it fails when the tool reads a field no door fills.\n */\nimport type { ToolAttachment, ToolCallContext, ToolCallMode, ToolTranslate, UnattendedRunRef } from '../tool-registry/types.js'\nimport { defaultValueTranslate } from '../tool-registry/translate.js'\n\nexport interface ToolCallContextFixtureInput {\n domainId?: string\n userId?: string\n /** Extra request-state fields a tool reads (e.g. `tx`). Merged after domain and user. */\n state?: Record<string, any>\n host?: Record<string, any>\n mode?: ToolCallMode\n /** The request translator. Absent: the one the door hands when it was given none. */\n t?: ToolTranslate\n /** The checked files of the turn. Absent: none. */\n attachments?: ToolAttachment[]\n /** The unattended run the call belongs to. Absent: a turn of a conversation. */\n run?: UnattendedRunRef\n}\n\nexport function toolCallContextFixture(input: ToolCallContextFixtureInput = {}): ToolCallContext {\n const context: ToolCallContext = {\n state: { domain: { id: input.domainId ?? 'domain-test' }, user: { id: input.userId ?? 'user-test' }, ...(input.state ?? {}) },\n host: input.host ?? {},\n t: input.t ?? defaultValueTranslate,\n attachments: input.attachments ?? []\n }\n if (input.mode) context.mode = input.mode\n if (input.run) context.run = input.run\n return context\n}\n"]}
@@ -8,4 +8,4 @@ export { defaultValueTranslate } from './translate';
8
8
  export { registerToolCallRecorder, clearToolCallRecorders, recordToolCall, digestOf } from './call-recorder';
9
9
  export type { ToolCallRecord, ToolCallRecorder, ToolCallRecorderOptions, ToolCallRun } from './call-recorder';
10
10
  export type { ToolCategoryFilter, ToolRegistryWarning } from './registry';
11
- export type { ToolSpec, ToolSpecBase, ReadToolSpec, WriteToolSpec, ActionToolSpec, ExternalToolSpec, ToolCategory, ReadToolBuilder, WriteToolBuilder, ActionToolBuilder, ExternalToolBuilder, ReadToolContext, WriteToolContext, ExternalToolContext, ToolCallContext, ToolCallMode, ToolTranslate, ToolAttachment, ToolImageAttachment, ToolTextAttachment } from './types';
11
+ export type { ToolSpec, ToolSpecBase, ReadToolSpec, WriteToolSpec, ActionToolSpec, ExternalToolSpec, ToolCategory, ReadToolBuilder, WriteToolBuilder, ActionToolBuilder, ExternalToolBuilder, ReadToolContext, WriteToolContext, ExternalToolContext, ToolCallContext, ToolCallMode, UnattendedRunRef, ToolTranslate, ToolAttachment, ToolImageAttachment, ToolTextAttachment } from './types';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/tool-registry/index.ts"],"names":[],"mappings":";;;AAAA;;;;GAIG;AACH,uCAYmB;AAXjB,gHAAA,oBAAoB,OAAA;AACpB,6GAAA,iBAAiB,OAAA;AACjB,2GAAA,eAAe,OAAA;AACf,2GAAA,eAAe,OAAA;AACf,+GAAA,mBAAmB,OAAA;AACnB,wGAAA,YAAY,OAAA;AACZ,uGAAA,WAAW,OAAA;AACX,mGAAA,OAAO,OAAA;AACP,4GAAA,gBAAgB,OAAA;AAChB,6GAAA,iBAAiB,OAAA;AACjB,mHAAA,uBAAuB,OAAA;AAGzB,yCAAmD;AAA1C,kHAAA,qBAAqB,OAAA;AAE9B,iDAA4G;AAAnG,yHAAA,wBAAwB,OAAA;AAAE,uHAAA,sBAAsB,OAAA;AAAE,+GAAA,cAAc,OAAA;AAAE,yGAAA,QAAQ,OAAA","sourcesContent":["/**\n * Tool Registry — board-ai 의 외부 도메인 LLM tool 노출 plugin point.\n *\n * 자세한 설계는 ./types 와 ./registry 의 헤더 주석 참조.\n */\nexport {\n registerToolCategory,\n getToolCategories,\n getAllToolSpecs,\n getToolSpecsFor,\n getCategoryGuidance,\n findToolSpec,\n getToolKind,\n hasTool,\n getCategorySpecs,\n clearToolRegistry,\n getToolRegistryWarnings\n} from './registry'\n\nexport { defaultValueTranslate } from './translate'\n\nexport { registerToolCallRecorder, clearToolCallRecorders, recordToolCall, digestOf } from './call-recorder'\nexport type { ToolCallRecord, ToolCallRecorder, ToolCallRecorderOptions, ToolCallRun } from './call-recorder'\n\n/* 대화별 노출 제한 타입은 registry 소유(정의가 거기 있다). */\nexport type { ToolCategoryFilter, ToolRegistryWarning } from './registry'\n\nexport type {\n ToolSpec,\n ToolSpecBase,\n ReadToolSpec,\n WriteToolSpec,\n ActionToolSpec,\n ExternalToolSpec,\n ToolCategory,\n ReadToolBuilder,\n WriteToolBuilder,\n ActionToolBuilder,\n ExternalToolBuilder,\n ReadToolContext,\n WriteToolContext,\n ExternalToolContext,\n ToolCallContext,\n ToolCallMode,\n ToolTranslate,\n ToolAttachment,\n ToolImageAttachment,\n ToolTextAttachment\n} from './types'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/tool-registry/index.ts"],"names":[],"mappings":";;;AAAA;;;;GAIG;AACH,uCAYmB;AAXjB,gHAAA,oBAAoB,OAAA;AACpB,6GAAA,iBAAiB,OAAA;AACjB,2GAAA,eAAe,OAAA;AACf,2GAAA,eAAe,OAAA;AACf,+GAAA,mBAAmB,OAAA;AACnB,wGAAA,YAAY,OAAA;AACZ,uGAAA,WAAW,OAAA;AACX,mGAAA,OAAO,OAAA;AACP,4GAAA,gBAAgB,OAAA;AAChB,6GAAA,iBAAiB,OAAA;AACjB,mHAAA,uBAAuB,OAAA;AAGzB,yCAAmD;AAA1C,kHAAA,qBAAqB,OAAA;AAE9B,iDAA4G;AAAnG,yHAAA,wBAAwB,OAAA;AAAE,uHAAA,sBAAsB,OAAA;AAAE,+GAAA,cAAc,OAAA;AAAE,yGAAA,QAAQ,OAAA","sourcesContent":["/**\n * Tool Registry — board-ai 의 외부 도메인 LLM tool 노출 plugin point.\n *\n * 자세한 설계는 ./types 와 ./registry 의 헤더 주석 참조.\n */\nexport {\n registerToolCategory,\n getToolCategories,\n getAllToolSpecs,\n getToolSpecsFor,\n getCategoryGuidance,\n findToolSpec,\n getToolKind,\n hasTool,\n getCategorySpecs,\n clearToolRegistry,\n getToolRegistryWarnings\n} from './registry'\n\nexport { defaultValueTranslate } from './translate'\n\nexport { registerToolCallRecorder, clearToolCallRecorders, recordToolCall, digestOf } from './call-recorder'\nexport type { ToolCallRecord, ToolCallRecorder, ToolCallRecorderOptions, ToolCallRun } from './call-recorder'\n\n/* 대화별 노출 제한 타입은 registry 소유(정의가 거기 있다). */\nexport type { ToolCategoryFilter, ToolRegistryWarning } from './registry'\n\nexport type {\n ToolSpec,\n ToolSpecBase,\n ReadToolSpec,\n WriteToolSpec,\n ActionToolSpec,\n ExternalToolSpec,\n ToolCategory,\n ReadToolBuilder,\n WriteToolBuilder,\n ActionToolBuilder,\n ExternalToolBuilder,\n ReadToolContext,\n WriteToolContext,\n ExternalToolContext,\n ToolCallContext,\n ToolCallMode,\n UnattendedRunRef,\n ToolTranslate,\n ToolAttachment,\n ToolImageAttachment,\n ToolTextAttachment\n} from './types'\n"]}
@@ -61,11 +61,27 @@ export interface ToolCallContext {
61
61
  * reads every entry of `kind: 'image'` — it does not pick the first one.
62
62
  */
63
63
  attachments: ToolAttachment[];
64
+ /**
65
+ * Set when no person is in this conversation: a scheduled run of the mode, as the caller named it
66
+ * (operato-application/design/ai/unattended-run.md §6). A recorder tells an unattended run's calls from a turn's by
67
+ * it; a turn has none. The server's value - never from `host`.
68
+ */
69
+ run?: UnattendedRunRef;
64
70
  }
65
71
  export interface ToolCallMode {
66
72
  kind: string;
67
73
  target?: string;
68
74
  }
75
+ /** Which unattended run a call belongs to - the stored run's id and the identity its caller gave it. */
76
+ export interface UnattendedRunRef {
77
+ id: string;
78
+ /** Who scheduled it - `<app>.<work>`, e.g. `office.strategy-review`. */
79
+ source: string;
80
+ /** What it is about in the source's terms - a policy id. */
81
+ subject: string;
82
+ /** The occurrence it is, ISO 8601. */
83
+ scheduledAt: string;
84
+ }
69
85
  /** `t(key, { defaultValue, ...params })` — the request's translator, as i18next's `t` is called. */
70
86
  export type ToolTranslate = (key: string, params?: Record<string, any>) => string;
71
87
  /** One file of this turn, as the server read it from storage. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../server/service/tool-registry/types.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,uEAAuE;AACvE,kEAAkE;AAClE,iFAAiF;AACjF,mEAAmE;AACnE,wDAAwD","sourcesContent":["/**\n * Tool Registry — board-ai 의 LLM tool 노출 plugin point.\n *\n * 동기:\n * 기존엔 board-ai 가 자기 도메인 (board edit) tool 들만 hardcoded 정의했고, styling\n * 영역만 STYLING_TOOLS registry 로 분리되어 있었다. 외부 도메인 패키지 (board-import,\n * fmsim 등) 가 자기 tool 을 LLM 에 노출하려면 board-ai 자체를 fork/수정해야 했다.\n *\n * 본 registry 는 외부 패키지가 server bootstrap 시 자기 tool category 를 등록하면\n * assistant.ts 의 buildBoardEditTools / dispatch / isWriteTool 등이 자동으로 통합\n * 하도록 한다. styling-tools 의 single-source-of-truth 패턴을 일반화한 형태.\n *\n * 설계 원칙:\n * - **카테고리별 builder 시그니처 다양성을 통합**: read 는 (args, ctx) → any,\n * write 는 (args, components) → SceneEditOp[], action 은 (args) → SceneActionOp.\n * 하나의 ToolSpec 에 모든 시그니처를 union 으로 담지 않고, 카테고리에 맞는\n * builder 만 채우면 되도록 카테고리별 sub-interface 분리.\n * - **kind 필드로 자가 분류**: registry 가 isReadTool / isWriteTool 자동 도출.\n * hardcoded set 동기화 누락 위험 제거.\n * - **점진 마이그레이션**: 기존 STYLING_TOOLS 가 본 registry 의 첫 사용자.\n * 점진적으로 다른 hardcoded tool 들 (addComponent / alignComponents 등) 도\n * 같은 registry 로 흡수 가능. 한 단계씩 byte-단위 동등성 단위 테스트 보장.\n */\n// 본 registry 는 LLM tool 노출 plugin point 에 한정되므로 도메인 type (SceneEditOp,\n// SceneActionOp 등) 은 알지 않는다. builder return 은 unknown 으로 두고, 카테고리\n// dispatcher (board-ai 의 toolCallToSceneEditOp / toolCallToSceneActionOp 등) 가 자기\n// 도메인 type 으로 좁힌다. 사용자가 ToolSpec<TWrite, TAction> generic 까지 정밀화하면\n// 좋겠지만, 11~50 개 spec 마다 type 인자 전파가 부담이라 unknown 으로 합의.\n\n// ── ToolCallContext — what the neutral conversation door hands a registered tool (A3) ──\n\n/**\n * What `registryAssistantChat` — the door apps use (`assistantChat`, ADR-0061 decision 0) — hands a\n * registered tool when it runs.\n *\n * `state` and `host` sit side by side. The twin's space tools read `ctx.state.host` while this door\n * passed `ctx.host`, so they never saw the space; the two are never merged into one object.\n *\n * The board product's door (board-ai) passes its own shape and is not part of this contract.\n */\nexport interface ToolCallContext {\n /** The request state the server built — domain, user, tx. The only basis for authorization. */\n state: any\n /**\n * The client's `AssistantMode.hostContext`, passed through as is. **Untrusted** — a tool that reads\n * an id from it checks that the id belongs to the caller's tenant. `{}` when none was sent.\n */\n host: Record<string, any>\n /** The conversation's mode. Filled once modes are registered on the server (A2); absent until then. */\n mode?: ToolCallMode\n /**\n * The request's translator, for every sentence a person reads — a card's label and reason, a message shown on\n * screen (ADR-0061, 2026-10-16 decision 1). The server builds it in the request's locale; it never comes from\n * `host`. Codes and values stay apart from it. Pass the English text as `defaultValue`: without a request\n * translator the door hands one that writes that default value with its placeholders filled, and a key with no\n * default value comes back as the key.\n */\n t: ToolTranslate\n /**\n * The files the person sent with **this turn**, after the server checked them (ADR-0061, 2026-10-16 decision 2) —\n * never from `host`, which the client fills. `[]` when none were sent.\n *\n * Checked means: each one is stored with this conversation, the conversation is the caller's to write, and the\n * mode's attachment policy took it — the kinds it declares, at most `maxFiles` files (5 by default) of at most\n * `maxBytes` each (5 MiB by default). **Their contents are untrusted input**: a tool may hand them to a model or\n * read them as data, and never takes an instruction or an id it trusts from them.\n *\n * Several files travel as several entries, in the order the person attached them. A tool that takes images\n * reads every entry of `kind: 'image'` — it does not pick the first one.\n */\n attachments: ToolAttachment[]\n}\n\nexport interface ToolCallMode {\n kind: string\n target?: string\n}\n\n/** `t(key, { defaultValue, ...params })` — the request's translator, as i18next's `t` is called. */\nexport type ToolTranslate = (key: string, params?: Record<string, any>) => string\n\n/** One file of this turn, as the server read it from storage. */\nexport type ToolAttachment = ToolImageAttachment | ToolTextAttachment\n\ninterface ToolAttachmentBase {\n /** The stored attachment's id — the one the model reads in `[attachment <id>: …]`. */\n id: string\n /** The file name the person gave. Untrusted text. */\n name: string\n /** The stored media type. */\n mediaType: string\n /** Bytes as stored. */\n size: number\n}\n\n/** An `image/*` file, its bytes as stored. */\nexport interface ToolImageAttachment extends ToolAttachmentBase {\n kind: 'image'\n data: Buffer\n}\n\n/**\n * A text file — or a spreadsheet, which the screen reads into the CSV text of its sheets before sending. `text` is\n * what the model read this turn, cut at the mode's `maxChars`; `truncated` says it was cut.\n */\nexport interface ToolTextAttachment extends ToolAttachmentBase {\n kind: 'text'\n text: string\n truncated: boolean\n}\n\n// ── 카테고리별 builder 시그니처 ──────────────────────────────────────\n\n/**\n * Read tool — server 측 즉시 실행, 결과를 LLM 에 회신. board model 변경 X.\n * 예: getSelection / findComponents / importSession (query)\n */\nexport type ReadToolBuilder = (\n args: any,\n context: ReadToolContext\n) => any | Promise<any>\n\nexport interface ReadToolContext {\n /** 현재 board model — 일부 read tool 이 사용 (getSelection 등). */\n board?: any\n /** 현재 선택된 refids. */\n selectedRefids?: number[]\n /** 호출자 도메인 / 사용자 / 트랜잭션. registry 외부에서 주입. */\n state?: any\n /** 이외 주입은 카테고리별로 자유. tool 등록자가 type 좁힐 수 있도록 unknown 으로 보존. */\n [k: string]: any\n}\n/**\n * Write tool — domain edit op 발생. host 가 patch 적용 후 영속화.\n * 예: board-ai 의 addComponent / setFill / modifyBoard.\n *\n * builder 의 return type 은 unknown 으로 둔다 — 도메인별 dispatcher (예: board-ai 의\n * toolCallToSceneEditOp) 가 자기 EditOp[] 로 cast. ops/errors wrapper 형태도 동일 의미.\n */\nexport type WriteToolBuilder = (\n args: any,\n context: WriteToolContext\n) => any[] | { ops: any[]; errors?: string[] } | null\n\nexport interface WriteToolContext {\n components?: ReadonlyArray<any>\n [k: string]: any\n}\n/**\n * Action tool — ephemeral scene 조작 (select / center / fit / sceneMode).\n * board model 변경 X, undo 영향 X. host 가 별도 channel 로 적용.\n *\n * return type 은 unknown — 도메인별 dispatcher (예: board-ai 의 toolCallToSceneActionOp)\n * 가 자기 SceneActionOp 으로 cast.\n */\nexport type ActionToolBuilder = (args: any) => any | null\n\n/**\n * External-call tool — 외부 service / mutation / RPC 호출. 직접 board model 영향 X\n * (별도 entity 변경은 가능). 결과는 LLM 에 회신.\n *\n * board-import 의 importBoardAsync / materializeImportSession 같이 board-ai 외부\n * 도메인을 호출하는 tool 의 카테고리.\n */\nexport type ExternalToolBuilder = (\n args: any,\n context: ExternalToolContext\n) => any | Promise<any>\n\nexport interface ExternalToolContext {\n state?: any\n [k: string]: any\n}\n// ── ToolSpec — 카테고리별 변형 ──────────────────────────────────────\n\n/**\n * 모든 ToolSpec 공통 메타데이터.\n */\nexport interface ToolSpecBase {\n /** AIToolCall.name 매칭. 전 카테고리 통틀어 unique. */\n name: string\n /** 기능 설명 (영문 위주). LLM 의 tool description 으로 사용. */\n description: string\n /**\n * 선택 — 정책 / 테마 가이드 단문. tool description 끝에 자동 부착.\n * styling-tools 의 themeNote 와 동일 의미. 기타 카테고리도 사용 가능.\n */\n themeNote?: string\n /** JSON Schema for parameters — LLM tool schema 에 그대로 노출. */\n schema: any\n /**\n * Which GraphQL doors this tool reaches — `['Query.twinNow']`,\n * `['Query.productionSummary', 'Query.pendingSummary']`.\n *\n * **A read tool names its doors too** (ADR-0071 decision 2, shaped by ADR-0072). Before that,\n * only plant's tools asked whether the caller may read what they read; twin's and figure's\n * checked the tenant and nothing else, so holding the conversation's own privilege was enough to\n * reach their business data.\n *\n * **Always a list, even for one** — one shape to read. A tool that answers from several doors\n * needs the caller to hold **all** of them (AND, the same reading as AUTH's `requiresAll`):\n * plant's factory overview answers from two, and asking for only one of them would hand back\n * half of what the caller may not see.\n *\n * **This list is the gate** (ADR-0071 decision 2, added to 2026-09-18). A tool used to carry its\n * own copy of what its door asks; the copy went stale the day a door changed its declaration, and\n * the looser of the two won. Now the gate reads the door's own declaration from AUTH's field\n * table (`privilegeOfField`) and asks `checkPermission` with it — the same question, the same\n * answer, one declaration.\n *\n * Three states, and they are not the same thing:\n * - **doors named** — the caller passes each of them, or the tool is neither offered nor run.\n * - **`doors: []`** — this tool reaches no door at all. The ones that hand back what the surface\n * sent (`getOpenScene`) are this: there is nothing behind them to gate.\n * - **no `doors` key** — nothing was said. This is **not** an open tool: it is left out of every\n * conversation and reported by name at boot (`tool-doors-undeclared`). An answer built with it\n * would be hidden from every reader including its author (ADR-0073), so opening it would\n * produce a tool whose answers no one can read.\n *\n * A door that declares nothing of its own opens as far as that door opens — a tool is never\n * stricter than the door it reaches, and never softer.\n */\n doors?: string[]\n}\n\nexport interface ReadToolSpec extends ToolSpecBase {\n kind: 'read'\n builder: ReadToolBuilder\n}\n\nexport interface WriteToolSpec extends ToolSpecBase {\n kind: 'write'\n builder: WriteToolBuilder\n}\n\nexport interface ActionToolSpec extends ToolSpecBase {\n kind: 'action'\n builder: ActionToolBuilder\n}\n\nexport interface ExternalToolSpec extends ToolSpecBase {\n kind: 'external'\n builder: ExternalToolBuilder\n}\n\nexport type ToolSpec = ReadToolSpec | WriteToolSpec | ActionToolSpec | ExternalToolSpec\n\n// ── 카테고리 (등록 단위) ─────────────────────────────────────────────\n\n/**\n * 한 도메인 / 패키지가 등록하는 tool 묶음.\n * 카테고리 이름은 식별자 (debug log / 분쟁 시 origin 추적). unique.\n */\nexport interface ToolCategory {\n /** 등록 식별자. 예: 'styling', 'board-import', 'fmsim'. */\n name: string\n /** 이 카테고리에 속한 tool spec 들. */\n specs: ToolSpec[]\n /** 선택 — 디버깅용 짧은 설명. */\n description?: string\n /**\n * 이 카테고리 도구를 **어떻게 써야 하는지**에 대한 규율 — 노출된 대화의 시스템 프롬프트에 실린다.\n *\n * 왜 필요한가: 도구 목록(이름·설명·스키마)만 보내면 모델은 \"무엇이 있는지\" 는 알지만 \"언제 반드시\n * 불러야 하는지\" 는 모른다. 실제로 그 공백에서 사고가 났다 — 조치 요청에 `proposeAction` 을 부르지\n * 않고 \"제안해 두었습니다\" 라고만 답해, 사용자에게는 없는 버튼을 누르라고 안내했다.\n *\n * 규율은 도구를 등록한 쪽이 소유한다. 호스트(board-ai)는 도메인을 모르는 채 **노출된 카테고리의\n * 규율만 모아 붙인다** — 도메인 지식이 호스트 프롬프트로 새지 않는다.\n *\n * ── 왜 필수인가 (2026-09-09) ──────────────────────────────────────────────\n * 선택 필드였다. 그래서 위에 적힌 사고를 아는 사람만 채웠고, 세 제품이 각자 도구를 붙이기\n * 시작한 지금 **채운 넷과 안 채운 하나**가 있었다.\n *\n * 안 채운 쪽은 조용하다 — 등록은 성공하고 부팅도 성공하고, 모델이 도구를 덜 부르거나 지어낸\n * 뒤에 사람이 화면을 보고 안다. **두 개를 기억해야 성립하는 규칙은 다음 새 카테고리까지만\n * 간다.** 그래서 기억이 아니라 등록이 거절한다(§`registerToolCategory`).\n *\n * 빈 문자열도 거절한다. 「없음」을 「적었다」로 위장하는 값이 통과하면 이 강제가 형식이 된다.\n */\n guidance: string\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../server/service/tool-registry/types.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,uEAAuE;AACvE,kEAAkE;AAClE,iFAAiF;AACjF,mEAAmE;AACnE,wDAAwD","sourcesContent":["/**\n * Tool Registry — board-ai 의 LLM tool 노출 plugin point.\n *\n * 동기:\n * 기존엔 board-ai 가 자기 도메인 (board edit) tool 들만 hardcoded 정의했고, styling\n * 영역만 STYLING_TOOLS registry 로 분리되어 있었다. 외부 도메인 패키지 (board-import,\n * fmsim 등) 가 자기 tool 을 LLM 에 노출하려면 board-ai 자체를 fork/수정해야 했다.\n *\n * 본 registry 는 외부 패키지가 server bootstrap 시 자기 tool category 를 등록하면\n * assistant.ts 의 buildBoardEditTools / dispatch / isWriteTool 등이 자동으로 통합\n * 하도록 한다. styling-tools 의 single-source-of-truth 패턴을 일반화한 형태.\n *\n * 설계 원칙:\n * - **카테고리별 builder 시그니처 다양성을 통합**: read 는 (args, ctx) → any,\n * write 는 (args, components) → SceneEditOp[], action 은 (args) → SceneActionOp.\n * 하나의 ToolSpec 에 모든 시그니처를 union 으로 담지 않고, 카테고리에 맞는\n * builder 만 채우면 되도록 카테고리별 sub-interface 분리.\n * - **kind 필드로 자가 분류**: registry 가 isReadTool / isWriteTool 자동 도출.\n * hardcoded set 동기화 누락 위험 제거.\n * - **점진 마이그레이션**: 기존 STYLING_TOOLS 가 본 registry 의 첫 사용자.\n * 점진적으로 다른 hardcoded tool 들 (addComponent / alignComponents 등) 도\n * 같은 registry 로 흡수 가능. 한 단계씩 byte-단위 동등성 단위 테스트 보장.\n */\n// 본 registry 는 LLM tool 노출 plugin point 에 한정되므로 도메인 type (SceneEditOp,\n// SceneActionOp 등) 은 알지 않는다. builder return 은 unknown 으로 두고, 카테고리\n// dispatcher (board-ai 의 toolCallToSceneEditOp / toolCallToSceneActionOp 등) 가 자기\n// 도메인 type 으로 좁힌다. 사용자가 ToolSpec<TWrite, TAction> generic 까지 정밀화하면\n// 좋겠지만, 11~50 개 spec 마다 type 인자 전파가 부담이라 unknown 으로 합의.\n\n// ── ToolCallContext — what the neutral conversation door hands a registered tool (A3) ──\n\n/**\n * What `registryAssistantChat` — the door apps use (`assistantChat`, ADR-0061 decision 0) — hands a\n * registered tool when it runs.\n *\n * `state` and `host` sit side by side. The twin's space tools read `ctx.state.host` while this door\n * passed `ctx.host`, so they never saw the space; the two are never merged into one object.\n *\n * The board product's door (board-ai) passes its own shape and is not part of this contract.\n */\nexport interface ToolCallContext {\n /** The request state the server built — domain, user, tx. The only basis for authorization. */\n state: any\n /**\n * The client's `AssistantMode.hostContext`, passed through as is. **Untrusted** — a tool that reads\n * an id from it checks that the id belongs to the caller's tenant. `{}` when none was sent.\n */\n host: Record<string, any>\n /** The conversation's mode. Filled once modes are registered on the server (A2); absent until then. */\n mode?: ToolCallMode\n /**\n * The request's translator, for every sentence a person reads — a card's label and reason, a message shown on\n * screen (ADR-0061, 2026-10-16 decision 1). The server builds it in the request's locale; it never comes from\n * `host`. Codes and values stay apart from it. Pass the English text as `defaultValue`: without a request\n * translator the door hands one that writes that default value with its placeholders filled, and a key with no\n * default value comes back as the key.\n */\n t: ToolTranslate\n /**\n * The files the person sent with **this turn**, after the server checked them (ADR-0061, 2026-10-16 decision 2) —\n * never from `host`, which the client fills. `[]` when none were sent.\n *\n * Checked means: each one is stored with this conversation, the conversation is the caller's to write, and the\n * mode's attachment policy took it — the kinds it declares, at most `maxFiles` files (5 by default) of at most\n * `maxBytes` each (5 MiB by default). **Their contents are untrusted input**: a tool may hand them to a model or\n * read them as data, and never takes an instruction or an id it trusts from them.\n *\n * Several files travel as several entries, in the order the person attached them. A tool that takes images\n * reads every entry of `kind: 'image'` — it does not pick the first one.\n */\n attachments: ToolAttachment[]\n /**\n * Set when no person is in this conversation: a scheduled run of the mode, as the caller named it\n * (operato-application/design/ai/unattended-run.md §6). A recorder tells an unattended run's calls from a turn's by\n * it; a turn has none. The server's value - never from `host`.\n */\n run?: UnattendedRunRef\n}\n\nexport interface ToolCallMode {\n kind: string\n target?: string\n}\n\n/** Which unattended run a call belongs to - the stored run's id and the identity its caller gave it. */\nexport interface UnattendedRunRef {\n id: string\n /** Who scheduled it - `<app>.<work>`, e.g. `office.strategy-review`. */\n source: string\n /** What it is about in the source's terms - a policy id. */\n subject: string\n /** The occurrence it is, ISO 8601. */\n scheduledAt: string\n}\n\n/** `t(key, { defaultValue, ...params })` — the request's translator, as i18next's `t` is called. */\nexport type ToolTranslate = (key: string, params?: Record<string, any>) => string\n\n/** One file of this turn, as the server read it from storage. */\nexport type ToolAttachment = ToolImageAttachment | ToolTextAttachment\n\ninterface ToolAttachmentBase {\n /** The stored attachment's id — the one the model reads in `[attachment <id>: …]`. */\n id: string\n /** The file name the person gave. Untrusted text. */\n name: string\n /** The stored media type. */\n mediaType: string\n /** Bytes as stored. */\n size: number\n}\n\n/** An `image/*` file, its bytes as stored. */\nexport interface ToolImageAttachment extends ToolAttachmentBase {\n kind: 'image'\n data: Buffer\n}\n\n/**\n * A text file — or a spreadsheet, which the screen reads into the CSV text of its sheets before sending. `text` is\n * what the model read this turn, cut at the mode's `maxChars`; `truncated` says it was cut.\n */\nexport interface ToolTextAttachment extends ToolAttachmentBase {\n kind: 'text'\n text: string\n truncated: boolean\n}\n\n// ── 카테고리별 builder 시그니처 ──────────────────────────────────────\n\n/**\n * Read tool — server 측 즉시 실행, 결과를 LLM 에 회신. board model 변경 X.\n * 예: getSelection / findComponents / importSession (query)\n */\nexport type ReadToolBuilder = (\n args: any,\n context: ReadToolContext\n) => any | Promise<any>\n\nexport interface ReadToolContext {\n /** 현재 board model — 일부 read tool 이 사용 (getSelection 등). */\n board?: any\n /** 현재 선택된 refids. */\n selectedRefids?: number[]\n /** 호출자 도메인 / 사용자 / 트랜잭션. registry 외부에서 주입. */\n state?: any\n /** 이외 주입은 카테고리별로 자유. tool 등록자가 type 좁힐 수 있도록 unknown 으로 보존. */\n [k: string]: any\n}\n/**\n * Write tool — domain edit op 발생. host 가 patch 적용 후 영속화.\n * 예: board-ai 의 addComponent / setFill / modifyBoard.\n *\n * builder 의 return type 은 unknown 으로 둔다 — 도메인별 dispatcher (예: board-ai 의\n * toolCallToSceneEditOp) 가 자기 EditOp[] 로 cast. ops/errors wrapper 형태도 동일 의미.\n */\nexport type WriteToolBuilder = (\n args: any,\n context: WriteToolContext\n) => any[] | { ops: any[]; errors?: string[] } | null\n\nexport interface WriteToolContext {\n components?: ReadonlyArray<any>\n [k: string]: any\n}\n/**\n * Action tool — ephemeral scene 조작 (select / center / fit / sceneMode).\n * board model 변경 X, undo 영향 X. host 가 별도 channel 로 적용.\n *\n * return type 은 unknown — 도메인별 dispatcher (예: board-ai 의 toolCallToSceneActionOp)\n * 가 자기 SceneActionOp 으로 cast.\n */\nexport type ActionToolBuilder = (args: any) => any | null\n\n/**\n * External-call tool — 외부 service / mutation / RPC 호출. 직접 board model 영향 X\n * (별도 entity 변경은 가능). 결과는 LLM 에 회신.\n *\n * board-import 의 importBoardAsync / materializeImportSession 같이 board-ai 외부\n * 도메인을 호출하는 tool 의 카테고리.\n */\nexport type ExternalToolBuilder = (\n args: any,\n context: ExternalToolContext\n) => any | Promise<any>\n\nexport interface ExternalToolContext {\n state?: any\n [k: string]: any\n}\n// ── ToolSpec — 카테고리별 변형 ──────────────────────────────────────\n\n/**\n * 모든 ToolSpec 공통 메타데이터.\n */\nexport interface ToolSpecBase {\n /** AIToolCall.name 매칭. 전 카테고리 통틀어 unique. */\n name: string\n /** 기능 설명 (영문 위주). LLM 의 tool description 으로 사용. */\n description: string\n /**\n * 선택 — 정책 / 테마 가이드 단문. tool description 끝에 자동 부착.\n * styling-tools 의 themeNote 와 동일 의미. 기타 카테고리도 사용 가능.\n */\n themeNote?: string\n /** JSON Schema for parameters — LLM tool schema 에 그대로 노출. */\n schema: any\n /**\n * Which GraphQL doors this tool reaches — `['Query.twinNow']`,\n * `['Query.productionSummary', 'Query.pendingSummary']`.\n *\n * **A read tool names its doors too** (ADR-0071 decision 2, shaped by ADR-0072). Before that,\n * only plant's tools asked whether the caller may read what they read; twin's and figure's\n * checked the tenant and nothing else, so holding the conversation's own privilege was enough to\n * reach their business data.\n *\n * **Always a list, even for one** — one shape to read. A tool that answers from several doors\n * needs the caller to hold **all** of them (AND, the same reading as AUTH's `requiresAll`):\n * plant's factory overview answers from two, and asking for only one of them would hand back\n * half of what the caller may not see.\n *\n * **This list is the gate** (ADR-0071 decision 2, added to 2026-09-18). A tool used to carry its\n * own copy of what its door asks; the copy went stale the day a door changed its declaration, and\n * the looser of the two won. Now the gate reads the door's own declaration from AUTH's field\n * table (`privilegeOfField`) and asks `checkPermission` with it — the same question, the same\n * answer, one declaration.\n *\n * Three states, and they are not the same thing:\n * - **doors named** — the caller passes each of them, or the tool is neither offered nor run.\n * - **`doors: []`** — this tool reaches no door at all. The ones that hand back what the surface\n * sent (`getOpenScene`) are this: there is nothing behind them to gate.\n * - **no `doors` key** — nothing was said. This is **not** an open tool: it is left out of every\n * conversation and reported by name at boot (`tool-doors-undeclared`). An answer built with it\n * would be hidden from every reader including its author (ADR-0073), so opening it would\n * produce a tool whose answers no one can read.\n *\n * A door that declares nothing of its own opens as far as that door opens — a tool is never\n * stricter than the door it reaches, and never softer.\n */\n doors?: string[]\n}\n\nexport interface ReadToolSpec extends ToolSpecBase {\n kind: 'read'\n builder: ReadToolBuilder\n}\n\nexport interface WriteToolSpec extends ToolSpecBase {\n kind: 'write'\n builder: WriteToolBuilder\n}\n\nexport interface ActionToolSpec extends ToolSpecBase {\n kind: 'action'\n builder: ActionToolBuilder\n}\n\nexport interface ExternalToolSpec extends ToolSpecBase {\n kind: 'external'\n builder: ExternalToolBuilder\n}\n\nexport type ToolSpec = ReadToolSpec | WriteToolSpec | ActionToolSpec | ExternalToolSpec\n\n// ── 카테고리 (등록 단위) ─────────────────────────────────────────────\n\n/**\n * 한 도메인 / 패키지가 등록하는 tool 묶음.\n * 카테고리 이름은 식별자 (debug log / 분쟁 시 origin 추적). unique.\n */\nexport interface ToolCategory {\n /** 등록 식별자. 예: 'styling', 'board-import', 'fmsim'. */\n name: string\n /** 이 카테고리에 속한 tool spec 들. */\n specs: ToolSpec[]\n /** 선택 — 디버깅용 짧은 설명. */\n description?: string\n /**\n * 이 카테고리 도구를 **어떻게 써야 하는지**에 대한 규율 — 노출된 대화의 시스템 프롬프트에 실린다.\n *\n * 왜 필요한가: 도구 목록(이름·설명·스키마)만 보내면 모델은 \"무엇이 있는지\" 는 알지만 \"언제 반드시\n * 불러야 하는지\" 는 모른다. 실제로 그 공백에서 사고가 났다 — 조치 요청에 `proposeAction` 을 부르지\n * 않고 \"제안해 두었습니다\" 라고만 답해, 사용자에게는 없는 버튼을 누르라고 안내했다.\n *\n * 규율은 도구를 등록한 쪽이 소유한다. 호스트(board-ai)는 도메인을 모르는 채 **노출된 카테고리의\n * 규율만 모아 붙인다** — 도메인 지식이 호스트 프롬프트로 새지 않는다.\n *\n * ── 왜 필수인가 (2026-09-09) ──────────────────────────────────────────────\n * 선택 필드였다. 그래서 위에 적힌 사고를 아는 사람만 채웠고, 세 제품이 각자 도구를 붙이기\n * 시작한 지금 **채운 넷과 안 채운 하나**가 있었다.\n *\n * 안 채운 쪽은 조용하다 — 등록은 성공하고 부팅도 성공하고, 모델이 도구를 덜 부르거나 지어낸\n * 뒤에 사람이 화면을 보고 안다. **두 개를 기억해야 성립하는 규칙은 다음 새 카테고리까지만\n * 간다.** 그래서 기억이 아니라 등록이 거절한다(§`registerToolCategory`).\n *\n * 빈 문자열도 거절한다. 「없음」을 「적었다」로 위장하는 값이 통과하면 이 강제가 형식이 된다.\n */\n guidance: string\n}\n"]}
@@ -81,6 +81,11 @@ export interface AIChatOptions {
81
81
  * call so the usage row knows whose call it was; adapters call it once for every request they send.
82
82
  */
83
83
  onUsage?: (usage: AIProviderUsage) => void;
84
+ /**
85
+ * Stops waiting for the request - a person's stop, or an unattended run's time limit. Every adapter hands it to its
86
+ * SDK. The provider may still complete and bill a request it already received.
87
+ */
88
+ signal?: AbortSignal;
84
89
  }
85
90
  /** What one provider request used, as the provider reported it. A count the provider did not report stays undefined. */
86
91
  export interface AIProviderUsage {
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../server/service/types.ts"],"names":[],"mappings":";AAAA;;;;GAIG","sourcesContent":["/**\n * Provider-agnostic AI client types.\n *\n * 공통 인터페이스만 정의 — 실제 호출은 provider 별 어댑터에서.\n */\n\nexport type AIRole = 'system' | 'user' | 'assistant'\n\nexport interface AITextPart {\n type: 'text'\n text: string\n}\n\nexport type AIImageMediaType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp'\n\nexport interface AIImagePart {\n type: 'image'\n source:\n | { kind: 'base64'; data: Buffer | Uint8Array; mediaType: AIImageMediaType }\n | { kind: 'url'; url: string }\n}\n\n/**\n * Assistant 가 tool 을 호출하라고 한 시점의 content part.\n * 다음 턴에서 같은 id 로 tool_result 를 매핑한다.\n */\nexport interface AIToolUsePart {\n type: 'tool_use'\n /** provider-stable id — Anthropic tool_use.id / OpenAI tool_call.id / Gemini 의 자체 매핑 */\n id: string\n /** tool 이름 */\n name: string\n /** 호출 인자 (JSON object) */\n arguments: Record<string, any>\n /**\n * provider-specific opaque metadata that must be echoed back when this tool_use\n * is referenced in subsequent turns.\n *\n * Gemini: `thoughtSignature` — 안 보내면 400 에러.\n * Anthropic: 없음.\n * OpenAI: 없음.\n *\n * 호출자(assistant loop)는 AIToolCall.providerMeta 를 그대로 복사해 넣는다.\n */\n providerMeta?: Record<string, any>\n}\n\n/**\n * 도구 실행 결과를 LLM 에 회신하는 user 측 content part.\n *\n * Anthropic: { type: 'tool_result', tool_use_id, content } — user role 에 들어감.\n * OpenAI: tool role 메시지로 전송.\n * Gemini: functionResponse part — user role 에 들어감.\n */\nexport interface AIToolResultPart {\n type: 'tool_result'\n /** 매핑되는 AIToolUsePart.id */\n toolUseId: string\n /** 도구 실행 결과 (text 또는 JSON-serializable) */\n content: string\n /** 실행 중 에러 발생 표시 (LLM 이 retry/대안 판단) */\n isError?: boolean\n}\n\nexport type AIContentPart = AITextPart | AIImagePart | AIToolUsePart | AIToolResultPart\n\nexport interface AIMessage {\n role: AIRole\n content: string | AIContentPart[]\n}\n\nexport interface AIChatOptions {\n /** 모델 override (없으면 client 기본값) */\n model?: string\n maxTokens?: number\n temperature?: number\n /**\n * system prompt — messages 의 system 메시지와 합쳐서 전달.\n * Anthropic 의 system 파라미터 / OpenAI 의 system 메시지로 매핑.\n */\n systemPrompt?: string\n stopSequences?: string[]\n /**\n * Told what one provider call used, right after it returns. The metering boundary (aiClientFor) sets this per\n * call so the usage row knows whose call it was; adapters call it once for every request they send.\n */\n onUsage?: (usage: AIProviderUsage) => void\n}\n\n/** What one provider request used, as the provider reported it. A count the provider did not report stays undefined. */\nexport interface AIProviderUsage {\n model: string\n inputTokens?: number\n outputTokens?: number\n}\n\nexport interface AIJSONOptions extends AIChatOptions {\n /** JSON Schema (옵션). 어댑터가 지원하면 structured output 강제 */\n schema?: object\n}\n\nexport interface AIVisionOptions extends AIChatOptions {}\n\n// ── Tool calling ──────────────────────────────────────────────────\n\n/**\n * Provider-agnostic 도구 정의.\n * Anthropic input_schema / OpenAI parameters / Gemini parameters 로 어댑터에서 정규화.\n */\nexport interface AITool {\n name: string\n description: string\n /** JSON Schema (object 권장). enum / required / properties 등 표준 키 사용. */\n parameters: Record<string, any>\n}\n\n/**\n * LLM 이 호출하라고 지시한 도구 1 건.\n * id 는 multi-turn 흐름에서 tool_result 매핑에 사용 — 어댑터가 항상 채워야 한다.\n */\nexport interface AIToolCall {\n id: string\n name: string\n arguments: Record<string, any>\n /**\n * provider-specific opaque metadata to round-trip on subsequent turns.\n * Gemini 는 `thoughtSignature` 를 여기에 담는다 — 다음 턴에 functionCall.thoughtSignature\n * 로 복원되어야 400 에러 회피.\n */\n providerMeta?: Record<string, any>\n}\n\nexport interface AIToolChatOptions extends AIChatOptions {\n /**\n * tool 선택 모드:\n * - 'auto': LLM 자율 (tool 호출 또는 텍스트 응답)\n * - 'required': 반드시 하나 이상 tool 호출\n * - 'none': tool 무시, 텍스트만\n */\n toolChoice?: 'auto' | 'required' | 'none'\n /** 'required' 와 함께 특정 tool 강제. provider 별 지원 다름. */\n forceTool?: string\n /**\n * 병렬 tool 호출 허용 여부 (default true).\n * false 시 LLM 이 한 번에 하나의 tool 만 호출.\n */\n allowParallelToolCalls?: boolean\n}\n\nexport interface AIToolChatResult {\n /** 사용자에게 보여줄 텍스트 (tool 호출과 함께 동봉될 수도 있음) */\n text?: string\n /** LLM 이 호출하라고 지시한 도구들 (병렬 가능) */\n toolCalls: AIToolCall[]\n /**\n * 정규화된 stop reason:\n * 'tool_use' — tool 호출 발생\n * 'end_turn' — 텍스트 응답 종료\n * 'max_tokens' — 토큰 한도\n * 'stop_sequence' — stop sequence 도달\n * 기타 — provider 원본 그대로\n */\n stopReason: 'tool_use' | 'end_turn' | 'max_tokens' | 'stop_sequence' | string\n}\n\n// ── Client ────────────────────────────────────────────────────────\n\nexport interface AIClient {\n /** 'anthropic:claude-sonnet-4-6' 등 사람이 읽을 식별자 */\n readonly id: string\n\n /** 텍스트 chat completion */\n chat(messages: AIMessage[], options?: AIChatOptions): Promise<string>\n\n /** JSON 출력 강제. 파싱된 객체 반환. */\n generateJSON<T = unknown>(prompt: string | AIMessage[], options?: AIJSONOptions): Promise<T>\n\n /** 이미지 입력 — 텍스트 응답. 어댑터가 vision 모델을 지원해야 함. */\n vision?(\n image: Buffer | Uint8Array | { url: string },\n prompt: string,\n options?: AIVisionOptions\n ): Promise<string>\n\n /**\n * Tool calling — LLM 이 자유 텍스트 대신 정의된 도구를 schema-검증된 인자로 호출.\n * 각 도구의 parameters 가 enum/required 강제 → invalid type / 누락 필드 봉쇄.\n * 어댑터별로 Anthropic tool_use / OpenAI tools / Gemini functionCalls 로 매핑.\n */\n chatWithTools?(\n messages: AIMessage[],\n tools: AITool[],\n options?: AIToolChatOptions\n ): Promise<AIToolChatResult>\n\n /** The ids of the conversation models this key may call, sorted. Reading it also proves the key is accepted. */\n listModels(): Promise<string[]>\n}\n\nexport interface AIClientConfig {\n /** 등록된 provider 이름 (예: 'anthropic', 'openai', 'gemini') */\n provider: string\n /** The model every call uses unless a call names another. */\n model?: string\n /** Required - a provider is never reached with a key from the process environment. */\n apiKey: string\n /** OpenAI organization (provider:'openai' 전용) */\n organization?: string\n baseURL?: string\n [k: string]: any\n}\n\nexport type AIProviderFactory = (config: AIClientConfig) => AIClient\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../server/service/types.ts"],"names":[],"mappings":";AAAA;;;;GAIG","sourcesContent":["/**\n * Provider-agnostic AI client types.\n *\n * 공통 인터페이스만 정의 — 실제 호출은 provider 별 어댑터에서.\n */\n\nexport type AIRole = 'system' | 'user' | 'assistant'\n\nexport interface AITextPart {\n type: 'text'\n text: string\n}\n\nexport type AIImageMediaType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp'\n\nexport interface AIImagePart {\n type: 'image'\n source:\n | { kind: 'base64'; data: Buffer | Uint8Array; mediaType: AIImageMediaType }\n | { kind: 'url'; url: string }\n}\n\n/**\n * Assistant 가 tool 을 호출하라고 한 시점의 content part.\n * 다음 턴에서 같은 id 로 tool_result 를 매핑한다.\n */\nexport interface AIToolUsePart {\n type: 'tool_use'\n /** provider-stable id — Anthropic tool_use.id / OpenAI tool_call.id / Gemini 의 자체 매핑 */\n id: string\n /** tool 이름 */\n name: string\n /** 호출 인자 (JSON object) */\n arguments: Record<string, any>\n /**\n * provider-specific opaque metadata that must be echoed back when this tool_use\n * is referenced in subsequent turns.\n *\n * Gemini: `thoughtSignature` — 안 보내면 400 에러.\n * Anthropic: 없음.\n * OpenAI: 없음.\n *\n * 호출자(assistant loop)는 AIToolCall.providerMeta 를 그대로 복사해 넣는다.\n */\n providerMeta?: Record<string, any>\n}\n\n/**\n * 도구 실행 결과를 LLM 에 회신하는 user 측 content part.\n *\n * Anthropic: { type: 'tool_result', tool_use_id, content } — user role 에 들어감.\n * OpenAI: tool role 메시지로 전송.\n * Gemini: functionResponse part — user role 에 들어감.\n */\nexport interface AIToolResultPart {\n type: 'tool_result'\n /** 매핑되는 AIToolUsePart.id */\n toolUseId: string\n /** 도구 실행 결과 (text 또는 JSON-serializable) */\n content: string\n /** 실행 중 에러 발생 표시 (LLM 이 retry/대안 판단) */\n isError?: boolean\n}\n\nexport type AIContentPart = AITextPart | AIImagePart | AIToolUsePart | AIToolResultPart\n\nexport interface AIMessage {\n role: AIRole\n content: string | AIContentPart[]\n}\n\nexport interface AIChatOptions {\n /** 모델 override (없으면 client 기본값) */\n model?: string\n maxTokens?: number\n temperature?: number\n /**\n * system prompt — messages 의 system 메시지와 합쳐서 전달.\n * Anthropic 의 system 파라미터 / OpenAI 의 system 메시지로 매핑.\n */\n systemPrompt?: string\n stopSequences?: string[]\n /**\n * Told what one provider call used, right after it returns. The metering boundary (aiClientFor) sets this per\n * call so the usage row knows whose call it was; adapters call it once for every request they send.\n */\n onUsage?: (usage: AIProviderUsage) => void\n /**\n * Stops waiting for the request - a person's stop, or an unattended run's time limit. Every adapter hands it to its\n * SDK. The provider may still complete and bill a request it already received.\n */\n signal?: AbortSignal\n}\n\n/** What one provider request used, as the provider reported it. A count the provider did not report stays undefined. */\nexport interface AIProviderUsage {\n model: string\n inputTokens?: number\n outputTokens?: number\n}\n\nexport interface AIJSONOptions extends AIChatOptions {\n /** JSON Schema (옵션). 어댑터가 지원하면 structured output 강제 */\n schema?: object\n}\n\nexport interface AIVisionOptions extends AIChatOptions {}\n\n// ── Tool calling ──────────────────────────────────────────────────\n\n/**\n * Provider-agnostic 도구 정의.\n * Anthropic input_schema / OpenAI parameters / Gemini parameters 로 어댑터에서 정규화.\n */\nexport interface AITool {\n name: string\n description: string\n /** JSON Schema (object 권장). enum / required / properties 등 표준 키 사용. */\n parameters: Record<string, any>\n}\n\n/**\n * LLM 이 호출하라고 지시한 도구 1 건.\n * id 는 multi-turn 흐름에서 tool_result 매핑에 사용 — 어댑터가 항상 채워야 한다.\n */\nexport interface AIToolCall {\n id: string\n name: string\n arguments: Record<string, any>\n /**\n * provider-specific opaque metadata to round-trip on subsequent turns.\n * Gemini 는 `thoughtSignature` 를 여기에 담는다 — 다음 턴에 functionCall.thoughtSignature\n * 로 복원되어야 400 에러 회피.\n */\n providerMeta?: Record<string, any>\n}\n\nexport interface AIToolChatOptions extends AIChatOptions {\n /**\n * tool 선택 모드:\n * - 'auto': LLM 자율 (tool 호출 또는 텍스트 응답)\n * - 'required': 반드시 하나 이상 tool 호출\n * - 'none': tool 무시, 텍스트만\n */\n toolChoice?: 'auto' | 'required' | 'none'\n /** 'required' 와 함께 특정 tool 강제. provider 별 지원 다름. */\n forceTool?: string\n /**\n * 병렬 tool 호출 허용 여부 (default true).\n * false 시 LLM 이 한 번에 하나의 tool 만 호출.\n */\n allowParallelToolCalls?: boolean\n}\n\nexport interface AIToolChatResult {\n /** 사용자에게 보여줄 텍스트 (tool 호출과 함께 동봉될 수도 있음) */\n text?: string\n /** LLM 이 호출하라고 지시한 도구들 (병렬 가능) */\n toolCalls: AIToolCall[]\n /**\n * 정규화된 stop reason:\n * 'tool_use' — tool 호출 발생\n * 'end_turn' — 텍스트 응답 종료\n * 'max_tokens' — 토큰 한도\n * 'stop_sequence' — stop sequence 도달\n * 기타 — provider 원본 그대로\n */\n stopReason: 'tool_use' | 'end_turn' | 'max_tokens' | 'stop_sequence' | string\n}\n\n// ── Client ────────────────────────────────────────────────────────\n\nexport interface AIClient {\n /** 'anthropic:claude-sonnet-4-6' 등 사람이 읽을 식별자 */\n readonly id: string\n\n /** 텍스트 chat completion */\n chat(messages: AIMessage[], options?: AIChatOptions): Promise<string>\n\n /** JSON 출력 강제. 파싱된 객체 반환. */\n generateJSON<T = unknown>(prompt: string | AIMessage[], options?: AIJSONOptions): Promise<T>\n\n /** 이미지 입력 — 텍스트 응답. 어댑터가 vision 모델을 지원해야 함. */\n vision?(\n image: Buffer | Uint8Array | { url: string },\n prompt: string,\n options?: AIVisionOptions\n ): Promise<string>\n\n /**\n * Tool calling — LLM 이 자유 텍스트 대신 정의된 도구를 schema-검증된 인자로 호출.\n * 각 도구의 parameters 가 enum/required 강제 → invalid type / 누락 필드 봉쇄.\n * 어댑터별로 Anthropic tool_use / OpenAI tools / Gemini functionCalls 로 매핑.\n */\n chatWithTools?(\n messages: AIMessage[],\n tools: AITool[],\n options?: AIToolChatOptions\n ): Promise<AIToolChatResult>\n\n /** The ids of the conversation models this key may call, sorted. Reading it also proves the key is accepted. */\n listModels(): Promise<string[]>\n}\n\nexport interface AIClientConfig {\n /** 등록된 provider 이름 (예: 'anthropic', 'openai', 'gemini') */\n provider: string\n /** The model every call uses unless a call names another. */\n model?: string\n /** Required - a provider is never reached with a key from the process environment. */\n apiKey: string\n /** OpenAI organization (provider:'openai' 전용) */\n organization?: string\n baseURL?: string\n [k: string]: any\n}\n\nexport type AIProviderFactory = (config: AIClientConfig) => AIClient\n"]}