@things-factory/ai-assistant 10.1.210 → 10.1.213
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/components/assistant-chat.ts +115 -2
- package/client/components/assistant-dock.ts +106 -3
- package/client/components/workspace-view.ts +56 -3
- package/client/index.ts +3 -0
- package/client/utils/assistant-ask.test.ts +38 -0
- package/client/utils/assistant-ask.ts +35 -1
- package/client/utils/assistant-proposals.ts +23 -0
- package/client/utils/assistant-selection.test.ts +30 -0
- package/client/utils/assistant-selection.ts +47 -0
- package/client/utils/assistant-session-controller.ts +2 -0
- package/client/utils/assistant-session-transport.ts +1 -1
- package/client/utils/invite-proposal-register.ts +31 -0
- package/client/utils/invite-proposal.test.ts +40 -0
- package/client/utils/invite-proposal.ts +54 -0
- package/dist-client/components/assistant-chat.d.ts +18 -0
- package/dist-client/components/assistant-chat.js +117 -2
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-dock.d.ts +11 -0
- package/dist-client/components/assistant-dock.js +97 -4
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/workspace-view.d.ts +2 -0
- package/dist-client/components/workspace-view.js +58 -3
- package/dist-client/components/workspace-view.js.map +1 -1
- package/dist-client/index.d.ts +2 -0
- package/dist-client/index.js +3 -0
- package/dist-client/index.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/assistant-ask.d.ts +22 -1
- package/dist-client/utils/assistant-ask.js +19 -0
- package/dist-client/utils/assistant-ask.js.map +1 -1
- package/dist-client/utils/assistant-ask.test.d.ts +1 -0
- package/dist-client/utils/assistant-ask.test.js +40 -0
- package/dist-client/utils/assistant-ask.test.js.map +1 -0
- package/dist-client/utils/assistant-proposals.d.ts +14 -0
- package/dist-client/utils/assistant-proposals.js +7 -0
- package/dist-client/utils/assistant-proposals.js.map +1 -1
- package/dist-client/utils/assistant-selection.d.ts +19 -0
- package/dist-client/utils/assistant-selection.js +35 -0
- package/dist-client/utils/assistant-selection.js.map +1 -0
- package/dist-client/utils/assistant-selection.test.d.ts +1 -0
- package/dist-client/utils/assistant-selection.test.js +26 -0
- package/dist-client/utils/assistant-selection.test.js.map +1 -0
- package/dist-client/utils/assistant-session-controller.d.ts +2 -0
- package/dist-client/utils/assistant-session-controller.js.map +1 -1
- package/dist-client/utils/assistant-session-transport.js +1 -1
- package/dist-client/utils/assistant-session-transport.js.map +1 -1
- package/dist-client/utils/invite-proposal-register.d.ts +1 -0
- package/dist-client/utils/invite-proposal-register.js +24 -0
- package/dist-client/utils/invite-proposal-register.js.map +1 -0
- package/dist-client/utils/invite-proposal.d.ts +17 -0
- package/dist-client/utils/invite-proposal.js +37 -0
- package/dist-client/utils/invite-proposal.js.map +1 -0
- package/dist-client/utils/invite-proposal.test.d.ts +1 -0
- package/dist-client/utils/invite-proposal.test.js +35 -0
- package/dist-client/utils/invite-proposal.test.js.map +1 -0
- package/dist-server/service/assistant-chat-resolver.js +2 -0
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/index.d.ts +2 -1
- package/dist-server/service/assistant-mode/index.js +6 -1
- package/dist-server/service/assistant-mode/index.js.map +1 -1
- package/dist-server/service/assistant-mode/mode-categories.d.ts +14 -1
- package/dist-server/service/assistant-mode/mode-categories.js +29 -4
- package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
- package/dist-server/service/assistant-mode/screen-rules.d.ts +6 -0
- package/dist-server/service/assistant-mode/screen-rules.js +21 -6
- package/dist-server/service/assistant-mode/screen-rules.js.map +1 -1
- package/dist-server/service/data-read/read-target.d.ts +24 -3
- package/dist-server/service/data-read/read-target.js +42 -10
- package/dist-server/service/data-read/read-target.js.map +1 -1
- package/dist-server/service/data-read/read-tools.d.ts +18 -1
- package/dist-server/service/data-read/read-tools.js +19 -5
- package/dist-server/service/data-read/read-tools.js.map +1 -1
- package/dist-server/service/index.d.ts +5 -1
- package/dist-server/service/index.js +10 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/people/index.d.ts +4 -0
- package/dist-server/service/people/index.js +22 -0
- package/dist-server/service/people/index.js.map +1 -0
- package/dist-server/service/people/invite-tools.d.ts +34 -0
- package/dist-server/service/people/invite-tools.js +144 -0
- package/dist-server/service/people/invite-tools.js.map +1 -0
- package/dist-server/service/people/people-keys.d.ts +8 -0
- package/dist-server/service/people/people-keys.js +16 -0
- package/dist-server/service/people/people-keys.js.map +1 -0
- package/dist-server/service/people/people-read.d.ts +4 -0
- package/dist-server/service/people/people-read.js +57 -0
- package/dist-server/service/people/people-read.js.map +1 -0
- package/dist-server/service/testing/mode-conformance.js +3 -0
- package/dist-server/service/testing/mode-conformance.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/server/service/assistant-chat-resolver.ts +2 -0
- package/server/service/assistant-mode/conversation-category.test.ts +35 -0
- package/server/service/assistant-mode/index.ts +3 -1
- package/server/service/assistant-mode/mode-categories.ts +41 -4
- package/server/service/assistant-mode/mode-front.test.ts +5 -3
- package/server/service/assistant-mode/resolve.test.ts +1 -1
- package/server/service/assistant-mode/screen-rules.test.ts +23 -1
- package/server/service/assistant-mode/screen-rules.ts +22 -5
- package/server/service/data-entry/data-entry.test.ts +2 -2
- package/server/service/data-read/detail-lists.test.ts +61 -0
- package/server/service/data-read/read-target.ts +54 -9
- package/server/service/data-read/read-tools.ts +32 -6
- package/server/service/index.ts +7 -1
- package/server/service/people/index.ts +13 -0
- package/server/service/people/invite-tools.ts +159 -0
- package/server/service/people/invite.test.ts +159 -0
- package/server/service/people/people-keys.ts +16 -0
- package/server/service/people/people-read.test.ts +82 -0
- package/server/service/people/people-read.ts +56 -0
- package/server/service/testing/mode-conformance.test.ts +14 -1
- package/server/service/testing/mode-conformance.ts +2 -0
- package/test/assistant-dock.test.ts +73 -0
- package/translations/en.json +18 -1
- package/translations/es.json +18 -1
- package/translations/ja.json +18 -1
- package/translations/ko.json +19 -2
- package/translations/ms.json +18 -1
- package/translations/zh.json +18 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.schema = exports.entities = exports.detailCategoryOf = exports.registerDetailRead = exports.readCategoryOf = exports.registerReadTarget = exports.MAX_CHANGES = exports.proposeChanges = exports.runAsCaller = exports.entryCategoryOf = exports.registerEntryTarget = exports.requestTranslator = void 0;
|
|
3
|
+
exports.schema = exports.entities = exports.assertMayWriteSession = exports.PROPOSAL_OUTCOME_PREFIX = exports.runDetail = exports.detailCategoryOf = exports.registerDetailRead = exports.readCategoryOf = exports.registerReadTarget = exports.MAX_CHANGES = exports.proposeChanges = exports.runAsCaller = exports.entryCategoryOf = exports.registerEntryTarget = exports.requestTranslator = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const chat_session_resolver_js_1 = require("./chat-session-resolver.js");
|
|
6
6
|
const assistant_chat_resolver_js_1 = require("./assistant-chat-resolver.js");
|
|
@@ -18,6 +18,8 @@ require("./chat-session/session-purge.js");
|
|
|
18
18
|
require("./chat-session/attachment-route.js");
|
|
19
19
|
const entry_resolver_js_1 = require("./data-entry/entry-resolver.js");
|
|
20
20
|
require("./workspace/workspace-tools.js");
|
|
21
|
+
/* Who works here, and the card that brings one of them into a conversation - every conversation carries both. */
|
|
22
|
+
require("./people/index.js");
|
|
21
23
|
const index_js_4 = require("./unattended-run/index.js");
|
|
22
24
|
tslib_1.__exportStar(require("./types.js"), exports);
|
|
23
25
|
tslib_1.__exportStar(require("./chat-session-resolver.js"), exports);
|
|
@@ -49,7 +51,9 @@ Object.defineProperty(exports, "registerReadTarget", { enumerable: true, get: fu
|
|
|
49
51
|
Object.defineProperty(exports, "readCategoryOf", { enumerable: true, get: function () { return read_tools_js_1.readCategoryOf; } });
|
|
50
52
|
Object.defineProperty(exports, "registerDetailRead", { enumerable: true, get: function () { return read_tools_js_1.registerDetailRead; } });
|
|
51
53
|
Object.defineProperty(exports, "detailCategoryOf", { enumerable: true, get: function () { return read_tools_js_1.detailCategoryOf; } });
|
|
54
|
+
Object.defineProperty(exports, "runDetail", { enumerable: true, get: function () { return read_tools_js_1.runDetail; } });
|
|
52
55
|
tslib_1.__exportStar(require("./data-read/list-read.js"), exports);
|
|
56
|
+
tslib_1.__exportStar(require("./people/index.js"), exports);
|
|
53
57
|
/* The AI 작업판 - large results on a page of their own (design/ai/workspace.md). */
|
|
54
58
|
tslib_1.__exportStar(require("./workspace/workspace-blocks.js"), exports);
|
|
55
59
|
/* 무엇이 근거인가 — 도구 결과 밖의 셋(프롬프트 · 대화 · 화면 스냅숏). */
|
|
@@ -78,6 +82,11 @@ tslib_1.__exportStar(require("./chat-session-participant/mention-invite.js"), ex
|
|
|
78
82
|
tslib_1.__exportStar(require("./chat-session-participant/presence.js"), exports);
|
|
79
83
|
tslib_1.__exportStar(require("./chat-message/index.js"), exports);
|
|
80
84
|
tslib_1.__exportStar(require("./chat-session-participant/index.js"), exports);
|
|
85
|
+
/* Reading the stored proposals of a conversation and whether one is answered - a package that applies its own card reads them as entry does. */
|
|
86
|
+
var proposal_outcome_js_1 = require("./chat-message/proposal-outcome.js");
|
|
87
|
+
Object.defineProperty(exports, "PROPOSAL_OUTCOME_PREFIX", { enumerable: true, get: function () { return proposal_outcome_js_1.PROPOSAL_OUTCOME_PREFIX; } });
|
|
88
|
+
var session_write_gate_js_1 = require("./chat-session/session-write-gate.js");
|
|
89
|
+
Object.defineProperty(exports, "assertMayWriteSession", { enumerable: true, get: function () { return session_write_gate_js_1.assertMayWriteSession; } });
|
|
81
90
|
/* A mode run with no person in it - a scheduled draft (operato-application/design/ai/unattended-run.md). */
|
|
82
91
|
tslib_1.__exportStar(require("./unattended-run/index.js"), exports);
|
|
83
92
|
exports.entities = [...index_js_1.entities, ...index_js_2.entities, ...index_js_3.entities, ...index_js_4.entities];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,yEAAgE;AAChE,6EAAoE;AACpE,4FAAmF;AACnF,yFAAgF;AAChF,iEAAwD;AACxD,sGAA+F;AAC/F,8FAAqF;AACrF,4FAAmF;AACnF,sDAAyE;AACzE,sDAAyE;AACzE,kEAAgG;AAChG,8CAA2C;AAC3C,2CAAwC;AACxC,8CAA2C;AAC3C,sEAAsE;AACtE,0CAAuC;AACvC,wDAA6E;AAE7E,qDAA0B;AAC1B,qEAA0C;AAC1C,uEAA4C;AAC5C,oGAAoG;AACpG,oEAAyC;AACzC,iEAA2D;AAAlD,0HAAA,iBAAiB,OAAA;AAC1B,6EAAkD;AAClD;;;GAGG;AACH,2DAAgC;AAChC,+GAA+G;AAC/G,uEAA4C;AAC5C,8DAA+F;AAAtF,qHAAA,mBAAmB,OAAA;AAAE,iHAAA,eAAe,OAAA;AAAE,6GAAA,WAAW,OAAA;AAC1D,yHAAyH;AACzH,gEAA0E;AAAjE,iHAAA,cAAc,OAAA;AAAE,8GAAA,WAAW,OAAA;AACpC,uHAAuH;AACvH,qEAA0C;AAC1C,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,yEAAgE;AAChE,6EAAoE;AACpE,4FAAmF;AACnF,yFAAgF;AAChF,iEAAwD;AACxD,sGAA+F;AAC/F,8FAAqF;AACrF,4FAAmF;AACnF,sDAAyE;AACzE,sDAAyE;AACzE,kEAAgG;AAChG,8CAA2C;AAC3C,2CAAwC;AACxC,8CAA2C;AAC3C,sEAAsE;AACtE,0CAAuC;AACvC,iHAAiH;AACjH,6BAA0B;AAC1B,wDAA6E;AAE7E,qDAA0B;AAC1B,qEAA0C;AAC1C,uEAA4C;AAC5C,oGAAoG;AACpG,oEAAyC;AACzC,iEAA2D;AAAlD,0HAAA,iBAAiB,OAAA;AAC1B,6EAAkD;AAClD;;;GAGG;AACH,2DAAgC;AAChC,+GAA+G;AAC/G,uEAA4C;AAC5C,8DAA+F;AAAtF,qHAAA,mBAAmB,OAAA;AAAE,iHAAA,eAAe,OAAA;AAAE,6GAAA,WAAW,OAAA;AAC1D,yHAAyH;AACzH,gEAA0E;AAAjE,iHAAA,cAAc,OAAA;AAAE,8GAAA,WAAW,OAAA;AACpC,uHAAuH;AACvH,qEAA0C;AAC1C,2DAA+I;AAAtI,mHAAA,kBAAkB,OAAA;AAAE,+GAAA,cAAc,OAAA;AAAE,mHAAA,kBAAkB,OAAA;AAAE,iHAAA,gBAAgB,OAAA;AAAE,0GAAA,SAAS,OAAA;AAC5F,mEAAwC;AACxC,4DAAiC;AACjC,iFAAiF;AACjF,0EAA+C;AAC/C,gDAAgD;AAChD,iEAAsC;AACtC,sDAAsD;AACtD,2EAAgD;AAChD,gHAAgH;AAChH,+EAAoD;AACpD,kEAAuC;AACvC,8DAA8D;AAC9D,0EAA+C;AAC/C,8EAAmD;AACnD,0EAA+C;AAC/C,qFAA0D;AAC1D,uFAA4D;AAC5D,wEAA6C;AAC7C,4EAAiD;AACjD,yEAA8C;AAC9C,wDAAwD;AACxD,iFAAsD;AACtD,iFAAsD;AACtD,mFAAwD;AACxD,4FAAiE;AACjE,2DAA2D;AAC3D,uFAA4D;AAC5D,iFAAsD;AACtD,kEAAuC;AACvC,8EAAmD;AACnD,gJAAgJ;AAChJ,0EAA4E;AAAnE,8HAAA,uBAAuB,OAAA;AAChC,8EAA4E;AAAnE,8HAAA,qBAAqB,OAAA;AAC9B,4GAA4G;AAC5G,oEAAyC;AAE5B,QAAA,QAAQ,GAAG,CAAC,GAAG,mBAAmB,EAAE,GAAG,mBAAmB,EAAE,GAAG,mBAA8B,EAAE,GAAG,mBAAqB,CAAC,CAAA;AAExH,QAAA,MAAM,GAAG;IACpB,eAAe,EAAE;QACf,8CAAmB;QACnB,sDAAsD;QACtD,kDAAqB;QACrB,uDAAuD;QACvD,kDAAqB;QACrB,wDAAwD;QACxD,8DAA2B;QAC3B,mGAAmG;QACnG,sCAAe;QACf,sDAAuB;QACvB,wDAAwD;QACxD,oDAAsB;QACtB,qDAAqD;QACrD,gEAA6B;QAC7B,mDAAmD;QACnD,yCAAqB;KACtB;CACF,CAAA","sourcesContent":["import { ChatSessionResolver } from './chat-session-resolver.js'\nimport { AssistantChatResolver } from './assistant-chat-resolver.js'\nimport { AssistantModeResolver } from './assistant-mode/assistant-mode-resolver.js'\nimport { AssistantCapabilityResolver } from './assistant-capability-resolver.js'\nimport { AiAgentResolver } from './ai-agent-resolver.js'\nimport { MySessionActivitySubscription } from './chat-session/session-activity-subscription.js'\nimport { ChatMessageSubscription } from './chat-message/chat-message-subscription.js'\nimport { ChatTypingSubscription } from './chat-message/chat-typing-subscription.js'\nimport { entities as ChatSessionEntities } from './chat-session/index.js'\nimport { entities as ChatMessageEntities } from './chat-message/index.js'\nimport { entities as ChatSessionParticipantEntities } from './chat-session-participant/index.js'\nimport './chat-session/attachment-tools.js'\nimport './chat-session/session-purge.js'\nimport './chat-session/attachment-route.js'\nimport { EntryProposalResolver } from './data-entry/entry-resolver.js'\nimport './workspace/workspace-tools.js'\n/* Who works here, and the card that brings one of them into a conversation - every conversation carries both. */\nimport './people/index.js'\nimport { entities as UnattendedRunEntities } from './unattended-run/index.js'\n\nexport * from './types.js'\nexport * from './chat-session-resolver.js'\nexport * from './assistant-chat-resolver.js'\n/* The mode registry — each app server declares what its conversations are (ADR-0061 decision 7). */\nexport * from './assistant-mode/index.js'\nexport { requestTranslator } from './request-translator.js'\nexport * from './assistant-capability-resolver.js'\n/*\n * 대화 각본 하네스는 **런타임 export 에서 뺀다**(ADR-0070 A15 결정 ④) — 시험 도구가 서버 번들에\n * 실린다. 시험은 경로로 가져온다: `…/service/testing/conversation-script.js`.\n */\nexport * from './tool-access.js'\n/* Data entry from material - an app registers what it can enter; a mode names it (design/ai/data-entry.md). */\nexport * from './data-entry/entry-target.js'\nexport { registerEntryTarget, entryCategoryOf, runAsCaller } from './data-entry/entry-tools.js'\n/* An application's tool that prepares many changes at once makes the same change card the model's would (2026-10-24). */\nexport { proposeChanges, MAX_CHANGES } from './data-entry/entry-update.js'\n/* Reading an application's lists - an app registers what it lets read; a mode names it (design/ai/read-targets.md). */\nexport * from './data-read/read-target.js'\nexport { registerReadTarget, readCategoryOf, registerDetailRead, detailCategoryOf, runDetail, type DetailRun } from './data-read/read-tools.js'\nexport * from './data-read/list-read.js'\nexport * from './people/index.js'\n/* The AI 작업판 - large results on a page of their own (design/ai/workspace.md). */\nexport * from './workspace/workspace-blocks.js'\n/* 무엇이 근거인가 — 도구 결과 밖의 셋(프롬프트 · 대화 · 화면 스냅숏). */\nexport * from './grounding-sources.js'\n/* 앵커 순수 로직 — 호스트가 앵커 종류를 참조한다(리졸버는 데코레이터 때문에 무겁다). */\nexport * from './chat-session/session-anchor.js'\n/* Who may read a conversation — one rule, also asked by doors outside this package (board-ai's revertPatch). */\nexport * from './chat-session/session-visibility.js'\nexport * from './chat-session/index.js'\n/* 순수 로직들 — 호스트(board-ai 등)가 같은 규칙을 쓰라고 공개한다. 베끼면 한쪽만 고쳐진다. */\nexport * from './chat-session/session-inbox.js'\nexport * from './chat-session/attachment-policy.js'\nexport * from './chat-session/session-purge.js'\nexport * from './chat-session/session-activity-publish.js'\nexport * from './chat-session/session-activity-broadcast.js'\nexport * from './chat-message/llm-history.js'\nexport * from './chat-message/history-summary.js'\nexport * from './chat-message/fold-history.js'\n/* 상한 밖으로 밀려난 앞부분을 요약으로 접는 orchestration — 중립 문이 부른다. */\nexport * from './chat-message/fold-session-history.js'\nexport * from './chat-message/chat-message-publish.js'\nexport * from './chat-message/chat-message-broadcast.js'\nexport * from './chat-session-participant/invite-notification.js'\n/* 멘션이 사람을 불러들이는 자리 — 사람의 말을 저장하는 문만 부른다(ADR-0071 결정 1). */\nexport * from './chat-session-participant/mention-invite.js'\nexport * from './chat-session-participant/presence.js'\nexport * from './chat-message/index.js'\nexport * from './chat-session-participant/index.js'\n/* Reading the stored proposals of a conversation and whether one is answered - a package that applies its own card reads them as entry does. */\nexport { PROPOSAL_OUTCOME_PREFIX } from './chat-message/proposal-outcome.js'\nexport { assertMayWriteSession } from './chat-session/session-write-gate.js'\n/* A mode run with no person in it - a scheduled draft (operato-application/design/ai/unattended-run.md). */\nexport * from './unattended-run/index.js'\n\nexport const entities = [...ChatSessionEntities, ...ChatMessageEntities, ...ChatSessionParticipantEntities, ...UnattendedRunEntities]\n\nexport const schema = {\n resolverClasses: [\n ChatSessionResolver,\n /* 문서를 갖지 않은 표면의 대화 — board 를 안 싣는 제품이 물어볼 수 있는 자리. */\n AssistantChatResolver,\n /* 화면이 자기 대화가 무엇을 주는지 묻는 자리 — 문구는 서버가 요청 로케일로 옮겨 낸다. */\n AssistantModeResolver,\n /* 「이 배포에 AI 가 있나 · 무엇이 선언 없이 등록됐나」 — 창구 하나(표준 A7-a). */\n AssistantCapabilityResolver,\n /* A domain's AI agents - the settings card and the dock's picker (design/ai/agent-settings.md). */\n AiAgentResolver,\n ChatMessageSubscription,\n /* 「입력 중」 — 저장하지 않는 방의 한 줄. 듣는 사람은 메시지와 같은 방 판정을 지난다. */\n ChatTypingSubscription,\n /* 내가 참여한 모든 세션의 활동 — 창을 닫아 둔 표면이 배지·목록을 갱신할 수 있게. */\n MySessionActivitySubscription,\n /* 사람이 누른 입력 제안을 쓰는 자리 — 행마다 앱의 코드로, 그 사람의 권한으로. */\n EntryProposalResolver\n ]\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_INVITE_CANDIDATES = exports.inviteCategory = exports.findPeopleNamed = exports.proposeInvite = exports.PEOPLE_SELECTION = exports.peopleReadTarget = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
/*
|
|
6
|
+
* The people tools every conversation carries - the people list (people-read.ts) and the invite card (invite-tools.ts).
|
|
7
|
+
* Registered once, at boot, as the AI 작업판 is.
|
|
8
|
+
*/
|
|
9
|
+
const read_tools_js_1 = require("../data-read/read-tools.js");
|
|
10
|
+
const people_read_js_1 = require("./people-read.js");
|
|
11
|
+
require("./invite-tools.js");
|
|
12
|
+
(0, read_tools_js_1.registerReadTarget)((0, people_read_js_1.peopleReadTarget)());
|
|
13
|
+
tslib_1.__exportStar(require("./people-keys.js"), exports);
|
|
14
|
+
var people_read_js_2 = require("./people-read.js");
|
|
15
|
+
Object.defineProperty(exports, "peopleReadTarget", { enumerable: true, get: function () { return people_read_js_2.peopleReadTarget; } });
|
|
16
|
+
Object.defineProperty(exports, "PEOPLE_SELECTION", { enumerable: true, get: function () { return people_read_js_2.PEOPLE_SELECTION; } });
|
|
17
|
+
var invite_tools_js_1 = require("./invite-tools.js");
|
|
18
|
+
Object.defineProperty(exports, "proposeInvite", { enumerable: true, get: function () { return invite_tools_js_1.proposeInvite; } });
|
|
19
|
+
Object.defineProperty(exports, "findPeopleNamed", { enumerable: true, get: function () { return invite_tools_js_1.findPeopleNamed; } });
|
|
20
|
+
Object.defineProperty(exports, "inviteCategory", { enumerable: true, get: function () { return invite_tools_js_1.inviteCategory; } });
|
|
21
|
+
Object.defineProperty(exports, "MAX_INVITE_CANDIDATES", { enumerable: true, get: function () { return invite_tools_js_1.MAX_INVITE_CANDIDATES; } });
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/people/index.ts"],"names":[],"mappings":";;;;AAAA;;;GAGG;AACH,8DAA+D;AAC/D,qDAAmD;AACnD,6BAA0B;AAE1B,IAAA,kCAAkB,EAAC,IAAA,iCAAgB,GAAE,CAAC,CAAA;AAEtC,2DAAgC;AAChC,mDAAqE;AAA5D,kHAAA,gBAAgB,OAAA;AAAE,kHAAA,gBAAgB,OAAA;AAC3C,qDAA+H;AAAtH,gHAAA,aAAa,OAAA;AAAE,kHAAA,eAAe,OAAA;AAAE,iHAAA,cAAc,OAAA;AAAE,wHAAA,qBAAqB,OAAA","sourcesContent":["/*\n * The people tools every conversation carries - the people list (people-read.ts) and the invite card (invite-tools.ts).\n * Registered once, at boot, as the AI 작업판 is.\n */\nimport { registerReadTarget } from '../data-read/read-tools.js'\nimport { peopleReadTarget } from './people-read.js'\nimport './invite-tools.js'\n\nregisterReadTarget(peopleReadTarget())\n\nexport * from './people-keys.js'\nexport { peopleReadTarget, PEOPLE_SELECTION } from './people-read.js'\nexport { proposeInvite, findPeopleNamed, inviteCategory, MAX_INVITE_CANDIDATES, type InviteCandidate } from './invite-tools.js'\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ToolCategory } from '@things-factory/ai-client-base';
|
|
2
|
+
import type { GraphqlRunner } from '../data-entry/entry-update.js';
|
|
3
|
+
/** At most this many people on one card - a name that matches more is too broad to pick from. */
|
|
4
|
+
export declare const MAX_INVITE_CANDIDATES = 8;
|
|
5
|
+
export interface InviteCandidate {
|
|
6
|
+
userId: string;
|
|
7
|
+
name: string;
|
|
8
|
+
department?: string;
|
|
9
|
+
position?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The people a name means, as the person may see them: the exact name first, then the name as a search term. Only
|
|
13
|
+
* people with an account - an invite seats a user.
|
|
14
|
+
*/
|
|
15
|
+
export declare function findPeopleNamed(run: GraphqlRunner, name: string): Promise<{
|
|
16
|
+
people: InviteCandidate[];
|
|
17
|
+
error?: string;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function proposeInvite(args: any, context: any, run?: GraphqlRunner): Promise<{
|
|
20
|
+
rejected: boolean;
|
|
21
|
+
error: string;
|
|
22
|
+
message: string;
|
|
23
|
+
} | {
|
|
24
|
+
people: InviteCandidate[];
|
|
25
|
+
message: string;
|
|
26
|
+
reason?: string;
|
|
27
|
+
proposed: boolean;
|
|
28
|
+
kind: string;
|
|
29
|
+
icon: string;
|
|
30
|
+
label: any;
|
|
31
|
+
rejected?: undefined;
|
|
32
|
+
error?: undefined;
|
|
33
|
+
}>;
|
|
34
|
+
export declare const inviteCategory: ToolCategory;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inviteCategory = exports.MAX_INVITE_CANDIDATES = void 0;
|
|
4
|
+
exports.findPeopleNamed = findPeopleNamed;
|
|
5
|
+
exports.proposeInvite = proposeInvite;
|
|
6
|
+
/*
|
|
7
|
+
* proposeInvite - the model offers a card that brings someone into this conversation (ADR-0073 decision 3).
|
|
8
|
+
*
|
|
9
|
+
* The card is a proposal: the model seats nobody. What an assistant writes must not seat people (mention-invite.ts,
|
|
10
|
+
* rule ①), so the seat is given when a person presses the card - the dock calls `inviteToAISession` with that
|
|
11
|
+
* person's request, and the door asks `maySeat` with their seat. This tool only finds who is meant and says whether
|
|
12
|
+
* this person could press it here.
|
|
13
|
+
*
|
|
14
|
+
* Who is meant is the server's to settle, never the model's memory: the name is looked up in the organization's own
|
|
15
|
+
* list as the person (the read people-read.ts opens), so a user id the model made up is never on a card. One person
|
|
16
|
+
* by that name - the card invites them. Several - the card lists each and the person picks one; the model does not ask.
|
|
17
|
+
*/
|
|
18
|
+
const ai_client_base_1 = require("@things-factory/ai-client-base");
|
|
19
|
+
const shell_1 = require("@things-factory/shell");
|
|
20
|
+
const entry_tools_js_1 = require("../data-entry/entry-tools.js");
|
|
21
|
+
const chat_session_js_1 = require("../chat-session/chat-session.js");
|
|
22
|
+
const session_visibility_js_1 = require("../chat-session/session-visibility.js");
|
|
23
|
+
const chat_session_participant_js_1 = require("../chat-session-participant/chat-session-participant.js");
|
|
24
|
+
const participant_lookup_roles_js_1 = require("../chat-session-participant/participant-lookup-roles.js");
|
|
25
|
+
const people_read_js_1 = require("./people-read.js");
|
|
26
|
+
const people_keys_js_1 = require("./people-keys.js");
|
|
27
|
+
/** At most this many people on one card - a name that matches more is too broad to pick from. */
|
|
28
|
+
exports.MAX_INVITE_CANDIDATES = 8;
|
|
29
|
+
const PEOPLE_QUERY = 'query ($filters: [Filter!], $pagination: Pagination, $search: String) ' +
|
|
30
|
+
`{ employees(filters: $filters, pagination: $pagination, search: $search) { items { ${people_read_js_1.PEOPLE_SELECTION} } total } }`;
|
|
31
|
+
const ACTIVE = { name: 'active', operator: 'eq', value: true };
|
|
32
|
+
const candidateOf = (row) => row?.user?.id
|
|
33
|
+
? {
|
|
34
|
+
userId: String(row.user.id),
|
|
35
|
+
name: String(row.name ?? ''),
|
|
36
|
+
...(row.department?.name ? { department: String(row.department.name) } : {}),
|
|
37
|
+
...(row.jobPosition ? { position: String(row.jobPosition) } : {})
|
|
38
|
+
}
|
|
39
|
+
: undefined;
|
|
40
|
+
/**
|
|
41
|
+
* The people a name means, as the person may see them: the exact name first, then the name as a search term. Only
|
|
42
|
+
* people with an account - an invite seats a user.
|
|
43
|
+
*/
|
|
44
|
+
async function findPeopleNamed(run, name) {
|
|
45
|
+
const ask = async (variables) => {
|
|
46
|
+
const answer = await run(PEOPLE_QUERY, { pagination: { page: 1, limit: exports.MAX_INVITE_CANDIDATES + 1 }, ...variables });
|
|
47
|
+
if (answer.errors?.length)
|
|
48
|
+
return { error: answer.errors.map(one => one.message).join('; ') };
|
|
49
|
+
return { rows: (answer.data?.employees?.items ?? []) };
|
|
50
|
+
};
|
|
51
|
+
const exact = await ask({ filters: [ACTIVE, { name: 'name', operator: 'eq', value: name }] });
|
|
52
|
+
if ('error' in exact)
|
|
53
|
+
return { people: [], error: exact.error };
|
|
54
|
+
const rows = exact.rows.length ? exact.rows : (await ask({ filters: [ACTIVE], search: name })).rows ?? [];
|
|
55
|
+
return { people: rows.map(candidateOf).filter(Boolean) };
|
|
56
|
+
}
|
|
57
|
+
/** Why this person cannot bring anyone into this room, in their words - or nothing when they can. */
|
|
58
|
+
function seatRefusal(visibility, t) {
|
|
59
|
+
if (visibility === 'private')
|
|
60
|
+
return t('ai-assistant.text.invite-private-refused', { defaultValue: 'a private conversation is yours alone. make it a group to bring people in.' });
|
|
61
|
+
return t('ai-assistant.text.invite-owner-only', { defaultValue: 'only the person who started this group conversation can bring people in.' });
|
|
62
|
+
}
|
|
63
|
+
async function proposeInvite(args, context, run = (0, entry_tools_js_1.runAsCaller)(context)) {
|
|
64
|
+
const t = (key, options) => (typeof context?.t === 'function' ? context.t(key, options) : String(options?.defaultValue ?? key));
|
|
65
|
+
const { domain, user } = context?.state ?? {};
|
|
66
|
+
const name = typeof args?.name === 'string' ? args.name.trim() : '';
|
|
67
|
+
const wanted = typeof args?.userId === 'string' ? args.userId.trim() : '';
|
|
68
|
+
if (!name)
|
|
69
|
+
return { rejected: true, error: 'no-name', message: 'say whom to invite - the name the person used.' };
|
|
70
|
+
const conversationId = context?.conversation?.id;
|
|
71
|
+
const session = conversationId && domain?.id ? await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).findOne({ where: { id: conversationId, domain: { id: domain.id } } }) : undefined;
|
|
72
|
+
if (!session)
|
|
73
|
+
return { rejected: true, error: 'no-conversation', message: 'this conversation is not stored yet, so nobody can be brought into it.' };
|
|
74
|
+
/* Asked before anyone is looked up: the card must be one this person can press here. */
|
|
75
|
+
const seats = await (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant).find({ where: { domain: { id: domain.id }, session: { id: session.id } } });
|
|
76
|
+
const mine = seats.find((seat) => seat.userId === user?.id)?.role;
|
|
77
|
+
if (!(0, session_visibility_js_1.maySeat)(session.visibility, mine)) {
|
|
78
|
+
/* Who reads the conversation is the person's to change, in the conversation list - the model has no tool for it (2026-10-24: it offered 「그룹 대화로 바꿔줘」). */
|
|
79
|
+
return {
|
|
80
|
+
rejected: true,
|
|
81
|
+
error: 'may-not-invite',
|
|
82
|
+
message: `${seatRefusal(session.visibility, t)} Say so in one line - do not offer a card. Who reads this conversation is changed by the person who started it, from the conversation list (⋯); you cannot change it, so never offer to or suggest asking you.`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const found = await findPeopleNamed(run, name);
|
|
86
|
+
if (found.error)
|
|
87
|
+
return { rejected: true, error: 'people-unreadable', message: `the people list could not be read: ${found.error}` };
|
|
88
|
+
const here = new Set(seats.filter((seat) => (0, participant_lookup_roles_js_1.isRoomRole)(seat.role)).map((seat) => seat.userId));
|
|
89
|
+
const already = found.people.filter(one => here.has(one.userId));
|
|
90
|
+
let people = found.people.filter(one => !here.has(one.userId) && one.userId !== user?.id);
|
|
91
|
+
if (wanted)
|
|
92
|
+
people = people.filter(one => one.userId === wanted);
|
|
93
|
+
if (!people.length) {
|
|
94
|
+
return already.length
|
|
95
|
+
? { rejected: true, error: 'already-here', message: `${already.map(one => one.name).join(', ')} is already in this conversation.` }
|
|
96
|
+
: { rejected: true, error: 'not-found', message: `no one named "${name}" with an account is in the people list this person may see.` };
|
|
97
|
+
}
|
|
98
|
+
if (people.length > exports.MAX_INVITE_CANDIDATES) {
|
|
99
|
+
return { rejected: true, error: 'too-many', message: `more than ${exports.MAX_INVITE_CANDIDATES} people match "${name}" - ask for the full name or the department.` };
|
|
100
|
+
}
|
|
101
|
+
const one = people.length === 1 ? people[0] : undefined;
|
|
102
|
+
return {
|
|
103
|
+
proposed: true,
|
|
104
|
+
kind: 'invite',
|
|
105
|
+
icon: 'person_add',
|
|
106
|
+
label: one
|
|
107
|
+
? t('ai-assistant.people.invite-one', { name: one.name, defaultValue: 'Invite {name} to this conversation' })
|
|
108
|
+
: t('ai-assistant.people.invite-pick', { name, count: people.length, defaultValue: '{count} people named {name} - pick whom to invite' }),
|
|
109
|
+
...(one && (one.department || one.position) ? { reason: [one.department, one.position].filter(Boolean).join(' · ') } : {}),
|
|
110
|
+
people,
|
|
111
|
+
message: one
|
|
112
|
+
? `the card invites ${one.name}${one.department ? ` (${one.department})` : ''}. The person presses it; they are told when they join. Say that in one line.`
|
|
113
|
+
: `${people.length} people match "${name}"; the card lists each with department and position and the person picks one. Do not ask which - the card does.`
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const spec = {
|
|
117
|
+
kind: 'read',
|
|
118
|
+
name: 'proposeInvite',
|
|
119
|
+
/* It reads people through the organization's list - the same door that list has, so it is offered where that list is. */
|
|
120
|
+
doors: ['Query.employees'],
|
|
121
|
+
description: 'Offer a card that brings someone into this conversation. Give the name the person used; the server finds them in ' +
|
|
122
|
+
"the organization's people list and puts them on the card (several people by that name: the card lists each, the " +
|
|
123
|
+
'person picks). The person presses the card - nobody joins until then. One call per person.',
|
|
124
|
+
schema: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
name: { type: 'string', description: 'The name of the person to invite, as the person said it.' },
|
|
128
|
+
userId: { type: 'string', description: 'The user id from the people list, when it was read already and the person meant that one.' }
|
|
129
|
+
},
|
|
130
|
+
required: ['name']
|
|
131
|
+
},
|
|
132
|
+
builder: (args, context) => proposeInvite(args, context)
|
|
133
|
+
};
|
|
134
|
+
exports.inviteCategory = {
|
|
135
|
+
name: people_keys_js_1.INVITE_CATEGORY,
|
|
136
|
+
description: 'bring someone into this conversation - a card the person presses',
|
|
137
|
+
guidance: [
|
|
138
|
+
'- When the person asks to invite, add or bring someone into this conversation, call proposeInvite with that name in the same turn. Never send them to a screen to do it.',
|
|
139
|
+
'- To answer who someone is or who works where, read the people list (queryOrganizationEmployee).'
|
|
140
|
+
].join('\n'),
|
|
141
|
+
specs: [spec]
|
|
142
|
+
};
|
|
143
|
+
(0, ai_client_base_1.registerToolCategory)(exports.inviteCategory);
|
|
144
|
+
//# sourceMappingURL=invite-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invite-tools.js","sourceRoot":"","sources":["../../../server/service/people/invite-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * proposeInvite - the model offers a card that brings someone into this conversation (ADR-0073 decision 3).\n *\n * The card is a proposal: the model seats nobody. What an assistant writes must not seat people (mention-invite.ts,\n * rule ①), so the seat is given when a person presses the card - the dock calls `inviteToAISession` with that\n * person's request, and the door asks `maySeat` with their seat. This tool only finds who is meant and says whether\n * this person could press it here.\n *\n * Who is meant is the server's to settle, never the model's memory: the name is looked up in the organization's own\n * list as the person (the read people-read.ts opens), so a user id the model made up is never on a card. One person\n * by that name - the card invites them. Several - the card lists each and the person picks one; the model does not ask.\n */\nimport { registerToolCategory, type ToolCategory, type ToolSpec } from '@things-factory/ai-client-base'\nimport { getRepository } from '@things-factory/shell'\n\nimport { runAsCaller } from '../data-entry/entry-tools.js'\nimport type { GraphqlRunner } from '../data-entry/entry-update.js'\nimport { ChatSession } from '../chat-session/chat-session.js'\nimport { maySeat } from '../chat-session/session-visibility.js'\nimport { ChatSessionParticipant } from '../chat-session-participant/chat-session-participant.js'\nimport { isRoomRole } from '../chat-session-participant/participant-lookup-roles.js'\nimport { PEOPLE_SELECTION } from './people-read.js'\nimport { INVITE_CATEGORY } from './people-keys.js'\n\n/** At most this many people on one card - a name that matches more is too broad to pick from. */\nexport const MAX_INVITE_CANDIDATES = 8\n\nexport interface InviteCandidate {\n userId: string\n name: string\n department?: string\n position?: string\n}\n\nconst PEOPLE_QUERY =\n 'query ($filters: [Filter!], $pagination: Pagination, $search: String) ' +\n `{ employees(filters: $filters, pagination: $pagination, search: $search) { items { ${PEOPLE_SELECTION} } total } }`\n\nconst ACTIVE = { name: 'active', operator: 'eq', value: true }\n\nconst candidateOf = (row: any): InviteCandidate | undefined =>\n row?.user?.id\n ? {\n userId: String(row.user.id),\n name: String(row.name ?? ''),\n ...(row.department?.name ? { department: String(row.department.name) } : {}),\n ...(row.jobPosition ? { position: String(row.jobPosition) } : {})\n }\n : undefined\n\n/**\n * The people a name means, as the person may see them: the exact name first, then the name as a search term. Only\n * people with an account - an invite seats a user.\n */\nexport async function findPeopleNamed(run: GraphqlRunner, name: string): Promise<{ people: InviteCandidate[]; error?: string }> {\n const ask = async (variables: Record<string, unknown>) => {\n const answer = await run(PEOPLE_QUERY, { pagination: { page: 1, limit: MAX_INVITE_CANDIDATES + 1 }, ...variables })\n if (answer.errors?.length) return { error: answer.errors.map(one => one.message).join('; ') }\n return { rows: (answer.data?.employees?.items ?? []) as any[] }\n }\n const exact = await ask({ filters: [ACTIVE, { name: 'name', operator: 'eq', value: name }] })\n if ('error' in exact) return { people: [], error: exact.error }\n const rows = exact.rows!.length ? exact.rows! : ((await ask({ filters: [ACTIVE], search: name })) as any).rows ?? []\n return { people: rows.map(candidateOf).filter(Boolean) as InviteCandidate[] }\n}\n\n/** Why this person cannot bring anyone into this room, in their words - or nothing when they can. */\nfunction seatRefusal(visibility: string | null | undefined, t: (key: string, options: any) => string): string | undefined {\n if (visibility === 'private')\n return t('ai-assistant.text.invite-private-refused', { defaultValue: 'a private conversation is yours alone. make it a group to bring people in.' })\n return t('ai-assistant.text.invite-owner-only', { defaultValue: 'only the person who started this group conversation can bring people in.' })\n}\n\nexport async function proposeInvite(args: any, context: any, run: GraphqlRunner = runAsCaller(context)) {\n const t = (key: string, options: any) => (typeof context?.t === 'function' ? context.t(key, options) : String(options?.defaultValue ?? key))\n const { domain, user } = context?.state ?? {}\n const name = typeof args?.name === 'string' ? args.name.trim() : ''\n const wanted = typeof args?.userId === 'string' ? args.userId.trim() : ''\n if (!name) return { rejected: true, error: 'no-name', message: 'say whom to invite - the name the person used.' }\n\n const conversationId = context?.conversation?.id\n const session: any = conversationId && domain?.id ? await getRepository(ChatSession).findOne({ where: { id: conversationId, domain: { id: domain.id } } as any }) : undefined\n if (!session) return { rejected: true, error: 'no-conversation', message: 'this conversation is not stored yet, so nobody can be brought into it.' }\n\n /* Asked before anyone is looked up: the card must be one this person can press here. */\n const seats = await getRepository(ChatSessionParticipant).find({ where: { domain: { id: domain.id }, session: { id: session.id } } as any })\n const mine = seats.find((seat: any) => seat.userId === user?.id)?.role\n if (!maySeat(session.visibility, mine)) {\n /* Who reads the conversation is the person's to change, in the conversation list - the model has no tool for it (2026-10-24: it offered 「그룹 대화로 바꿔줘」). */\n return {\n rejected: true,\n error: 'may-not-invite',\n message: `${seatRefusal(session.visibility, t)} Say so in one line - do not offer a card. Who reads this conversation is changed by the person who started it, from the conversation list (⋯); you cannot change it, so never offer to or suggest asking you.`\n }\n }\n\n const found = await findPeopleNamed(run, name)\n if (found.error) return { rejected: true, error: 'people-unreadable', message: `the people list could not be read: ${found.error}` }\n const here = new Set(seats.filter((seat: any) => isRoomRole(seat.role)).map((seat: any) => seat.userId))\n const already = found.people.filter(one => here.has(one.userId))\n let people = found.people.filter(one => !here.has(one.userId) && one.userId !== user?.id)\n if (wanted) people = people.filter(one => one.userId === wanted)\n\n if (!people.length) {\n return already.length\n ? { rejected: true, error: 'already-here', message: `${already.map(one => one.name).join(', ')} is already in this conversation.` }\n : { rejected: true, error: 'not-found', message: `no one named \"${name}\" with an account is in the people list this person may see.` }\n }\n if (people.length > MAX_INVITE_CANDIDATES) {\n return { rejected: true, error: 'too-many', message: `more than ${MAX_INVITE_CANDIDATES} people match \"${name}\" - ask for the full name or the department.` }\n }\n\n const one = people.length === 1 ? people[0] : undefined\n return {\n proposed: true,\n kind: 'invite',\n icon: 'person_add',\n label: one\n ? t('ai-assistant.people.invite-one', { name: one.name, defaultValue: 'Invite {name} to this conversation' })\n : t('ai-assistant.people.invite-pick', { name, count: people.length, defaultValue: '{count} people named {name} - pick whom to invite' }),\n ...(one && (one.department || one.position) ? { reason: [one.department, one.position].filter(Boolean).join(' · ') } : {}),\n people,\n message: one\n ? `the card invites ${one.name}${one.department ? ` (${one.department})` : ''}. The person presses it; they are told when they join. Say that in one line.`\n : `${people.length} people match \"${name}\"; the card lists each with department and position and the person picks one. Do not ask which - the card does.`\n }\n}\n\nconst spec = {\n kind: 'read',\n name: 'proposeInvite',\n /* It reads people through the organization's list - the same door that list has, so it is offered where that list is. */\n doors: ['Query.employees'],\n description:\n 'Offer a card that brings someone into this conversation. Give the name the person used; the server finds them in ' +\n \"the organization's people list and puts them on the card (several people by that name: the card lists each, the \" +\n 'person picks). The person presses the card - nobody joins until then. One call per person.',\n schema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'The name of the person to invite, as the person said it.' },\n userId: { type: 'string', description: 'The user id from the people list, when it was read already and the person meant that one.' }\n },\n required: ['name']\n },\n builder: (args: any, context: any) => proposeInvite(args, context)\n} as ToolSpec\n\nexport const inviteCategory: ToolCategory = {\n name: INVITE_CATEGORY,\n description: 'bring someone into this conversation - a card the person presses',\n guidance: [\n '- When the person asks to invite, add or bring someone into this conversation, call proposeInvite with that name in the same turn. Never send them to a screen to do it.',\n '- To answer who someone is or who works where, read the people list (queryOrganizationEmployee).'\n ].join('\\n'),\n specs: [spec]\n}\n\nregisterToolCategory(inviteCategory)\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The organization's people as a read target - its `employees` list, read as the person (people-read.ts). */
|
|
2
|
+
export declare const PEOPLE_READ_KEY = "organization.employee";
|
|
3
|
+
/** The category of that read - `data-read:<key>`, as every read target's. */
|
|
4
|
+
export declare const PEOPLE_READ_CATEGORY = "data-read:organization.employee";
|
|
5
|
+
/** The invite card - bring someone into this conversation (invite-tools.ts). */
|
|
6
|
+
export declare const INVITE_CATEGORY = "chat-invite";
|
|
7
|
+
/** Both, in the order a turn puts them in front: find the person, then the card. */
|
|
8
|
+
export declare const PEOPLE_CATEGORIES: string[];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* The names of the people tools - every conversation carries them (mode-categories.ts), so they live apart from the
|
|
4
|
+
* tools themselves and the categories can be named without loading them.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.PEOPLE_CATEGORIES = exports.INVITE_CATEGORY = exports.PEOPLE_READ_CATEGORY = exports.PEOPLE_READ_KEY = void 0;
|
|
8
|
+
/** The organization's people as a read target - its `employees` list, read as the person (people-read.ts). */
|
|
9
|
+
exports.PEOPLE_READ_KEY = 'organization.employee';
|
|
10
|
+
/** The category of that read - `data-read:<key>`, as every read target's. */
|
|
11
|
+
exports.PEOPLE_READ_CATEGORY = `data-read:${exports.PEOPLE_READ_KEY}`;
|
|
12
|
+
/** The invite card - bring someone into this conversation (invite-tools.ts). */
|
|
13
|
+
exports.INVITE_CATEGORY = 'chat-invite';
|
|
14
|
+
/** Both, in the order a turn puts them in front: find the person, then the card. */
|
|
15
|
+
exports.PEOPLE_CATEGORIES = [exports.PEOPLE_READ_CATEGORY, exports.INVITE_CATEGORY];
|
|
16
|
+
//# sourceMappingURL=people-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"people-keys.js","sourceRoot":"","sources":["../../../server/service/people/people-keys.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,8GAA8G;AACjG,QAAA,eAAe,GAAG,uBAAuB,CAAA;AAEtD,6EAA6E;AAChE,QAAA,oBAAoB,GAAG,aAAa,uBAAe,EAAE,CAAA;AAElE,gFAAgF;AACnE,QAAA,eAAe,GAAG,aAAa,CAAA;AAE5C,oFAAoF;AACvE,QAAA,iBAAiB,GAAG,CAAC,4BAAoB,EAAE,uBAAe,CAAC,CAAA","sourcesContent":["/*\n * The names of the people tools - every conversation carries them (mode-categories.ts), so they live apart from the\n * tools themselves and the categories can be named without loading them.\n */\n\n/** The organization's people as a read target - its `employees` list, read as the person (people-read.ts). */\nexport const PEOPLE_READ_KEY = 'organization.employee'\n\n/** The category of that read - `data-read:<key>`, as every read target's. */\nexport const PEOPLE_READ_CATEGORY = `data-read:${PEOPLE_READ_KEY}`\n\n/** The invite card - bring someone into this conversation (invite-tools.ts). */\nexport const INVITE_CATEGORY = 'chat-invite'\n\n/** Both, in the order a turn puts them in front: find the person, then the card. */\nexport const PEOPLE_CATEGORIES = [PEOPLE_READ_CATEGORY, INVITE_CATEGORY]\n"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ReadTarget } from '../data-read/read-target.js';
|
|
2
|
+
/** The fields read of each person - the answer may carry nothing else. */
|
|
3
|
+
export declare const PEOPLE_SELECTION = "id name alias jobPosition jobResponsibility department { id name } user { id }";
|
|
4
|
+
export declare function peopleReadTarget(): ReadTarget;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PEOPLE_SELECTION = void 0;
|
|
4
|
+
exports.peopleReadTarget = peopleReadTarget;
|
|
5
|
+
/*
|
|
6
|
+
* Who works here - the organization's `employees` list opened to every conversation as a read target
|
|
7
|
+
* (operato-application/design/ai/read-targets.md), the way an application opens its own lists.
|
|
8
|
+
*
|
|
9
|
+
* Asked to bring 정연희 into a group conversation, the model answered that it had no tool to look up people and
|
|
10
|
+
* sent the person to search a screen (2026-10-24). The list existed; no conversation could read it.
|
|
11
|
+
*
|
|
12
|
+
* Nothing new is built: the read runs the organization module's own query as the person, so its door
|
|
13
|
+
* (`employee` query, or the domain owner) decides who may read people, and an application without the organization
|
|
14
|
+
* module has no such door - the tool is not offered there. The selection is the projection: name, department,
|
|
15
|
+
* position and the user an invite takes. No phone, e-mail, address, bank account or emergency contact is read.
|
|
16
|
+
*/
|
|
17
|
+
const list_read_js_1 = require("../data-read/list-read.js");
|
|
18
|
+
const people_keys_js_1 = require("./people-keys.js");
|
|
19
|
+
/** The fields read of each person - the answer may carry nothing else. */
|
|
20
|
+
exports.PEOPLE_SELECTION = 'id name alias jobPosition jobResponsibility department { id name } user { id }';
|
|
21
|
+
function peopleReadTarget() {
|
|
22
|
+
const target = (0, list_read_js_1.listReadTarget)({
|
|
23
|
+
key: people_keys_js_1.PEOPLE_READ_KEY,
|
|
24
|
+
title: 'ai-assistant.people.title',
|
|
25
|
+
name: 'people of the organization',
|
|
26
|
+
listQuery: 'employees',
|
|
27
|
+
/* People who left are not offered - an invite, a mention or an answer about who does what is about who is here. */
|
|
28
|
+
fixedFilters: [{ name: 'active', operator: 'eq', value: true }],
|
|
29
|
+
selection: exports.PEOPLE_SELECTION,
|
|
30
|
+
columns: [
|
|
31
|
+
{ name: 'name', type: 'string', labelKey: 'ai-assistant.people.name' },
|
|
32
|
+
{ name: 'department', type: 'string', path: 'department.name', labelKey: 'ai-assistant.people.department' },
|
|
33
|
+
{ name: 'position', type: 'string', path: 'jobPosition', labelKey: 'ai-assistant.people.position' },
|
|
34
|
+
{ name: 'responsibility', type: 'string', path: 'jobResponsibility', labelKey: 'ai-assistant.people.responsibility' },
|
|
35
|
+
{ name: 'userId', type: 'string', path: 'user.id', labelKey: 'ai-assistant.people.user' }
|
|
36
|
+
],
|
|
37
|
+
search: 'name, employee number and alias'
|
|
38
|
+
});
|
|
39
|
+
/*
|
|
40
|
+
* Who holds a position, who is in a team - asked 「프로 직책인 사람은 누구야?」 the model put the position into the search
|
|
41
|
+
* term, which matches names, numbers and aliases only, and answered that nobody held it (2026-10-24).
|
|
42
|
+
*/
|
|
43
|
+
const contains = (name) => (value) => ({ name, operator: 'i_like', value });
|
|
44
|
+
return {
|
|
45
|
+
...target,
|
|
46
|
+
criteria: [
|
|
47
|
+
...target.criteria,
|
|
48
|
+
{ name: 'department', type: 'string', labelKey: 'ai-assistant.people.department', description: "part of the department's name", filter: contains('departmentName') },
|
|
49
|
+
{ name: 'position', type: 'string', labelKey: 'ai-assistant.people.position', description: 'part of the position (직책)', filter: contains('jobPosition') },
|
|
50
|
+
{ name: 'responsibility', type: 'string', labelKey: 'ai-assistant.people.responsibility', description: 'part of what they are responsible for (담당)', filter: contains('jobResponsibility') }
|
|
51
|
+
],
|
|
52
|
+
description: 'the people who work here, as the organization lists them - name, department, position, responsibility, and the user id ' +
|
|
53
|
+
'an invite into a conversation takes (empty when the person has no account). Read only who the asker may see. ' +
|
|
54
|
+
'The text condition finds by name, employee number or alias only - a department, position or responsibility has its own condition.'
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=people-read.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"people-read.js","sourceRoot":"","sources":["../../../server/service/people/people-read.ts"],"names":[],"mappings":";;;AAmBA,4CAoCC;AAvDD;;;;;;;;;;;GAWG;AACH,4DAA0D;AAE1D,qDAAkD;AAElD,0EAA0E;AAC7D,QAAA,gBAAgB,GAAG,gFAAgF,CAAA;AAEhH,SAAgB,gBAAgB;IAC9B,MAAM,MAAM,GAAG,IAAA,6BAAc,EAAC;QAC5B,GAAG,EAAE,gCAAe;QACpB,KAAK,EAAE,2BAA2B;QAClC,IAAI,EAAE,4BAA4B;QAClC,SAAS,EAAE,WAAW;QACtB,mHAAmH;QACnH,YAAY,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QAC/D,SAAS,EAAE,wBAAgB;QAC3B,OAAO,EAAE;YACP,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,0BAA0B,EAAE;YACtE,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,iBAAiB,EAAE,QAAQ,EAAE,gCAAgC,EAAE;YAC3G,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,8BAA8B,EAAE;YACnG,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,mBAAmB,EAAE,QAAQ,EAAE,oCAAoC,EAAE;YACrH,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,0BAA0B,EAAE;SAC1F;QACD,MAAM,EAAE,iCAAiC;KAC1C,CAAC,CAAA;IACF;;;OAGG;IACH,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,KAAU,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAA;IACxF,OAAO;QACL,GAAG,MAAM;QACT,QAAQ,EAAE;YACR,GAAG,MAAM,CAAC,QAAQ;YAClB,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,gCAAgC,EAAE,WAAW,EAAE,+BAA+B,EAAE,MAAM,EAAE,QAAQ,CAAC,gBAAgB,CAAC,EAAE;YACpK,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,8BAA8B,EAAE,WAAW,EAAE,2BAA2B,EAAE,MAAM,EAAE,QAAQ,CAAC,aAAa,CAAC,EAAE;YACzJ,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,oCAAoC,EAAE,WAAW,EAAE,4CAA4C,EAAE,MAAM,EAAE,QAAQ,CAAC,mBAAmB,CAAC,EAAE;SAC7L;QACD,WAAW,EACT,yHAAyH;YACzH,+GAA+G;YAC/G,mIAAmI;KACtI,CAAA;AACH,CAAC","sourcesContent":["/*\n * Who works here - the organization's `employees` list opened to every conversation as a read target\n * (operato-application/design/ai/read-targets.md), the way an application opens its own lists.\n *\n * Asked to bring 정연희 into a group conversation, the model answered that it had no tool to look up people and\n * sent the person to search a screen (2026-10-24). The list existed; no conversation could read it.\n *\n * Nothing new is built: the read runs the organization module's own query as the person, so its door\n * (`employee` query, or the domain owner) decides who may read people, and an application without the organization\n * module has no such door - the tool is not offered there. The selection is the projection: name, department,\n * position and the user an invite takes. No phone, e-mail, address, bank account or emergency contact is read.\n */\nimport { listReadTarget } from '../data-read/list-read.js'\nimport type { ReadTarget } from '../data-read/read-target.js'\nimport { PEOPLE_READ_KEY } from './people-keys.js'\n\n/** The fields read of each person - the answer may carry nothing else. */\nexport const PEOPLE_SELECTION = 'id name alias jobPosition jobResponsibility department { id name } user { id }'\n\nexport function peopleReadTarget(): ReadTarget {\n const target = listReadTarget({\n key: PEOPLE_READ_KEY,\n title: 'ai-assistant.people.title',\n name: 'people of the organization',\n listQuery: 'employees',\n /* People who left are not offered - an invite, a mention or an answer about who does what is about who is here. */\n fixedFilters: [{ name: 'active', operator: 'eq', value: true }],\n selection: PEOPLE_SELECTION,\n columns: [\n { name: 'name', type: 'string', labelKey: 'ai-assistant.people.name' },\n { name: 'department', type: 'string', path: 'department.name', labelKey: 'ai-assistant.people.department' },\n { name: 'position', type: 'string', path: 'jobPosition', labelKey: 'ai-assistant.people.position' },\n { name: 'responsibility', type: 'string', path: 'jobResponsibility', labelKey: 'ai-assistant.people.responsibility' },\n { name: 'userId', type: 'string', path: 'user.id', labelKey: 'ai-assistant.people.user' }\n ],\n search: 'name, employee number and alias'\n })\n /*\n * Who holds a position, who is in a team - asked 「프로 직책인 사람은 누구야?」 the model put the position into the search\n * term, which matches names, numbers and aliases only, and answered that nobody held it (2026-10-24).\n */\n const contains = (name: string) => (value: any) => ({ name, operator: 'i_like', value })\n return {\n ...target,\n criteria: [\n ...target.criteria,\n { name: 'department', type: 'string', labelKey: 'ai-assistant.people.department', description: \"part of the department's name\", filter: contains('departmentName') },\n { name: 'position', type: 'string', labelKey: 'ai-assistant.people.position', description: 'part of the position (직책)', filter: contains('jobPosition') },\n { name: 'responsibility', type: 'string', labelKey: 'ai-assistant.people.responsibility', description: 'part of what they are responsible for (담당)', filter: contains('jobResponsibility') }\n ],\n description:\n 'the people who work here, as the organization lists them - name, department, position, responsibility, and the user id ' +\n 'an invite into a conversation takes (empty when the person has no account). Read only who the asker may see. ' +\n 'The text condition finds by name, employee number or alias only - a department, position or responsibility has its own condition.'\n }\n}\n"]}
|
|
@@ -131,6 +131,9 @@ function modeCarriesSharedRules(kind) {
|
|
|
131
131
|
found.push('finish-rule-copy');
|
|
132
132
|
if (/\[writer → addressee\]/.test(prompt))
|
|
133
133
|
found.push('speaker-rule-copy');
|
|
134
|
+
/* The picked records' note is the framework's form (SELECTION_RULE) - an app reading its own form of it is a copy. */
|
|
135
|
+
if (/explicit-page-selection|\[keys\]/i.test(prompt))
|
|
136
|
+
found.push('selection-rule-copy');
|
|
134
137
|
return found;
|
|
135
138
|
}
|
|
136
139
|
/** Subjects that more than one conversation names - a screen has one (design/ai/screen-pattern.md §3). */
|
|
@@ -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 { openSubjectLine, 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'\nimport { modeFrontCategories, type TurnScreen } from '../assistant-mode/mode-categories.js'\n\nexport interface ModeRunResult {\n /** 이 모드가 모델에게 실제로 건넨 도구 이름. */\n offeredTools: string[]\n /** 건네지 않았지만 모델이 openTools 로 꺼낼 수 있던 도구 — 둘을 합치면 이 모드로 쓸 수 있는 전부(ADR-0061 결정 9). */\n openableTools: 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; screen?: TurnScreen } = {} 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 /* What the door puts in front on that screen (ADR-0061 decision 9) - a mode too large for one call is offered as a person meets it. */\n front: { categories: modeFrontCategories(getAssistantMode(kind) ?? {}, options.screen ?? {}) },\n requireToolOnFirstTurn: (requiredToolOnFirstTurn = Boolean(settings.requireToolOnFirstTurn)),\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 openableTools: [...result.openableTools],\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\n/**\n * What an app's prompt carries that every conversation already gets (design/ai/screen-pattern.md §6 · §7).\n *\n * The absence rule is added by the door to every conversation. A copy in an app's prompt says it twice, and the\n * copies drift - five apps had five spellings of it.\n */\nexport function modeCarriesSharedRules(kind: string): string[] {\n const prompt = String(getAssistantMode(kind)?.systemPrompt ?? '')\n const found: string[] = []\n if (/name our plumbing|do not describe our machinery/i.test(prompt)) found.push('absence-rule-copy')\n if (/^\\s*Next:/im.test(prompt) || /end the reply with one last line/i.test(prompt)) found.push('next-request-rule-copy')\n if (/finish what was asked in this turn|never ask whether to propose/i.test(prompt)) found.push('finish-rule-copy')\n if (/\\[writer → addressee\\]/.test(prompt)) found.push('speaker-rule-copy')\n return found\n}\n\n/** Subjects that more than one conversation names - a screen has one (design/ai/screen-pattern.md §3). */\nexport function subjectsClaimedTwice(): string[] {\n return getAssistantModeWarnings()\n .filter(warning => warning.code === 'assistant-mode-subject-claimed-twice')\n .map(warning => String(warning.params.subject))\n}\n\n/** The line the model reads for an open subject - an app's test checks its describer names its own record. */\nexport function openSubjectLineFor(anchorType: string, anchorId: string, domainId: string, state?: any): Promise<string | undefined> {\n return openSubjectLine({ anchorType, anchorId, domain: { id: domainId }, state })\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 { openSubjectLine, 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'\nimport { modeFrontCategories, type TurnScreen } from '../assistant-mode/mode-categories.js'\n\nexport interface ModeRunResult {\n /** 이 모드가 모델에게 실제로 건넨 도구 이름. */\n offeredTools: string[]\n /** 건네지 않았지만 모델이 openTools 로 꺼낼 수 있던 도구 — 둘을 합치면 이 모드로 쓸 수 있는 전부(ADR-0061 결정 9). */\n openableTools: 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; screen?: TurnScreen } = {} 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 /* What the door puts in front on that screen (ADR-0061 decision 9) - a mode too large for one call is offered as a person meets it. */\n front: { categories: modeFrontCategories(getAssistantMode(kind) ?? {}, options.screen ?? {}) },\n requireToolOnFirstTurn: (requiredToolOnFirstTurn = Boolean(settings.requireToolOnFirstTurn)),\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 openableTools: [...result.openableTools],\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\n/**\n * What an app's prompt carries that every conversation already gets (design/ai/screen-pattern.md §6 · §7).\n *\n * The absence rule is added by the door to every conversation. A copy in an app's prompt says it twice, and the\n * copies drift - five apps had five spellings of it.\n */\nexport function modeCarriesSharedRules(kind: string): string[] {\n const prompt = String(getAssistantMode(kind)?.systemPrompt ?? '')\n const found: string[] = []\n if (/name our plumbing|do not describe our machinery/i.test(prompt)) found.push('absence-rule-copy')\n if (/^\\s*Next:/im.test(prompt) || /end the reply with one last line/i.test(prompt)) found.push('next-request-rule-copy')\n if (/finish what was asked in this turn|never ask whether to propose/i.test(prompt)) found.push('finish-rule-copy')\n if (/\\[writer → addressee\\]/.test(prompt)) found.push('speaker-rule-copy')\n /* The picked records' note is the framework's form (SELECTION_RULE) - an app reading its own form of it is a copy. */\n if (/explicit-page-selection|\\[keys\\]/i.test(prompt)) found.push('selection-rule-copy')\n return found\n}\n\n/** Subjects that more than one conversation names - a screen has one (design/ai/screen-pattern.md §3). */\nexport function subjectsClaimedTwice(): string[] {\n return getAssistantModeWarnings()\n .filter(warning => warning.code === 'assistant-mode-subject-claimed-twice')\n .map(warning => String(warning.params.subject))\n}\n\n/** The line the model reads for an open subject - an app's test checks its describer names its own record. */\nexport function openSubjectLineFor(anchorType: string, anchorId: string, domainId: string, state?: any): Promise<string | undefined> {\n return openSubjectLine({ anchorType, anchorId, domain: { id: domainId }, state })\n}\n"]}
|