@things-factory/twin-ai 10.0.0-zeta.12

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 (40) hide show
  1. package/dist-server/index.d.ts +8 -0
  2. package/dist-server/index.js +12 -0
  3. package/dist-server/index.js.map +1 -0
  4. package/dist-server/service/assistant.d.ts +58 -0
  5. package/dist-server/service/assistant.js +422 -0
  6. package/dist-server/service/assistant.js.map +1 -0
  7. package/dist-server/service/index.d.ts +8 -0
  8. package/dist-server/service/index.js +13 -0
  9. package/dist-server/service/index.js.map +1 -0
  10. package/dist-server/service/read-predict.d.ts +24 -0
  11. package/dist-server/service/read-predict.js +236 -0
  12. package/dist-server/service/read-predict.js.map +1 -0
  13. package/dist-server/service/twin-ai-resolver.d.ts +6 -0
  14. package/dist-server/service/twin-ai-resolver.js +36 -0
  15. package/dist-server/service/twin-ai-resolver.js.map +1 -0
  16. package/dist-server/service/zero-to-twin.d.ts +100 -0
  17. package/dist-server/service/zero-to-twin.js +310 -0
  18. package/dist-server/service/zero-to-twin.js.map +1 -0
  19. package/dist-server/tsconfig.tsbuildinfo +1 -0
  20. package/package.json +34 -0
  21. package/scripts/verify-llm.ts +73 -0
  22. package/server/index.ts +8 -0
  23. package/server/service/assistant.ts +447 -0
  24. package/server/service/index.ts +10 -0
  25. package/server/service/read-predict.ts +214 -0
  26. package/server/service/twin-ai-resolver.ts +27 -0
  27. package/server/service/zero-to-twin.ts +307 -0
  28. package/test/anchors.test.ts +81 -0
  29. package/test/capability-signals.test.ts +79 -0
  30. package/test/engine-tools.test.ts +150 -0
  31. package/test/forecast-dynamics.test.ts +44 -0
  32. package/test/grounding-guard.test.ts +69 -0
  33. package/test/grounding.test.ts +121 -0
  34. package/test/journal.test.ts +62 -0
  35. package/test/locale.test.ts +48 -0
  36. package/test/loop.test.ts +100 -0
  37. package/test/tools.test.ts +93 -0
  38. package/test/zero-to-twin.test.ts +210 -0
  39. package/things-factory.config.js +2 -0
  40. package/tsconfig.json +11 -0
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @things-factory/twin-ai — operato-twin 디지털트윈용 AI 어시스턴트(서버-only).
3
+ *
4
+ * 접지 대화(P1): "정상?/뭐 봐야 해?" 를 트윈 실데이터(attentions/state)를 도구로 조회해 답한다(환각 금지).
5
+ * ai-client-base(프로바이더 추상화) + headless-twin(TwinEngine 스냅샷) 위에 트윈 도메인 어댑터만 얹는다.
6
+ * 설계 SoT: operato-twin/design/plans/twin-ai-assistant.md
7
+ */
8
+ export * from './service/index.js';
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ /**
5
+ * @things-factory/twin-ai — operato-twin 디지털트윈용 AI 어시스턴트(서버-only).
6
+ *
7
+ * 접지 대화(P1): "정상?/뭐 봐야 해?" 를 트윈 실데이터(attentions/state)를 도구로 조회해 답한다(환각 금지).
8
+ * ai-client-base(프로바이더 추상화) + headless-twin(TwinEngine 스냅샷) 위에 트윈 도메인 어댑터만 얹는다.
9
+ * 설계 SoT: operato-twin/design/plans/twin-ai-assistant.md
10
+ */
11
+ tslib_1.__exportStar(require("./service/index.js"), exports);
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;AAAA;;;;;;GAMG;AACH,6DAAkC","sourcesContent":["/**\n * @things-factory/twin-ai — operato-twin 디지털트윈용 AI 어시스턴트(서버-only).\n *\n * 접지 대화(P1): \"정상?/뭐 봐야 해?\" 를 트윈 실데이터(attentions/state)를 도구로 조회해 답한다(환각 금지).\n * ai-client-base(프로바이더 추상화) + headless-twin(TwinEngine 스냅샷) 위에 트윈 도메인 어댑터만 얹는다.\n * 설계 SoT: operato-twin/design/plans/twin-ai-assistant.md\n */\nexport * from './service/index.js'\n"]}
@@ -0,0 +1,58 @@
1
+ import type { AITool } from '@things-factory/ai-client-base';
2
+ export declare const TOOLS: AITool[];
3
+ /** 도구 실행 — canonical 소스(라이브 커널 스냅샷·이벤트 저널)에서 직접(in-process).
4
+ * export: P0 테스트가 접지·propose-only 게이트를 도구 단위로 검증(런타임 동작 무변경 — 테스트 이음매). */
5
+ export declare function execTool(name: string, args: Record<string, any>, domainId?: string): Promise<unknown>;
6
+ export interface TwinAiAnswer {
7
+ reply: string;
8
+ toolCalls: string[];
9
+ /** 답이 언급한 공간 앵커(attentions/signals 파생) — 클라가 칩·하이라이트로 공간과 연결.
10
+ * kind 는 클라 focus 표준(location|mover|order). attention 은 그 신호(의미·조치)로, 클라가
11
+ * 공간 진입 시 entity360 상단에 원인·권고 조치를 그대로 재현하도록 함께 실어 보낸다. */
12
+ anchors: {
13
+ kind: string;
14
+ id: string;
15
+ attention?: any;
16
+ }[];
17
+ /** AI 가 제안한 조치(실행 아님 — 사용자 확인 후 dispatchTwinCommand 로 실행). 살아있는 시스템 게이트. */
18
+ proposedActions: {
19
+ command: string;
20
+ args: any;
21
+ label: string;
22
+ reason?: string;
23
+ }[];
24
+ /** zero-to-twin 초안(생성 아님 — 사용자 확인 후 saveTwinInstance+start). §12 프로비저닝 게이트. */
25
+ proposedProvisions: {
26
+ system: string;
27
+ instanceId: string;
28
+ label: string;
29
+ board: any;
30
+ scenario: any;
31
+ space: any;
32
+ summary?: any;
33
+ warnings?: string[];
34
+ viability?: any;
35
+ explanation?: any;
36
+ }[];
37
+ /** 접지 가드 — reply 가 언급했으나 근거(도구 출력·사용자 입력·이력)에 없는 엔티티 id(환각 후보). 비면 접지 정상. */
38
+ groundingWarnings: string[];
39
+ }
40
+ export declare function extractIdTokens(s: string): Set<string>;
41
+ /** reply 언급 id − 근거코퍼스 id = 미확인(환각 후보). */
42
+ export declare function checkGrounding(reply: string, groundedCorpus: string): string[];
43
+ /** 도구 결과에서 공간 앵커 수집(중복 제거). 두 경로 모두 커버: queryAttentions→out.attentions(full),
44
+ * summarizeStatus→out.signals(trimmed). kind 는 클라 focus 표준으로(노드=location), 신호(원인·조치)를
45
+ * attention 으로 함께 실어 공간 진입 시 재현되게 한다. attentions(full)가 signals(trimmed)를 덮도록 뒤에 둔다. */
46
+ export declare function collectAnchors(out: any, into: Map<string, {
47
+ kind: string;
48
+ id: string;
49
+ attention?: any;
50
+ }>): void;
51
+ /**
52
+ * 접지된 단발 질의 — 완주 후 반환. tool 루프는 단순(단일 read 도구, 최대 4 iteration).
53
+ * Gemini providerMeta(thoughtSignature) 는 tool_use turn 에 반드시 echo(안 하면 400).
54
+ */
55
+ export declare function twinAiAsk(instanceId: string, message: string, domainId?: string, history?: {
56
+ role: string;
57
+ text: string;
58
+ }[], locale?: string): Promise<TwinAiAnswer>;
@@ -0,0 +1,422 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TOOLS = void 0;
4
+ exports.execTool = execTool;
5
+ exports.extractIdTokens = extractIdTokens;
6
+ exports.checkGrounding = checkGrounding;
7
+ exports.collectAnchors = collectAnchors;
8
+ exports.twinAiAsk = twinAiAsk;
9
+ const ai_client_base_1 = require("@things-factory/ai-client-base");
10
+ /* 도구 실행은 관심사별 모듈에 위임(execTool 은 디스패치만). assistant 는 오케스트레이션(agentic 루프)에 집중. */
11
+ const zero_to_twin_ts_1 = require("./zero-to-twin.js");
12
+ const read_predict_ts_1 = require("./read-predict.js");
13
+ /*
14
+ * 트윈 AI 어시스턴트 — 첫 수직 슬라이스(P1): 접지된 read Q&A("정상?/뭐 봐야 해?").
15
+ * 설계 SoT: operato-twin/design/plans/twin-ai-assistant.md
16
+ *
17
+ * 사상: AI 도구 = 트윈 canonical 연산 1:1(무방언). 답은 도구로 조회한 사실에만 근거(grounded, 환각 금지).
18
+ * 첫 도구 = queryAttentions(스냅샷의 요약 주목신호 — 집계 API·full-state 덤프 없이 "정상?" 접지).
19
+ * LLM 프로바이더는 ai-client-base 추상화(config.aiClient) 재사용. 완주 후 반환(스트리밍은 후행).
20
+ */
21
+ const SYSTEM_PROMPT = `당신은 operato-twin 디지털 트윈의 운영 어시스턴트다.
22
+ 규칙:
23
+ - 반드시 도구로 조회한 사실에만 근거해 답한다. 지어내지 말 것. 데이터가 없으면 "데이터 없음"이라고 답한다.
24
+ - **"정상인가?/지금 어때?/괜찮아?" 류엔 summarizeStatus 로 전반 health(normal|watch|attention|critical)와 봐야 할 신호를 한눈에 받아** 초보자 언어로 전한다: normal 이면 "정상", 아니면 무엇이 문제인지·어떡할지(suggestedAction)를 평이하게. 기술 title 대신 kind·severity 로 옮겨 말한다.
25
+ - **원인을 묻는 질문("작업 지연의 원인은?", "왜 정체돼?")에는 queryAttentions 와 queryState 를 함께 조회해 여러 신호를 상관지어 근원을 추론한다.**
26
+ 예: 설비 고장(mover status=down) → 그 노드(weld-station 등)의 점유율 상승·태스크 정체 → 하류 오더 지연. **인과 사슬을 "A → B → C" 형태로 근거와 함께 제시**한다.
27
+ 추론은 조회한 데이터로만 뒷받침하고, 데이터로 단정 못 하면 "데이터로는 여기까지"라고 밝힌다(그럴듯한 추측 금지).
28
+ - **"언제부터/이력/시점" 질문에는 traceJournal 도구로 이벤트 저널 타임라인을 조회한다**(eventType: 'equipment.status'=설비 고장/복구 이력, 'order.status'=오더, 'task.status'=작업). 문제가 **시작된 시점·이벤트**를 특정해 "T 시점 welder-2 고장 이후 누적" 처럼 시간 축으로 근원을 짚는다.
29
+ - **미래를 묻는 질문("앞으로 어떻게 돼?/언제 회복돼?/전망은?")에는 forecast 도구를 쓴다.** 현재를 여러 번 복제(fork)해 미래 확률을 변주한 **몬테카를로 예측**이다("현재 조건 지속" 가정 명시). now 대비 forecast 의 **p50(중앙값)·p90(비관 시나리오)**로 전망하고(단정 대신 분포로 정직하게 — 예: "p50이면 이 정도, p90 최악엔 포화"), **recovery(회복 예상 시점)**를 함께 전한다.
30
+ - **"몇 대 필요?/최적 자원" 질문에는 optimizeResource 도구를 쓴다.** 자원 대수를 0..N 스윕해 각 대수의 정체 지표(tasks·occupancy)를 비교하고, **개선이 꺾이는 지점(수확체감)** 또는 목표 충족 **최소 대수**를 짚는다("N대부터는 효과 미미" 처럼 marginal gain 명시). 목표 기준이 모호하면 지표 추세로 판단하거나 되묻는다.
31
+ - **구성을 바꾼 가정 질문("지게차 +1이면?/용량 늘리면 나아져?")에는 whatIf 도구를 쓴다.** baseline(무변경) vs modified(변경) 를 현재에서 각각 fork·투영해 비교하고, delta(occupancy/tasks/orders 차이)로 **개선/무변화**를 판단한다. 대상 노드·무버 kind/id 가 필요하면 queryState 로 먼저 확인. "현재 조건 지속 + 그 변경" 가정임을 명시하고, 원본은 무간섭(fork).
32
+ - 근거를 함께 제시한다: 주목신호의 kind·severity·anchor(어느 노드/무버/오더)·since. suggestedAction 이 있으면 "권고: …"로 알리되, 직접 실행하지는 않는다.
33
+ - **조치 실행 요청("그 오더 보류해줘/재개해줘")에는 절대 직접 실행하지 말고 proposeAction 도구로 "제안"만 한다**(command·args·label·reason). 실제 실행은 **사용자가 확인 버튼으로** 한다. 제안 후 답에 "실행하려면 아래에서 확인하세요"라고 안내한다. attentions 의 suggestedAction 을 그대로 제안에 쓸 수 있다.
34
+ - **지원되는 라이브 커맨드는 이것뿐이다(다른 command 를 지어내지 말 것)**: order.hold / order.resume / order.release / attention.ack / resource.hold(계획 정지) / resource.resume / resource.down(고장 주입) / resource.repair(수리) / **resource.add(자원 추가 — args:{kind, homeNode, count})**. 목록 밖은 제안 금지.
35
+ - **"지게차 추가해줘/한 대 더 투입"류에는 resource.add 를 proposeAction 으로 제안한다(실제 반영됨).** homeNode 는 queryState 로 적절한 노드(입고 도크 등)를 골라 채운다. "효과가 어떨까?/추가하면 나아져?"처럼 **가정·검토**면 먼저 whatIf 로 미리보기하고, "추가해줘"처럼 **실행 의도**면 resource.add 를 제안한다.
36
+ - **자원 제거·노드/구조 변경은 아직 커맨드가 아니다(미지원).** 그런 요청엔 "새 구성으로 트윈을 다시 만들어야 한다(프로비저닝)"고 정직하게 안내한다. **없는 기능을 실행되는 것처럼 말하지 말 것.**
37
+ - **사용자가 무엇을 만들 수 있는지 모르거나("트윈 뭐 만들 수 있어?", "우리 상황엔 뭐가 맞아?") 시스템 선택이 모호하면, 먼저 listTwinSystems 로 선택지(창고/야드/공장)와 각각 다루는 것을 보여주고, 사용자 상황(취급물·공정)을 물어 맞는 시스템을 고른 뒤 아래 zero-to-twin 으로 잇는다.** 초보의 첫 장벽은 "무엇을 만들지 모름"이다 — 대신 골라주지 말고 골라내도록 돕는다.
38
+ - **새 트윈을 만들어 달라는 요청("이런 창고/야드/공장 만들어줘", "~를 시뮬레이션하고 싶어")에는 zero-to-twin 순서를 따른다**:
39
+ 1. 먼저 **listTwinTypes** 로 해당 시스템(wms=창고/yms=야드/mes=제조)의 **가능한 노드/무버 타입 팔레트를 확인**한다. 타입을 지어내지 말 것 — 카탈로그 어휘만 쓴다(모르면 listTwinTypes 로 확인).
40
+ 2. **규모·구조가 모호하면(예: "창고 만들어줘"만 말함) 먼저 suggestArchetype(system, scale) 로 완비된 합리적 기본 구성(입고·보관·출고·자원 다 포함 → 실제 작동)을 받는다.** 사용자가 규모를 말했으면 scale(small/medium/large)로, 특이사항을 말했으면 반환된 nodes/movers 를 조정한다. 그대로 쓰든 조정하든 **어떤 가정을 했는지 명시**한다. (구체적으로 다 말한 요청이면 그 명세를 그대로 쓴다.)
41
+ 3. **proposeTwin** 도구로 구조·좌표·시나리오 **초안을 "제안"만** 한다(직접 생성 아님). 좌표는 도구가 자동 배치한다. 제안 후 **proposeTwin 결과의 explanation.parts(각 {label, count, role})로** 무엇을 잡았는지 사용자 언어로 평이하게 전한다(spaceCount·resourceCount 로 요약). **기술 키(typeKey: dock-ship 등)는 절대 노출하지 말고 label(출고 도크 등)을 사용자 언어로 옮겨 말한다.** "아래에서 확인하면 생성됩니다"라고 안내하고, 실제 생성은 사용자가 확인 버튼으로.
42
+ 4. **proposeTwin 결과의 viability 를 반드시 확인한다.** ok=false 면 issues[].message(영어 canonical)를 **사용자 언어로 옮겨** 전문용어 없이 짚고 고침을 제안한다 — 예(no-active-resource): "지금은 물건을 옮길 지게차가 없어 트윈이 멈춰 있어요. 몇 대 넣을까요?" (없는 기능을 되는 것처럼 말하지 말 것.)
43
+ 5. **이미 제안한 트윈을 사용자가 바꿔달라고 하면("더 크게/작게", "지게차 빼줘/5대로", "보관 늘려줘") refineTwin 을 쓴다.** 직전 제안의 nodes/movers 를 그대로 넘기고 변경(scale/setCounts/removeKinds)만 지정한다 — **개수를 직접 재계산하지 말 것(도구가 계산).** 결과의 explanation·viability 를 다시 평이하게 전한다. 새로 처음부터 만드는 게 아니라 다듬는 것이다.
44
+ - 현장 목표(SLA·목표 처리량 등)가 설정돼 있지 않으면 "현재 도메인 판단 기준"이라고 명시한다(목표 대비 정상 여부는 아직 판단 불가).
45
+ - **답변은 사용자가 쓴 언어로 한다**(한국어로 물으면 한국어, 영어면 영어). 도구가 돌려준 영어 message·영어 code·카탈로그 label 은 사용자 언어로 옮겨 전한다. 간결하게.`;
46
+ /* export: P0 테스트 토대가 도구 스펙을 직접 검증(런타임 동작 무변경 — 테스트 이음매). */
47
+ exports.TOOLS = [
48
+ {
49
+ name: 'summarizeStatus',
50
+ description: '"지금 괜찮아?/어때?/정상?" 류에 트윈 전반 상태를 한눈에 반환한다(운영측 만만함). health(normal|watch|attention|critical) + severity별 counts + signals[{severity,kind,title,anchor,suggestedAction}]. health·kind·severity 는 언어 중립 — 초보자 언어로 옮겨 전하고, suggestedAction 있으면 권고로. 개별 신호를 깊이 파려면 queryState/traceJournal 로.',
51
+ parameters: {
52
+ type: 'object',
53
+ properties: { instanceId: { type: 'string', description: '트윈 인스턴스 id' } },
54
+ required: ['instanceId']
55
+ }
56
+ },
57
+ {
58
+ name: 'queryAttentions',
59
+ description: '현재 트윈 스냅샷의 주목신호(봐야 할 것) 목록을 반환한다. 각 항목: kind, severity(low|medium|high|critical), state(active|acknowledged|cleared), title, detail, anchor{nodeId,moverId,orderId}, since(simClockMs), suggestedAction{command,label}. 요약이므로 전체 상태를 덤프하지 않고 이 도구로 판단한다.',
60
+ parameters: {
61
+ type: 'object',
62
+ properties: {
63
+ instanceId: { type: 'string', description: '트윈 인스턴스 id' }
64
+ },
65
+ required: ['instanceId']
66
+ }
67
+ },
68
+ {
69
+ name: 'queryState',
70
+ description: '현재 트윈 스냅샷의 요약 상태(전체 덤프 아님) — 원인 추론용. 반환: nodes[{id,type,occupancy,capacity}](점유율=병목 신호), movers[{id,kind,status}](status=down 이면 설비 고장), taskCounts(status별 수), orderCounts(status별 수). attentions 와 상관지어 인과를 추론한다.',
71
+ parameters: {
72
+ type: 'object',
73
+ properties: {
74
+ instanceId: { type: 'string', description: '트윈 인스턴스 id' }
75
+ },
76
+ required: ['instanceId']
77
+ }
78
+ },
79
+ {
80
+ name: 'forecast',
81
+ description: '현재 상태를 복제(fork)해 horizonMinutes 뒤까지 굴린 투영(현재 조건·부하·고장/수리 일정 지속 가정). 반환: now·projected 각각 { occupancy(총점유), orders, tasks, moversDown }. "앞으로/언제 회복/전망" 질문에 사용. 원본 라이브 무간섭(fork 격리).',
82
+ parameters: {
83
+ type: 'object',
84
+ properties: {
85
+ instanceId: { type: 'string', description: '트윈 인스턴스 id' },
86
+ horizonMinutes: { type: 'number', description: '예측 지평(분). 기본 30, 1~120.' }
87
+ },
88
+ required: ['instanceId']
89
+ }
90
+ },
91
+ {
92
+ name: 'whatIf',
93
+ description: '구성을 바꾼 가상 시나리오를 현재에서 fork해 비교("지게차 +1이면?", "용량 늘리면?"). baseline(무변경) vs modified 를 horizonMinutes 까지 굴려 비교. 반환: baseline·modified·delta 의 {occupancy,orders,tasks,moversDown}. 원본 무간섭(fork). 대상 kind/nodeId 는 queryState 로 확인.',
94
+ parameters: {
95
+ type: 'object',
96
+ properties: {
97
+ instanceId: { type: 'string', description: '트윈 인스턴스 id' },
98
+ horizonMinutes: { type: 'number', description: '기본 30, 1~120' },
99
+ addMovers: {
100
+ type: 'array',
101
+ description: '추가할 자원(무버). 예: [{"kind":"forklift","homeNode":"dock-recv","count":1}]',
102
+ items: {
103
+ type: 'object',
104
+ properties: { kind: { type: 'string' }, homeNode: { type: 'string' }, count: { type: 'number' } },
105
+ required: ['kind', 'homeNode']
106
+ }
107
+ },
108
+ setCapacity: {
109
+ type: 'array',
110
+ description: '노드 용량 변경. 예: [{"nodeId":"bin-1","capacity":200}]',
111
+ items: {
112
+ type: 'object',
113
+ properties: { nodeId: { type: 'string' }, capacity: { type: 'number' } },
114
+ required: ['nodeId', 'capacity']
115
+ }
116
+ }
117
+ },
118
+ required: ['instanceId']
119
+ }
120
+ },
121
+ {
122
+ name: 'optimizeResource',
123
+ description: '자원(무버) 대수를 0..maxCount 로 바꿔가며 각각 fork·투영(동일 조건, 무버만 증가)해 정체 지표를 비교한다. "정체 해소에 몇 대 필요?"의 근거. 반환: sweep[{added, occupancy, tasks, orders}]. AI 가 수확체감 꺾임/목표 충족 최소 대수를 판단.',
124
+ parameters: {
125
+ type: 'object',
126
+ properties: {
127
+ instanceId: { type: 'string', description: '트윈 인스턴스 id' },
128
+ kind: { type: 'string', description: '추가할 자원 종류(예: forklift, welder)' },
129
+ homeNode: { type: 'string', description: '자원 배치 노드 id' },
130
+ maxCount: { type: 'number', description: '최대 추가 대수(기본 5, 1~10)' },
131
+ horizonMinutes: { type: 'number', description: '기본 30, 1~120' }
132
+ },
133
+ required: ['instanceId', 'kind', 'homeNode']
134
+ }
135
+ },
136
+ {
137
+ name: 'traceJournal',
138
+ description: '이벤트 저널(append-only)의 최근 타임라인을 조회한다. "언제부터/이력/시점" 질문의 근거. eventType 로 필터(equipment.status=설비 고장/복구, order.status=오더, task.status=작업). 반환: events[{revision, eventTime, eventType, info}] 시간 오름차순.',
139
+ parameters: {
140
+ type: 'object',
141
+ properties: {
142
+ instanceId: { type: 'string', description: '트윈 인스턴스 id' },
143
+ eventType: { type: 'string', description: "필터: 'equipment.status' | 'order.status' | 'task.status' 등(생략 시 전체)" },
144
+ limit: { type: 'number', description: '최근 N건(기본 25, 1~50)' }
145
+ },
146
+ required: ['instanceId']
147
+ }
148
+ },
149
+ {
150
+ name: 'proposeAction',
151
+ description: '사용자에게 조치 실행을 "제안"한다(직접 실행 아님 — 사용자가 확인 후 실행). "~보류해/재개해/고장/수리/추가해" 같은 요청에 쓴다. **지원 command 는 이것뿐(그 외 발명 금지)**: order.hold / order.resume / order.release / attention.ack / resource.hold / resource.resume / resource.down / resource.repair / resource.add. resource.add 의 args 는 {kind, homeNode, count} — 라이브 트윈에 자원(무버)을 실제로 추가한다("지게차 추가해줘"). **자원 제거·노드/구조 변경은 미지원 — 그런 요청엔 재프로비저닝을 안내.**',
152
+ parameters: {
153
+ type: 'object',
154
+ properties: {
155
+ command: { type: 'string', description: '커맨드 타입(order.hold/order.resume/order.release/attention.ack 등)' },
156
+ args: { type: 'object', description: '커맨드 인자(예: {"orderId":"order-7"})' },
157
+ label: { type: 'string', description: '사람용 짧은 설명(예: "order-7 보류 해제")' },
158
+ reason: { type: 'string', description: '이 조치를 제안하는 근거' }
159
+ },
160
+ required: ['command', 'label']
161
+ }
162
+ },
163
+ {
164
+ name: 'listTwinSystems',
165
+ description: '사용자가 무엇을 트윈으로 만들 수 있는지 모르거나("트윈 뭐 만들 수 있어?") 어떤 시스템이 자기 상황에 맞는지 물을 때, 가능한 시스템(wms=창고·yms=야드·mes=제조)과 각각 다루는 노드/자원을 반환한다. 이걸로 선택지를 보여주고 사용자 상황을 물어 맞는 시스템을 고른 뒤 suggestArchetype→proposeTwin 으로 잇는다. 파라미터 없음. 반환: { systems:[{system,label,nodeTypes,resourceTypes}] }.',
166
+ parameters: { type: 'object', properties: {} }
167
+ },
168
+ {
169
+ name: 'suggestArchetype',
170
+ description: '규모·구조가 모호한 요청("창고 만들어줘", "공장 하나 시뮬레이션")에 쓸, 시스템별 합리적 기본 트윈 구성(입고·보관·출고·자원 완비 → 실제로 작동)을 카탈로그 타입으로 반환한다. 반환된 nodes/movers 를 proposeTwin 에 그대로 넘기거나 사용자가 말한 규모/특이사항에 맞게 조정한다. 초보 입력을 완전한 트윈으로 만드는 지름길. 반환: { system, scale, nodes[{id,type}], movers[{kind,homeNode,count}], note }.',
171
+ parameters: {
172
+ type: 'object',
173
+ properties: {
174
+ system: { type: 'string', description: "'wms'(창고) | 'yms'(야드) | 'mes'(제조)" },
175
+ scale: { type: 'string', description: "규모: 'small' | 'medium'(기본) | 'large'. 사용자 표현(작은/큰 등)에 맞춰 고른다." }
176
+ },
177
+ required: ['system']
178
+ }
179
+ },
180
+ {
181
+ name: 'listTwinTypes',
182
+ description: '트윈을 새로 만들 때 쓸 수 있는 노드/무버 타입 팔레트를 커널 카탈로그(SSOT)에서 반환한다. 새 트윈 생성(proposeTwin) 전에 반드시 이 도구로 가능한 타입을 확인한다(타입 발명 금지). 반환: { system, label, nodeTypes[{key,label}], moverTypes[{key,label}] }.',
183
+ parameters: {
184
+ type: 'object',
185
+ properties: {
186
+ system: { type: 'string', description: "'wms'(창고) | 'yms'(야드) | 'mes'(제조)" }
187
+ },
188
+ required: ['system']
189
+ }
190
+ },
191
+ {
192
+ name: 'proposeTwin',
193
+ description: '새 트윈 인스턴스의 구조·좌표·시나리오 초안을 "제안"한다(직접 생성 아님 — 사용자가 확인 후 생성). zero-to-twin. 노드/무버 타입은 listTwinTypes 팔레트에서만 고른다. 좌표(layout)는 서버가 타입별 흐름 순서로 자동 배치하므로 지정하지 않는다. 시나리오(자극)를 생략하면 시스템 기본(입고 도착+출고 오더)을 붙인다.',
194
+ parameters: {
195
+ type: 'object',
196
+ properties: {
197
+ system: { type: 'string', description: "'wms' | 'yms' | 'mes'" },
198
+ instanceId: { type: 'string', description: '새 인스턴스 id(영문/숫자/하이픈, 예: "wh-seoul-1"). 사용자가 안 정하면 의미있는 값 제안.' },
199
+ label: { type: 'string', description: '사람용 이름(예: "서울 물류창고")' },
200
+ nodes: {
201
+ type: 'array',
202
+ description: '로케이션 노드. type 은 listTwinTypes 의 nodeTypes 에서만. id 는 유일.',
203
+ items: {
204
+ type: 'object',
205
+ properties: {
206
+ id: { type: 'string' },
207
+ type: { type: 'string', description: '카탈로그 노드 타입 key' },
208
+ capacity: { type: 'number', description: '수용량(생략 시 0=무제한 취급)' }
209
+ },
210
+ required: ['id', 'type']
211
+ }
212
+ },
213
+ movers: {
214
+ type: 'array',
215
+ description: '자원(무버). kind 는 listTwinTypes 의 moverTypes 에서만. homeNode 는 위 nodes 의 id.',
216
+ items: {
217
+ type: 'object',
218
+ properties: {
219
+ kind: { type: 'string', description: '카탈로그 무버 타입 key' },
220
+ homeNode: { type: 'string', description: '초기 배치 노드 id' },
221
+ count: { type: 'number', description: '대수(기본 1)' }
222
+ },
223
+ required: ['kind', 'homeNode']
224
+ }
225
+ }
226
+ },
227
+ required: ['system', 'instanceId', 'nodes']
228
+ }
229
+ },
230
+ {
231
+ name: 'refineTwin',
232
+ description: '이미 제안된 트윈을 사용자 요청대로 다듬어 재제안한다("더 크게/작게", "지게차 빼줘/5대로", "보관 6곳으로"). 직전 제안의 nodes/movers 를 그대로 넘기고 변경만 지정한다 — 카운트 계산은 도구가 하니 직접 재계산하지 말 것. 반환은 proposeTwin 과 동형(draft·viability·explanation·warnings).',
233
+ parameters: {
234
+ type: 'object',
235
+ properties: {
236
+ system: { type: 'string', description: "'wms'|'yms'|'mes' (직전 제안과 동일)" },
237
+ instanceId: { type: 'string', description: '직전 제안의 instanceId' },
238
+ label: { type: 'string', description: '이름(직전과 동일)' },
239
+ nodes: {
240
+ type: 'array',
241
+ description: '직전 제안의 현재 노드 [{id,type}]',
242
+ items: { type: 'object', properties: { id: { type: 'string' }, type: { type: 'string' } }, required: ['id', 'type'] }
243
+ },
244
+ movers: {
245
+ type: 'array',
246
+ description: '직전 제안의 현재 무버 [{kind,homeNode,count}]',
247
+ items: {
248
+ type: 'object',
249
+ properties: { kind: { type: 'string' }, homeNode: { type: 'string' }, count: { type: 'number' } },
250
+ required: ['kind', 'homeNode']
251
+ }
252
+ },
253
+ scale: { type: 'string', description: "'up'(더 크게) | 'down'(더 작게) — 전체 규모를 배로/반으로" },
254
+ setCounts: {
255
+ type: 'array',
256
+ description: '특정 구성요소 개수 지정. 예: [{"key":"storage","count":6}] 또는 [{"key":"forklift","count":5}]. count 0 = 제거.',
257
+ items: { type: 'object', properties: { key: { type: 'string' }, count: { type: 'number' } }, required: ['key', 'count'] }
258
+ },
259
+ removeKinds: { type: 'array', description: '통째로 뺄 타입/자원 키. 예: ["forklift"]', items: { type: 'string' } }
260
+ },
261
+ required: ['system', 'instanceId', 'nodes']
262
+ }
263
+ }
264
+ ];
265
+ /* 관심사별 모듈 분리(모놀리스 분할) — execTool 이 위임한다:
266
+ * · zero-to-twin.ts : 저작(listTwinSystems·suggestArchetype·listTwinTypes·proposeTwin·refineTwin·viability·explain)
267
+ * · read-predict.ts : 관측·예측(queryAttentions·queryState·traceJournal·forecast·whatIf·optimizeResource) */
268
+ /** 도구 실행 — canonical 소스(라이브 커널 스냅샷·이벤트 저널)에서 직접(in-process).
269
+ * export: P0 테스트가 접지·propose-only 게이트를 도구 단위로 검증(런타임 동작 무변경 — 테스트 이음매). */
270
+ async function execTool(name, args, domainId) {
271
+ if (name === 'summarizeStatus')
272
+ return (0, read_predict_ts_1.summarizeStatus)(args); // → read-predict.ts (운영측 만만함: health 한눈에)
273
+ if (name === 'queryAttentions')
274
+ return (0, read_predict_ts_1.queryAttentions)(args); // → read-predict.ts
275
+ if (name === 'queryState')
276
+ return (0, read_predict_ts_1.queryState)(args); // → read-predict.ts
277
+ if (name === 'forecast')
278
+ return (0, read_predict_ts_1.forecast)(args); // → read-predict.ts (몬테카를로)
279
+ if (name === 'whatIf')
280
+ return (0, read_predict_ts_1.whatIf)(args); // → read-predict.ts (구성 변주)
281
+ if (name === 'optimizeResource')
282
+ return (0, read_predict_ts_1.optimizeResource)(args); // → read-predict.ts (대수 스윕)
283
+ if (name === 'traceJournal')
284
+ return (0, read_predict_ts_1.traceJournal)(args, domainId); // → read-predict.ts (저널)
285
+ if (name === 'proposeAction') {
286
+ // 실행하지 않는다 — 제안만 기록. 실제 실행은 사용자 확인 후 dispatchTwinCommand(살아있는 시스템 게이트).
287
+ return { proposed: true, note: 'Proposal recorded (not executed). The user executes it via the confirm button.' };
288
+ }
289
+ if (name === 'listTwinSystems')
290
+ return (0, zero_to_twin_ts_1.listTwinSystems)(); // → zero-to-twin.ts (시작 이전 안내)
291
+ if (name === 'suggestArchetype')
292
+ return (0, zero_to_twin_ts_1.suggestArchetype)(args); // → zero-to-twin.ts (모호 입력→완비 기본)
293
+ if (name === 'listTwinTypes')
294
+ return (0, zero_to_twin_ts_1.listTwinTypes)(args); // → zero-to-twin.ts (카탈로그 접지)
295
+ if (name === 'proposeTwin')
296
+ return (0, zero_to_twin_ts_1.proposeTwin)(args); // → zero-to-twin.ts (propose-only + viability)
297
+ if (name === 'refineTwin')
298
+ return (0, zero_to_twin_ts_1.refineTwin)(args); // → zero-to-twin.ts (다듬기 델타 적용 후 재제안)
299
+ return { error: `unknown tool: ${name}` };
300
+ }
301
+ /* ── 접지 가드(무환각 코드 강제) ─────────────────────────────────────────────
302
+ * LLM 은 tool_result(JSON)·사용자 메시지·이력만 근거로 가진다. reply 가 언급한 엔티티 id 가
303
+ * 그 근거에 없으면 = 환각 후보. id 관례(하이픈 포함 소문자-영숫자: fk-1·weld-station·order-7)로
304
+ * 보수적으로 추출(하이픈 없는 일반어·한국어·p90·gtin 은 매칭 안 함) → 근거집합과 차집합.
305
+ * reply 는 변형하지 않고 구조화 경고만 노출(비파괴·클라 렌더·향후 재생성 토대). */
306
+ const ID_TOKEN = /[a-z][a-z0-9]*(?:-[a-z0-9]+)+/g;
307
+ function extractIdTokens(s) {
308
+ const out = new Set();
309
+ for (const m of (s ?? '').matchAll(ID_TOKEN))
310
+ out.add(m[0]);
311
+ return out;
312
+ }
313
+ /** reply 언급 id − 근거코퍼스 id = 미확인(환각 후보). */
314
+ function checkGrounding(reply, groundedCorpus) {
315
+ const grounded = extractIdTokens(groundedCorpus);
316
+ const mentioned = extractIdTokens(reply);
317
+ return [...mentioned].filter(id => !grounded.has(id));
318
+ }
319
+ /* 생성/조회 계열 도구 — 현재 인스턴스에 스코프되지 않음(instanceId 강제 주입 제외).
320
+ * proposeTwin 은 새 인스턴스를 만드는 것이라 주입하면 제안 id 가 덮어써진다. listTwinTypes 는 인스턴스 무관. */
321
+ const UNSCOPED_TOOLS = new Set(['proposeTwin', 'listTwinTypes', 'suggestArchetype', 'refineTwin', 'listTwinSystems']);
322
+ /** 신호(full attention 또는 trimmed signal)를 클라 entity360 가 읽는 attention 형태로 정규화.
323
+ * full=recommendedActions[], trimmed=suggestedAction(단일) → recommendedActions[] 로 통일. */
324
+ function anchorSignal(a) {
325
+ if (!a)
326
+ return undefined;
327
+ const acts = Array.isArray(a.recommendedActions) && a.recommendedActions.length
328
+ ? a.recommendedActions
329
+ : a.suggestedAction
330
+ ? [a.suggestedAction]
331
+ : [];
332
+ return { severity: a.severity, kind: a.kind, title: a.title, detail: a.detail, rationale: a.rationale, recommendedActions: acts };
333
+ }
334
+ /** 도구 결과에서 공간 앵커 수집(중복 제거). 두 경로 모두 커버: queryAttentions→out.attentions(full),
335
+ * summarizeStatus→out.signals(trimmed). kind 는 클라 focus 표준으로(노드=location), 신호(원인·조치)를
336
+ * attention 으로 함께 실어 공간 진입 시 재현되게 한다. attentions(full)가 signals(trimmed)를 덮도록 뒤에 둔다. */
337
+ function collectAnchors(out, into) {
338
+ const list = [...(out?.signals ?? []), ...(out?.attentions ?? [])];
339
+ for (const a of list) {
340
+ const an = a?.anchor ?? {};
341
+ const sig = anchorSignal(a);
342
+ if (an.moverId)
343
+ into.set(`mover:${an.moverId}`, { kind: 'mover', id: an.moverId, attention: sig });
344
+ if (an.nodeId)
345
+ into.set(`location:${an.nodeId}`, { kind: 'location', id: an.nodeId, attention: sig });
346
+ if (an.orderId)
347
+ into.set(`order:${an.orderId}`, { kind: 'order', id: an.orderId, attention: sig });
348
+ }
349
+ }
350
+ /**
351
+ * 접지된 단발 질의 — 완주 후 반환. tool 루프는 단순(단일 read 도구, 최대 4 iteration).
352
+ * Gemini providerMeta(thoughtSignature) 는 tool_use turn 에 반드시 echo(안 하면 400).
353
+ */
354
+ async function twinAiAsk(instanceId, message, domainId, history = [], locale) {
355
+ const client = (0, ai_client_base_1.getDefaultAIClient)();
356
+ if (!client?.chatWithTools) {
357
+ throw new Error('AI client (chatWithTools) not configured — check config.aiClient (provider/model/apiKey)');
358
+ }
359
+ // 로케일(프레임워크 제공값, 클라 i18next.language)이 오면 답변 언어를 명시 지시 — 메시지 언어에만
360
+ // 의존하지 않도록(짧거나 id 위주 질의·혼합 언어에도 안정). 구조화 라벨/카탈로그도 이 언어로 옮겨 말함.
361
+ const systemPrompt = locale
362
+ ? `${SYSTEM_PROMPT}\n- 사용자 로케일은 "${locale}" 이다. **답변과, 도구가 돌려준 영어 message·code·카탈로그 label 을 모두 이 언어로** 옮겨 전한다(메시지 언어와 무관).`
363
+ : SYSTEM_PROMPT;
364
+ // 대화 이력(클라 보관) 앞에 붙여 멀티턴 맥락 유지. 내부 tool_use/tool_result turn 은 넘기지 않고
365
+ // 사용자·어시스턴트 텍스트만 재현(필요하면 AI 가 도구를 다시 호출). 최근 12턴으로 제한(프롬프트 폭증 방지).
366
+ const priorTurns = (history ?? [])
367
+ .filter(h => h && typeof h.text === 'string' && h.text.trim())
368
+ .slice(-12)
369
+ .map(h => ({ role: h.role === 'ai' ? 'assistant' : 'user', content: h.text }));
370
+ const userTurn = `[instanceId=${instanceId}] ${message}`;
371
+ const messages = [...priorTurns, { role: 'user', content: userTurn }];
372
+ const used = [];
373
+ const anchors = new Map();
374
+ const proposals = [];
375
+ const provisions = [];
376
+ // 접지 근거 코퍼스 — LLM 이 실제로 받은 것: 사용자 입력 + 이력 텍스트 + 도구 출력(누적).
377
+ const grounded = [userTurn, ...priorTurns.map(t => (typeof t.content === 'string' ? t.content : ''))];
378
+ const finish = (reply) => ({
379
+ reply,
380
+ toolCalls: used,
381
+ anchors: [...anchors.values()],
382
+ proposedActions: proposals,
383
+ proposedProvisions: provisions,
384
+ groundingWarnings: checkGrounding(reply, grounded.join('\n'))
385
+ });
386
+ for (let i = 0; i < 4; i++) {
387
+ const res = await client.chatWithTools(messages, exports.TOOLS, { systemPrompt, maxTokens: 4096 });
388
+ if (!res.toolCalls?.length) {
389
+ return finish(res.text ?? '');
390
+ }
391
+ // assistant 의 tool_use turn + user 의 tool_result turn 을 누적(providerMeta echo 필수).
392
+ const assistantContent = [];
393
+ if (res.text)
394
+ assistantContent.push({ type: 'text', text: res.text });
395
+ for (const tc of res.toolCalls) {
396
+ assistantContent.push({ type: 'tool_use', id: tc.id, name: tc.name, arguments: tc.arguments, providerMeta: tc.providerMeta });
397
+ }
398
+ const toolResults = [];
399
+ for (const tc of res.toolCalls) {
400
+ used.push(tc.name);
401
+ if (tc.name === 'proposeAction') {
402
+ proposals.push({ command: tc.arguments.command, args: tc.arguments.args ?? {}, label: tc.arguments.label, reason: tc.arguments.reason });
403
+ }
404
+ // 생성/조회 계열은 현재 인스턴스에 스코프 안 함(proposeTwin=새 id). 그 외는 instanceId 강제(타 인스턴스 조회 방지).
405
+ const callArgs = UNSCOPED_TOOLS.has(tc.name) ? tc.arguments : { ...tc.arguments, instanceId };
406
+ const out = await execTool(tc.name, callArgs, domainId);
407
+ collectAnchors(out, anchors);
408
+ if ((tc.name === 'proposeTwin' || tc.name === 'refineTwin') && out?.draft) {
409
+ const o = out;
410
+ // viability·explanation 도 전달 — 클라 카드가 "작동 여부"·"평이한 구성"을 구조적으로 렌더(만만함, §12.6).
411
+ provisions.push({ ...o.draft, summary: o.summary, warnings: o.warnings, viability: o.viability, explanation: o.explanation });
412
+ }
413
+ const content = JSON.stringify(out);
414
+ grounded.push(content); // 도구 출력 = LLM 이 근거로 받는 사실(접지 가드 코퍼스)
415
+ toolResults.push({ type: 'tool_result', toolUseId: tc.id, content });
416
+ }
417
+ messages.push({ role: 'assistant', content: assistantContent });
418
+ messages.push({ role: 'user', content: toolResults });
419
+ }
420
+ return finish('(tool-iteration limit reached — please narrow the question and retry.)');
421
+ }
422
+ //# sourceMappingURL=assistant.js.map