@things-factory/ai-assistant 10.1.38 → 10.1.40

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 (37) hide show
  1. package/client/components/assistant-chat.ts +30 -1
  2. package/client/utils/assistant-request-context.ts +32 -0
  3. package/dist-client/components/assistant-chat.d.ts +12 -0
  4. package/dist-client/components/assistant-chat.js +24 -1
  5. package/dist-client/components/assistant-chat.js.map +1 -1
  6. package/dist-client/tsconfig.tsbuildinfo +1 -1
  7. package/dist-client/utils/assistant-request-context.d.ts +23 -0
  8. package/dist-client/utils/assistant-request-context.js +25 -0
  9. package/dist-client/utils/assistant-request-context.js.map +1 -1
  10. package/dist-server/service/chat-message/chat-message-broadcast.d.ts +6 -6
  11. package/dist-server/service/chat-message/chat-message-broadcast.js +5 -5
  12. package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -1
  13. package/dist-server/service/chat-message/chat-message-room.d.ts +3 -3
  14. package/dist-server/service/chat-message/chat-message-room.js +10 -7
  15. package/dist-server/service/chat-message/chat-message-room.js.map +1 -1
  16. package/dist-server/service/chat-message/chat-message-subscription.js +6 -5
  17. package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -1
  18. package/dist-server/service/chat-session/chat-session.js +1 -1
  19. package/dist-server/service/chat-session/chat-session.js.map +1 -1
  20. package/dist-server/service/chat-session/session-anchor.d.ts +5 -1
  21. package/dist-server/service/chat-session/session-anchor.js +23 -1
  22. package/dist-server/service/chat-session/session-anchor.js.map +1 -1
  23. package/dist-server/service/testing/mode-conformance.js +6 -5
  24. package/dist-server/service/testing/mode-conformance.js.map +1 -1
  25. package/dist-server/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +5 -5
  27. package/server/service/chat-message/chat-message-broadcast.ts +15 -13
  28. package/server/service/chat-message/chat-message-room.ts +10 -7
  29. package/server/service/chat-message/chat-message-subscription.ts +6 -5
  30. package/server/service/chat-session/chat-session.ts +1 -1
  31. package/server/service/chat-session/session-anchor.ts +26 -1
  32. package/server/service/session-anchor.test.ts +55 -1
  33. package/server/service/testing/conversation-script.test.ts +1 -1
  34. package/server/service/testing/mode-conformance.test.ts +2 -2
  35. package/server/service/testing/mode-conformance.ts +6 -5
  36. package/test/assistant-request-context.test.ts +41 -1
  37. package/test/open-doors.test.ts +132 -0
@@ -1 +1 @@
1
- {"version":3,"file":"mode-conformance.js","sourceRoot":"","sources":["../../../server/service/testing/mode-conformance.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 등록된 모드가 **선언한 대로 도는가** — 글자가 아니라 문을 지나서 본다.\n *\n * 모드 선언은 소스에 적힌 값이고, 사람이 만나는 것은 그 값이 문을 지나 만들어 낸 것이다. 그 둘이\n * 어긋나는 자리가 오늘 하루에만 여럿 나왔다: 범주에 없는 도구를 프롬프트가 부르라고 했고, 키는 있는데\n * 번역이 없었고, 부품에 규칙이 있는데 부르는 쪽이 없었다. 전부 「선언은 맞는데 돌지 않는」 모양이다.\n *\n * 그래서 앱의 시험이 이것으로 **자기 모드를 실제로 돌려 본다**:\n * · 그 이름으로 모드가 서는가\n * · 그 모드가 여는 도구가 무엇인가 — 모델에게 실제로 건네지는 목록\n * · 첫 턴 도구 호출을 강제하는가\n * · 화면에 낼 말이 **키가 아니라 문장**으로 나오는가\n * · 슬래시가 가리키는 도구가 그 모드 안에 있는가\n */\nimport { registryAssistantChat } from '@things-factory/ai-client-base'\n\nimport { getAssistantMode, getAssistantModeWarnings } from '../assistant-mode/registry.js'\nimport { assistantModeView, resolveModeSettings, type Translate } from '../assistant-mode/resolve.js'\n\nexport interface ModeRunResult {\n /** 이 모드가 모델에게 실제로 건넨 도구 이름. */\n offeredTools: string[]\n /** 모델이 받은 시스템 프롬프트 — 서버의 것인지 확인할 수 있다. */\n systemPrompt: string\n /** 첫 턴에 도구를 강제했는가. */\n requiredToolOnFirstTurn: boolean\n reply: string\n}\n\n/** 시험용 번역기 — 키를 그대로 돌려준다. 문장이 나와야 할 자리에 키가 나오면 눈에 띈다. */\nexport const keyEchoTranslate: Translate = key => key\n\n/**\n * 등록된 모드로 한 턴을 돌린다 — 프롬프트도 도구도 **등록에서** 나온다.\n *\n * 시험이 자기 값을 넘기면 그 시험은 선언을 안 본 것이다. 그래서 여기서는 `kind` 만 받는다.\n */\nexport async function runRegisteredMode(\n kind: string,\n options: { client: any; hostContext?: any; state?: any; toolAccess?: any; reply?: string } = {} as any\n): Promise<ModeRunResult> {\n const settings = resolveModeSettings({ kind }, keyEchoTranslate)\n let requiredToolOnFirstTurn = false\n\n const result = await registryAssistantChat({\n client: options.client,\n messages: [{ role: 'user', content: '한 턴' }] as any,\n systemPrompt: settings.systemPrompt,\n toolCategories: settings.toolCategories,\n requireToolOnFirstTurn: (requiredToolOnFirstTurn = Boolean(settings.requireToolOnFirstTurn)),\n model: settings.model,\n temperature: settings.temperature,\n maxTokens: settings.maxTokens,\n hostContext: options.hostContext,\n /*\n * 이 하네스가 묻는 것은 **모드**다 — 「이 모드가 무엇을 여는가」이지 「이 사람이 문을 지나는가」가\n * 아니다. 관문이 `doors` 로 옮겨간 뒤(ADR-0071 결정 2) 검사를 안 넘기면 문을 적은 도구가 전부\n * 닫히고, 그러면 앱의 적합성 시험에서 **실제 자료를 읽는 도구만** 조용히 사라진다. 그래서 기본은\n * 모든 문을 지나는 사람이고, 다른 쪽을 보고 싶은 앱은 `toolAccess` 를 적어 그 경우를 쓴다.\n * 권한 축은 자기 시험이 따로 본다(`tool-access.test.ts` · `tool-scope.test.ts`).\n */\n toolAccess: options.toolAccess ?? (async () => true),\n state: options.state ?? { domain: { id: 'dom-1' }, user: { id: 'u-1' } }\n })\n\n return {\n offeredTools: [...result.offeredTools],\n systemPrompt: settings.systemPrompt,\n requiredToolOnFirstTurn,\n reply: result.reply\n }\n}\n\nexport interface ModeWording {\n placeholder?: string\n /** 빈 대화의 제목과 한 줄 — 없으면 공용 대화창이 보드 제품의 글을 낸다. */\n intro?: { title: string; subtitle: string }\n /** 빈 대화의 예시 — 이 대화가 실제로 할 수 있는 말. */\n examples?: Array<{ label: string; items: string[] }>\n slash: { name: string; description: string; template: string }[]\n footerNotice?: string\n stagedNotice?: string\n}\n\n/** 화면에 낼 말 — 주어진 사전으로 옮긴 결과. 키가 그대로 남으면 사전에 없는 것이다. */\nexport function modeWording(kind: string, dictionary: Record<string, string>): ModeWording {\n const view = assistantModeView(kind, key => dictionary[key] ?? key)\n return {\n placeholder: view.placeholder,\n intro: view.intro,\n examples: view.examples,\n slash: view.slashTemplates,\n footerNotice: view.footerNotice,\n stagedNotice: view.stagedNotice\n }\n}\n\n/** 사전에 없어서 키가 그대로 화면에 나갈 말들. 비어 있어야 한다. */\nexport function missingWording(kind: string, dictionary: Record<string, string>): string[] {\n const missing: string[] = []\n const spec = getAssistantMode(kind)\n if (!spec) return [`mode:${kind}`]\n\n for (const key of [spec.placeholderKey, spec.footerNoticeKey, spec.stagedNoticeKey]) {\n if (key && !(key in dictionary)) missing.push(key)\n }\n /* 빈 대화의 말도 사전에 있어야 한다 — 없으면 화면에 키가 그대로 찍힌다. */\n for (const key of [spec.introKeys?.title, spec.introKeys?.subtitle]) {\n if (key && !(key in dictionary)) missing.push(key)\n }\n for (const group of spec.exampleKeys ?? []) {\n for (const key of [group.labelKey, ...group.itemKeys]) {\n if (!(key in dictionary)) missing.push(key)\n }\n }\n for (const slash of spec.slashTemplates ?? []) {\n for (const key of [slash.descriptionKey, slash.templateKey]) {\n if (!(key in dictionary)) missing.push(key)\n }\n }\n return missing\n}\n\n/** 이 모드가 자기 도구로 못 하는 일을 제안하는가 — 부팅 보고와 같은 답. */\nexport function modePromisesItCannotKeep(kind: string): string[] {\n return getAssistantModeWarnings()\n .filter(warning => warning.params.kind === kind)\n .map(warning => `${warning.code}:${warning.params.slash}→${warning.params.tool}`)\n}\n"]}
1
+ {"version":3,"file":"mode-conformance.js","sourceRoot":"","sources":["../../../server/service/testing/mode-conformance.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 등록된 모드가 **선언한 대로 도는가** — 글자가 아니라 문을 지나서 본다.\n *\n * 모드 선언은 소스에 적힌 값이고, 사람이 만나는 것은 그 값이 문을 지나 만들어 낸 것이다. 그 둘이\n * 어긋나는 자리가 오늘 하루에만 여럿 나왔다: 범주에 없는 도구를 프롬프트가 부르라고 했고, 키는 있는데\n * 번역이 없었고, 부품에 규칙이 있는데 부르는 쪽이 없었다. 전부 「선언은 맞는데 돌지 않는」 모양이다.\n *\n * 그래서 앱의 시험이 이것으로 **자기 모드를 실제로 돌려 본다**:\n * · 그 이름으로 모드가 서는가\n * · 그 모드가 여는 도구가 무엇인가 — 모델에게 실제로 건네지는 목록\n * · 첫 턴 도구 호출을 강제하는가\n * · 화면에 낼 말이 **키가 아니라 문장**으로 나오는가\n * · 슬래시가 가리키는 도구가 그 모드 안에 있는가\n */\nimport { registryAssistantChat } from '@things-factory/ai-client-base'\n\nimport { getAssistantMode, getAssistantModeWarnings } from '../assistant-mode/registry.js'\nimport { assistantModeView, resolveModeSettings, type Translate } from '../assistant-mode/resolve.js'\n\nexport interface ModeRunResult {\n /** 이 모드가 모델에게 실제로 건넨 도구 이름. */\n offeredTools: string[]\n /** 모델이 받은 시스템 프롬프트 — 서버의 것인지 확인할 수 있다. */\n systemPrompt: string\n /** 첫 턴에 도구를 강제했는가. */\n requiredToolOnFirstTurn: boolean\n reply: string\n}\n\n/** 시험용 번역기 — 키를 그대로 돌려준다. 문장이 나와야 할 자리에 키가 나오면 눈에 띈다. */\nexport const keyEchoTranslate: Translate = key => key\n\n/**\n * 등록된 모드로 한 턴을 돌린다 — 프롬프트도 도구도 **등록에서** 나온다.\n *\n * 시험이 자기 값을 넘기면 그 시험은 선언을 안 본 것이다. 그래서 여기서는 `kind` 만 받는다.\n */\nexport async function runRegisteredMode(\n kind: string,\n options: { client: any; hostContext?: any; state?: any; toolAccess?: any; reply?: string } = {} as any\n): Promise<ModeRunResult> {\n const settings = resolveModeSettings({ kind }, keyEchoTranslate)\n let requiredToolOnFirstTurn = false\n\n const result = await registryAssistantChat({\n client: options.client,\n messages: [{ role: 'user', content: '한 턴' }] as any,\n systemPrompt: settings.systemPrompt,\n toolCategories: settings.toolCategories,\n requireToolOnFirstTurn: (requiredToolOnFirstTurn = Boolean(settings.requireToolOnFirstTurn)),\n model: settings.model,\n temperature: settings.temperature,\n maxTokens: settings.maxTokens,\n hostContext: options.hostContext,\n /*\n * This harness asks about **the mode** — \"what does this mode open\", not \"does this person pass the\n * doors\". Since the gate moved onto `doors` (ADR-0071 decision 2), passing no check closes every tool\n * that names a door, and an app's conformance test would quietly lose **exactly the tools that read\n * real data**. So the default is someone who passes every door; an app that wants the other case\n * writes `toolAccess`. The permission axis has its own tests (`tool-access.test.ts` ·\n * `tool-scope.test.ts`).\n */\n toolAccess: options.toolAccess ?? (async () => true),\n state: options.state ?? { domain: { id: 'dom-1' }, user: { id: 'u-1' } }\n })\n\n return {\n offeredTools: [...result.offeredTools],\n systemPrompt: settings.systemPrompt,\n requiredToolOnFirstTurn,\n reply: result.reply\n }\n}\n\nexport interface ModeWording {\n placeholder?: string\n /** 빈 대화의 제목과 한 줄 — 없으면 공용 대화창이 보드 제품의 글을 낸다. */\n intro?: { title: string; subtitle: string }\n /** 빈 대화의 예시 — 이 대화가 실제로 할 수 있는 말. */\n examples?: Array<{ label: string; items: string[] }>\n slash: { name: string; description: string; template: string }[]\n footerNotice?: string\n stagedNotice?: string\n}\n\n/** 화면에 낼 말 — 주어진 사전으로 옮긴 결과. 키가 그대로 남으면 사전에 없는 것이다. */\nexport function modeWording(kind: string, dictionary: Record<string, string>): ModeWording {\n const view = assistantModeView(kind, key => dictionary[key] ?? key)\n return {\n placeholder: view.placeholder,\n intro: view.intro,\n examples: view.examples,\n slash: view.slashTemplates,\n footerNotice: view.footerNotice,\n stagedNotice: view.stagedNotice\n }\n}\n\n/** 사전에 없어서 키가 그대로 화면에 나갈 말들. 비어 있어야 한다. */\nexport function missingWording(kind: string, dictionary: Record<string, string>): string[] {\n const missing: string[] = []\n const spec = getAssistantMode(kind)\n if (!spec) return [`mode:${kind}`]\n\n for (const key of [spec.placeholderKey, spec.footerNoticeKey, spec.stagedNoticeKey]) {\n if (key && !(key in dictionary)) missing.push(key)\n }\n /* 빈 대화의 말도 사전에 있어야 한다 — 없으면 화면에 키가 그대로 찍힌다. */\n for (const key of [spec.introKeys?.title, spec.introKeys?.subtitle]) {\n if (key && !(key in dictionary)) missing.push(key)\n }\n for (const group of spec.exampleKeys ?? []) {\n for (const key of [group.labelKey, ...group.itemKeys]) {\n if (!(key in dictionary)) missing.push(key)\n }\n }\n for (const slash of spec.slashTemplates ?? []) {\n for (const key of [slash.descriptionKey, slash.templateKey]) {\n if (!(key in dictionary)) missing.push(key)\n }\n }\n return missing\n}\n\n/** 이 모드가 자기 도구로 못 하는 일을 제안하는가 — 부팅 보고와 같은 답. */\nexport function modePromisesItCannotKeep(kind: string): string[] {\n return getAssistantModeWarnings()\n .filter(warning => warning.params.kind === kind)\n .map(warning => `${warning.code}:${warning.params.slash}→${warning.params.tool}`)\n}\n"]}