Update ConnectAI codebase
@@ -3,15 +3,15 @@
|
|||||||
<!-- ASTRA:AUTO-START -->
|
<!-- ASTRA:AUTO-START -->
|
||||||
|
|
||||||
## Snapshot
|
## Snapshot
|
||||||
- **Workspace**: `ConnectAI` `v2.2.18` _(absolute path varies by environment; resolved from the active VS Code workspace)_
|
- **Workspace**: `ConnectAI` `v2.2.19` _(absolute path varies by environment; resolved from the active VS Code workspace)_
|
||||||
- **Description**: The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.
|
- **Description**: The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.
|
||||||
- **Stack**: TypeScript, Node.js, VS Code Extension, LM Studio SDK, Test runner
|
- **Stack**: TypeScript, Node.js, VS Code Extension, LM Studio SDK, Test runner
|
||||||
- **Stats**: 257 source files, ~52,121 lines across 5 top-level modules.
|
- **Stats**: 265 source files, ~52,981 lines across 5 top-level modules.
|
||||||
|
|
||||||
## Last Refresh
|
## Last Refresh
|
||||||
- **Time**: 2026-05-16T14:34:03.239Z
|
- **Time**: 2026-05-17T14:50:50.535Z
|
||||||
- **Files newly analysed**: 3
|
- **Files newly analysed**: 5
|
||||||
- **Files reused from cache**: 254
|
- **Files reused from cache**: 260
|
||||||
|
|
||||||
## Directory Map
|
## Directory Map
|
||||||
```mermaid
|
```mermaid
|
||||||
@@ -37,11 +37,11 @@ mindmap
|
|||||||
> Arrows: which top-level module imports from which.
|
> Arrows: which top-level module imports from which.
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
src["src/<br/>134 files"]
|
src["src/<br/>137 files"]
|
||||||
media["media/<br/>6 files"]
|
media["media/<br/>6 files"]
|
||||||
tests["tests/<br/>33 files"]
|
tests["tests/<br/>35 files"]
|
||||||
core_py["core_py/<br/>6 files"]
|
core_py["core_py/<br/>6 files"]
|
||||||
docs["docs/<br/>78 files"]
|
docs["docs/<br/>81 files"]
|
||||||
tests --> src
|
tests --> src
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -64,10 +64,10 @@ flowchart LR
|
|||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
### `src/` — 134 files, ~35,756 lines
|
### `src/` — 137 files, ~36,157 lines
|
||||||
|
|
||||||
**Sub-directories**
|
**Sub-directories**
|
||||||
- `src/features/` (61) — Astra Office — public API. 다음 세션에서 추가될 OfficeSnapshot presenter / schema 도 같은 entry 로 노출 예정. 현재 노출: full webview panel H
|
- `src/features/` (64) — Astra Office — public API. 다음 세션에서 추가될 OfficeSnapshot presenter / schema 도 같은 entry 로 노출 예정. 현재 노출: full webview panel H
|
||||||
- `src/core/` (15) — Astra Path Resolver (경로 해결기) Astra의 모든 데이터 파일(.astra 디렉토리)의 경로를 중앙에서 관리합니다. 확장 프로그램의 설치 경로(extensionUri) 기반으로 .astra 디렉토
|
- `src/core/` (15) — Astra Path Resolver (경로 해결기) Astra의 모든 데이터 파일(.astra 디렉토리)의 경로를 중앙에서 관리합니다. 확장 프로그램의 설치 경로(extensionUri) 기반으로 .astra 디렉토
|
||||||
- `src/memory/` (8) — Episodic Memory (일화 기억) 과거 대화/회의/결정의 맥락 흐름을 저장합니다. 세션 종료 시 자동으로 에피소드를 요약하여 저장합니다. "왜 이렇게 결정했는지", "어떤 흐름으로 진행했는지" 기록. 저장
|
- `src/memory/` (8) — Episodic Memory (일화 기억) 과거 대화/회의/결정의 맥락 흐름을 저장합니다. 세션 종료 시 자동으로 에피소드를 요약하여 저장합니다. "왜 이렇게 결정했는지", "어떤 흐름으로 진행했는지" 기록. 저장
|
||||||
- `src/retrieval/` (8) — Brain Index — persistent, mtime-keyed tokenized cache of the Second Brain RAG 검색은 매 질의마다 브레인의 모든 .md 파일을 읽고 토크나이즈해서 TF-I
|
- `src/retrieval/` (8) — Brain Index — persistent, mtime-keyed tokenized cache of the Second Brain RAG 검색은 매 질의마다 브레인의 모든 .md 파일을 읽고 토크나이즈해서 TF-I
|
||||||
@@ -92,32 +92,32 @@ flowchart LR
|
|||||||
- `src/retrieval/scoring.ts` (518 lines) — Scoring Engine — TF-IDF + Bilingual Tokenizer 단순 includes() 키워드 매칭을 넘어서, TF-IDF 가중치 기반의 문서 스코어링을 제공합니다. 한국어/영어 양국어 토크나이저를 포함합니다.
|
- `src/retrieval/scoring.ts` (518 lines) — Scoring Engine — TF-IDF + Bilingual Tokenizer 단순 includes() 키워드 매칭을 넘어서, TF-IDF 가중치 기반의 문서 스코어링을 제공합니다. 한국어/영어 양국어 토크나이저를 포함합니다.
|
||||||
- `src/skills/agentKnowledgeMap.ts` (374 lines)
|
- `src/skills/agentKnowledgeMap.ts` (374 lines)
|
||||||
- `src/retrieval/lessonHelpers.ts` (325 lines) — Lesson / Experience Memory — pure helpers (no vscode dependency) "Lesson" = a markdown file in the active brain that captures a past mistake/risk and how to avoid repeating it. Identified by a lessons
|
- `src/retrieval/lessonHelpers.ts` (325 lines) — Lesson / Experience Memory — pure helpers (no vscode dependency) "Lesson" = a markdown file in the active brain that captures a past mistake/risk and how to avoid repeating it. Identified by a lessons
|
||||||
- `src/agent.ts` (3579 lines)
|
- `src/agent.ts` (3633 lines)
|
||||||
- `src/features/providers/types.ts` (63 lines) — Cloud LLM provider routing — model id prefix → provider id 매핑. Prefix 규칙: openrouter:anthropic/claude-3.5-sonnet → { provider: 'openrouter', model: 'anthropic/claude-3.5-sonnet' } anthropic:claude-3-5
|
- `src/features/providers/types.ts` (63 lines) — Cloud LLM provider routing — model id prefix → provider id 매핑. Prefix 규칙: openrouter:anthropic/claude-3.5-sonnet → { provider: 'openrouter', model: 'anthropic/claude-3.5-sonnet' } anthropic:claude-3-5
|
||||||
- `src/lib/engine.ts` (906 lines)
|
- `src/lib/engine.ts` (906 lines)
|
||||||
- `src/features/company/dispatcher.ts` (1435 lines) — Sequential dispatcher for 1인 기업 모드. Drives one company "turn": user prompt → CEO planner (JSON {brief, tasks}) → for each task in plan: dispatch one specialist (sequentially) - build specialist prompt
|
- `src/features/company/dispatcher.ts` (1435 lines) — Sequential dispatcher for 1인 기업 모드. Drives one company "turn": user prompt → CEO planner (JSON {brief, tasks}) → for each task in plan: dispatch one specialist (sequentially) - build specialist prompt
|
||||||
- `src/features/providers/providerConfig.ts` (78 lines) — Provider 별 API key + enable 토글 저장소. 설계: - API key 자체는 vscode.SecretStorage (secrets) 에 — settings.json / Settings Sync 침범 안 받음. - enabled 토글은 일반 settings (g1nation.providers.<id>.enabled) — 사용자가 패널에서
|
- `src/features/providers/providerConfig.ts` (78 lines) — Provider 별 API key + enable 토글 저장소. 설계: - API key 자체는 vscode.SecretStorage (secrets) 에 — settings.json / Settings Sync 침범 안 받음. - enabled 토글은 일반 settings (g1nation.providers.<id>.enabled) — 사용자가 패널에서
|
||||||
- `src/features/approval/approvalQueue.ts` (129 lines)
|
- `src/features/approval/approvalQueue.ts` (129 lines)
|
||||||
- `src/integrations/telegram/telegramClient.ts` (154 lines)
|
- `src/integrations/telegram/telegramClient.ts` (154 lines)
|
||||||
- `src/features/astraOffice/view/runtime.ts` (1765 lines) — 자동 분리: src/sidebarProvider.ts 4002-5116 (IIFE 본문) 에서 추출. 동작 동등. ${assets.derivedBase} placeholder 는 panelHtml 에서 .replace() 로 실제 값 주입. 다음 세션에서 OfficeSnapshot 기반으로 단계적으로 잘라낼 예정.
|
- `src/features/astraOffice/view/runtime.ts` (1887 lines) — 자동 분리: src/sidebarProvider.ts 4002-5116 (IIFE 본문) 에서 추출. 동작 동등. ${assets.derivedBase} placeholder 는 panelHtml 에서 .replace() 로 실제 값 주입. 다음 세션에서 OfficeSnapshot 기반으로 단계적으로 잘라낼 예정.
|
||||||
- `src/features/company/agents.ts` (211 lines) — 기본 에이전트 로스터 — 1인 기업 모드의 출고 디폴트. 설계 의도: 소프트웨어/게임 개발 IT 회사의 1인 기업 운영을 가정. 한 사람이 기획 → 디자인 → 개발 → QA → 출시 → 운영/마케팅을 모두 책임질 때 필요한 직군을 빠짐없이 커버하되 역할이 겹치지 않게 분리한다. 직군 구분 (혼동 방지): - 기획자(business) : 무엇을 만들지 정의
|
- `src/features/company/agents.ts` (211 lines) — 기본 에이전트 로스터 — 1인 기업 모드의 출고 디폴트. 설계 의도: 소프트웨어/게임 개발 IT 회사의 1인 기업 운영을 가정. 한 사람이 기획 → 디자인 → 개발 → QA → 출시 → 운영/마케팅을 모두 책임질 때 필요한 직군을 빠짐없이 커버하되 역할이 겹치지 않게 분리한다. 직군 구분 (혼동 방지): - 기획자(business) : 무엇을 만들지 정의
|
||||||
- `src/features/company/pixelOfficeState.ts` (286 lines) — Pixel Office — Agent Work Pipeline 상태를 시각화하는 UI Layer 전용 모듈. ─────────────────── 설계 원칙 ─────────────────── 1. Agent 핵심 판단 로직을 절대 바꾸지 않는다. Pipeline 진행, contract 합의, 검수 cycle, 승인 게이트 — 모두 기존 dispatcher
|
- `src/features/company/pixelOfficeState.ts` (286 lines) — Pixel Office — Agent Work Pipeline 상태를 시각화하는 UI Layer 전용 모듈. ─────────────────── 설계 원칙 ─────────────────── 1. Agent 핵심 판단 로직을 절대 바꾸지 않는다. Pipeline 진행, contract 합의, 검수 cycle, 승인 게이트 — 모두 기존 dispatcher
|
||||||
- `src/features/company/sessionStore.ts` (231 lines) — Disk persistence for company-mode session artefacts. Each company turn produces a timestamped directory: <workspaceRoot>/.astra/company/sessions/2026-05-13T21-29/ ├─ brief.md ← CEO's task decompositio
|
- `src/features/company/sessionStore.ts` (231 lines) — Disk persistence for company-mode session artefacts. Each company turn produces a timestamped directory: <workspaceRoot>/.astra/company/sessions/2026-05-13T21-29/ ├─ brief.md ← CEO's task decompositio
|
||||||
- `src/features/projectArchitecture/scanner.ts` (644 lines) — Deep static analyser for the Project Architecture Context generator. Walks the project tree (skipping the usual nodemodules / out / dist noise), pulls the role of each interesting file from its leadin
|
- `src/features/projectArchitecture/scanner.ts` (644 lines) — Deep static analyser for the Project Architecture Context generator. Walks the project tree (skipping the usual nodemodules / out / dist noise), pulls the role of each interesting file from its leadin
|
||||||
- `src/lib/contextManager.ts` (275 lines) — Context Manager (컨텍스트 한계 관리) "context length = 132k" 는 "답변을 132k 토큰까지 생성해도 된다" 가 아닙니다. 시스템 프롬프트 + 대화 기록 + 입력 문서 + 생성될 답변 + 여유분 ≤ context length 이 모듈은 요청을 보내기 전에 입력 토큰을 추정하고, - 동적으로 출력 상한(maxTokens)을 계
|
- `src/lib/contextManager.ts` (275 lines) — Context Manager (컨텍스트 한계 관리) "context length = 132k" 는 "답변을 132k 토큰까지 생성해도 된다" 가 아닙니다. 시스템 프롬프트 + 대화 기록 + 입력 문서 + 생성될 답변 + 여유분 ≤ context length 이 모듈은 요청을 보내기 전에 입력 토큰을 추정하고, - 동적으로 출력 상한(maxTokens)을 계
|
||||||
- `src/extension.ts` (1202 lines)
|
- `src/extension.ts` (1215 lines)
|
||||||
|
|
||||||
### `media/` — 6 files, ~7,011 lines
|
### `media/` — 6 files, ~7,243 lines
|
||||||
|
|
||||||
**Key files**
|
**Key files**
|
||||||
- `media/sidebar.css` (2016 lines) — Stylesheet
|
- `media/sidebar.css` (2068 lines) — Stylesheet
|
||||||
- `media/sidebar.js` (3657 lines)
|
- `media/sidebar.js` (3680 lines)
|
||||||
- `media/sidebar.html` (546 lines) — Astra
|
- `media/sidebar.html` (546 lines) — Astra
|
||||||
|
- `media/settings-panel.html` (328 lines) — Astra Settings
|
||||||
- `media/settings-panel.css` (210 lines) — Stylesheet
|
- `media/settings-panel.css` (210 lines) — Stylesheet
|
||||||
- `media/settings-panel.html` (244 lines) — Astra Settings
|
- `media/settings-panel.js` (411 lines)
|
||||||
- `media/settings-panel.js` (338 lines)
|
|
||||||
|
|
||||||
### `tests/` — 33 files, ~5,811 lines
|
### `tests/` — 35 files, ~5,969 lines
|
||||||
*Depends on*: `src/`
|
*Depends on*: `src/`
|
||||||
|
|
||||||
**Sub-directories**
|
**Sub-directories**
|
||||||
@@ -160,10 +160,10 @@ flowchart LR
|
|||||||
- `core_py/optimizer.py` (55 lines)
|
- `core_py/optimizer.py` (55 lines)
|
||||||
- `core_py/queue_worker.py` (82 lines)
|
- `core_py/queue_worker.py` (82 lines)
|
||||||
|
|
||||||
### `docs/` — 78 files, ~3,134 lines
|
### `docs/` — 81 files, ~3,203 lines
|
||||||
|
|
||||||
**Sub-directories**
|
**Sub-directories**
|
||||||
- `docs/records/` (65) — Astra Project Chronicle Records
|
- `docs/records/` (68) — Astra Project Chronicle Records
|
||||||
- `docs/docs/` (5) — docs Chronicle Records
|
- `docs/docs/` (5) — docs Chronicle Records
|
||||||
|
|
||||||
**Key files**
|
**Key files**
|
||||||
@@ -173,7 +173,7 @@ flowchart LR
|
|||||||
- `docs/EXPERIENCE_MEMORY_PLAN.md` (122 lines) — Experience Memory (Mistake / Lesson Loop) — Implementation Plan
|
- `docs/EXPERIENCE_MEMORY_PLAN.md` (122 lines) — Experience Memory (Mistake / Lesson Loop) — Implementation Plan
|
||||||
- `docs/records/ConnectAI/development/2026-05-02_connectai_project_knowledge_overview.md` (121 lines) — Astra Project Knowledge Overview
|
- `docs/records/ConnectAI/development/2026-05-02_connectai_project_knowledge_overview.md` (121 lines) — Astra Project Knowledge Overview
|
||||||
- `docs/records/ConnectAI/development/2026-05-03_connectai_project_knowledge_overview.md` (121 lines) — Astra Project Knowledge Overview
|
- `docs/records/ConnectAI/development/2026-05-03_connectai_project_knowledge_overview.md` (121 lines) — Astra Project Knowledge Overview
|
||||||
- `docs/records/ConnectAI/timeline.md` (146 lines) — Project Timeline
|
- `docs/records/ConnectAI/timeline.md` (155 lines) — Project Timeline
|
||||||
- `docs/Advanced_Features_Implementation_Guide.md` (40 lines) — Advanced Features Implementation Guide
|
- `docs/Advanced_Features_Implementation_Guide.md` (40 lines) — Advanced Features Implementation Guide
|
||||||
- `docs/PROJECT_CHRONICLE_GUARD_ROADMAP.md` (43 lines) — Project Chronicle Guard: Search Engine Roadmap
|
- `docs/PROJECT_CHRONICLE_GUARD_ROADMAP.md` (43 lines) — Project Chronicle Guard: Search Engine Roadmap
|
||||||
- `docs/UX_UI_Consistency_Guidelines.md` (44 lines) — UX/UI Consistency Guidelines
|
- `docs/UX_UI_Consistency_Guidelines.md` (44 lines) — UX/UI Consistency Guidelines
|
||||||
@@ -196,7 +196,7 @@ flowchart LR
|
|||||||
## VS Code Extension Surface
|
## VS Code Extension Surface
|
||||||
- **Extension ID**: `g1nation.astra`
|
- **Extension ID**: `g1nation.astra`
|
||||||
- **Activation events**: `onStartupFinished`
|
- **Activation events**: `onStartupFinished`
|
||||||
- **Commands** (27):
|
- **Commands** (28):
|
||||||
- `g1nation.newChat` — Astra: New Chat
|
- `g1nation.newChat` — Astra: New Chat
|
||||||
- `g1nation.exportChat` — Astra: Export Chat as Markdown
|
- `g1nation.exportChat` — Astra: Export Chat as Markdown
|
||||||
- `g1nation.explainSelection` — Astra: Explain Selected Code
|
- `g1nation.explainSelection` — Astra: Explain Selected Code
|
||||||
@@ -224,7 +224,8 @@ flowchart LR
|
|||||||
- `g1nation.calendar.connect` — Astra: Google Calendar (iCal) 연결 📅
|
- `g1nation.calendar.connect` — Astra: Google Calendar (iCal) 연결 📅
|
||||||
- `g1nation.calendar.refresh` — Astra: Google Calendar 새로고침 📅
|
- `g1nation.calendar.refresh` — Astra: Google Calendar 새로고침 📅
|
||||||
- `g1nation.calendar.connectOAuth` — Astra: Google Calendar OAuth 연결 (쓰기) 🔐
|
- `g1nation.calendar.connectOAuth` — Astra: Google Calendar OAuth 연결 (쓰기) 🔐
|
||||||
- **Configuration** (62 settings):
|
- `g1nation.devilAgent.toggle` — Astra: Toggle Devil Agent 🎭
|
||||||
|
- **Configuration** (63 settings):
|
||||||
- `g1nation.multiAgentEnabled` *(boolean)* _(default: `false`)_ — Enable Multi-Agent Workflow (Planner -> Researcher -> Writer) for complex tasks.
|
- `g1nation.multiAgentEnabled` *(boolean)* _(default: `false`)_ — Enable Multi-Agent Workflow (Planner -> Researcher -> Writer) for complex tasks.
|
||||||
- `g1nation.memoryEnabled` *(boolean)* _(default: `true`)_ — Enable layered memory injection before each model response.
|
- `g1nation.memoryEnabled` *(boolean)* _(default: `true`)_ — Enable layered memory injection before each model response.
|
||||||
- `g1nation.memoryShortTermMessages` *(number)* _(default: `8`)_ — Number of recent conversation messages included as short-term memory.
|
- `g1nation.memoryShortTermMessages` *(number)* _(default: `8`)_ — Number of recent conversation messages included as short-term memory.
|
||||||
@@ -285,7 +286,7 @@ flowchart LR
|
|||||||
- `g1nation.providers.openrouter.defaultModel` *(string)* _(default: `""`)_ — OpenRouter 의 기본 모델 (예: 'anthropic/claude-3.5-sonnet'). 모델 선택 시 'openrouter:<model>' 형식으로 사이드바 dropdown 에 표시.
|
- `g1nation.providers.openrouter.defaultModel` *(string)* _(default: `""`)_ — OpenRouter 의 기본 모델 (예: 'anthropic/claude-3.5-sonnet'). 모델 선택 시 'openrouter:<model>' 형식으로 사이드바 dropdown 에 표시.
|
||||||
- `g1nation.providers.anthropic.enabled` *(boolean)* _(default: `false`)_ — Anthropic Claude 직접 API 활성화. OpenRouter 보다 마진 적고 prompt caching 등 native 기능 사용 가능. API key 는 Secret Storage.
|
- `g1nation.providers.anthropic.enabled` *(boolean)* _(default: `false`)_ — Anthropic Claude 직접 API 활성화. OpenRouter 보다 마진 적고 prompt caching 등 native 기능 사용 가능. API key 는 Secret Storage.
|
||||||
- `g1nation.providers.anthropic.defaultModel` *(string)* _(default: `"claude-3-5-sonnet-20241022"`)_ — Anthropic 의 기본 모델. 예: 'claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022'.
|
- `g1nation.providers.anthropic.defaultModel` *(string)* _(default: `"claude-3-5-sonnet-20241022"`)_ — Anthropic 의 기본 모델. 예: 'claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022'.
|
||||||
- _…and 2 more_
|
- _…and 3 more_
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
- **Runtime** (2): `@lmstudio/sdk`, `pdf-parse`
|
- **Runtime** (2): `@lmstudio/sdk`, `pdf-parse`
|
||||||
@@ -333,7 +334,7 @@ Astra는 대표님의 명시적인 승인 하에 로컬 시스템의 강력한
|
|||||||
**Designed for High-Performance Decision Making.**
|
**Designed for High-Performance Decision Making.**
|
||||||
Copyright (C) **g1nation**. All rights reserved.
|
Copyright (C) **g1nation**. All rights reserved.
|
||||||
|
|
||||||
_Last auto-scan: 2026-05-16T14:34:03.239Z · signature `857a73ab`_
|
_Last auto-scan: 2026-05-17T14:50:50.535Z · signature `5421df2d`_
|
||||||
<!-- ASTRA:AUTO-END -->
|
<!-- ASTRA:AUTO-END -->
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"generatedAt": "2026-05-16T14:34:03.249Z",
|
"generatedAt": "2026-05-17T14:50:50.547Z",
|
||||||
"files": {
|
"files": {
|
||||||
"src/agent.ts": {
|
"src/agent.ts": {
|
||||||
"mtimeMs": 1778941597000,
|
"mtimeMs": 1778943938000,
|
||||||
"size": 205909,
|
"size": 209036,
|
||||||
"lines": 3579,
|
"lines": 3633,
|
||||||
"role": "",
|
"role": "",
|
||||||
"imports": [
|
"imports": [
|
||||||
"src/utils",
|
"src/utils",
|
||||||
@@ -260,9 +260,9 @@
|
|||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
"src/extension.ts": {
|
"src/extension.ts": {
|
||||||
"mtimeMs": 1778938013000,
|
"mtimeMs": 1778943661000,
|
||||||
"size": 62397,
|
"size": 63135,
|
||||||
"lines": 1202,
|
"lines": 1215,
|
||||||
"role": "",
|
"role": "",
|
||||||
"imports": [
|
"imports": [
|
||||||
"src/utils",
|
"src/utils",
|
||||||
@@ -292,7 +292,8 @@
|
|||||||
"src/retrieval/lessonHelpers",
|
"src/retrieval/lessonHelpers",
|
||||||
"src/skills/scopedBrainRetriever",
|
"src/skills/scopedBrainRetriever",
|
||||||
"src/integrations/telegram/conversationHistory",
|
"src/integrations/telegram/conversationHistory",
|
||||||
"src/features/calendar"
|
"src/features/calendar",
|
||||||
|
"src/features/devilAgent"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/features/approval/approvalPanelProvider.ts": {
|
"src/features/approval/approvalPanelProvider.ts": {
|
||||||
@@ -370,9 +371,9 @@
|
|||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
"src/features/astraOffice/view/officeStyles.ts": {
|
"src/features/astraOffice/view/officeStyles.ts": {
|
||||||
"mtimeMs": 1778938908000,
|
"mtimeMs": 1778942662000,
|
||||||
"size": 23999,
|
"size": 25747,
|
||||||
"lines": 401,
|
"lines": 419,
|
||||||
"role": "",
|
"role": "",
|
||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
@@ -388,9 +389,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/features/astraOffice/view/runtime.ts": {
|
"src/features/astraOffice/view/runtime.ts": {
|
||||||
"mtimeMs": 1778941900000,
|
"mtimeMs": 1779028539000,
|
||||||
"size": 82015,
|
"size": 90525,
|
||||||
"lines": 1765,
|
"lines": 1887,
|
||||||
"role": "자동 분리: src/sidebarProvider.ts 4002-5116 (IIFE 본문) 에서 추출. 동작 동등. ${assets.derivedBase} placeholder 는 panelHtml 에서 .replace() 로 실제 값 주입. 다음 세션에서 OfficeSnapshot 기반으로 단계적으로 잘라낼 예정.",
|
"role": "자동 분리: src/sidebarProvider.ts 4002-5116 (IIFE 본문) 에서 추출. 동작 동등. ${assets.derivedBase} placeholder 는 panelHtml 에서 .replace() 로 실제 값 주입. 다음 세션에서 OfficeSnapshot 기반으로 단계적으로 잘라낼 예정.",
|
||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
@@ -650,6 +651,32 @@
|
|||||||
"role": "Type definitions for the 1인 기업 (One-Person Company) mode. The mode turns the user into a virtual CEO that dispatches work to a roster of specialist agents. Each turn produces a session directory conta",
|
"role": "Type definitions for the 1인 기업 (One-Person Company) mode. The mode turns the user into a virtual CEO that dispatches work to a roster of specialist agents. Each turn produces a session directory conta",
|
||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
|
"src/features/devilAgent/devilPrompt.ts": {
|
||||||
|
"mtimeMs": 1778943576000,
|
||||||
|
"size": 3591,
|
||||||
|
"lines": 71,
|
||||||
|
"role": "Devil's Advocate (도현) — system prompt 빌더. 설계 원칙: - 모든 약점을 나열하지 않음. 한 turn 에 1개 — 사용자가 깊게 생각하게. - 'X 입장에서 본다면' framing — 본인 의견이 아니라 다른 시점 으로 제시. 이는 LLM 의 '내가 옳다' 경향을 줄이는 잘 알려진 패턴. - 통계 / 구체 수치 / 외부 사례 ",
|
||||||
|
"imports": []
|
||||||
|
},
|
||||||
|
"src/features/devilAgent/devilService.ts": {
|
||||||
|
"mtimeMs": 1778943601000,
|
||||||
|
"size": 2457,
|
||||||
|
"lines": 60,
|
||||||
|
"role": "Devil Agent — 직전 답변에 대한 반박 생성기. 동작: 1. agent.ts 가 main turn 완료 직후 호출 2. 같은 model/engine 으로 별도 LLM call 1회 — 짧은 비판 한 문단 생성 3. 결과를 webview 로 'devilRebuttal' message 로 send 4. 실패 / 비활성 시 silent skip — ma",
|
||||||
|
"imports": [
|
||||||
|
"src/features/devilAgent/devilPrompt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"src/features/devilAgent/index.ts": {
|
||||||
|
"mtimeMs": 1778943609000,
|
||||||
|
"size": 263,
|
||||||
|
"lines": 13,
|
||||||
|
"role": "",
|
||||||
|
"imports": [
|
||||||
|
"src/features/devilAgent/devilPrompt",
|
||||||
|
"src/features/devilAgent/devilService"
|
||||||
|
]
|
||||||
|
},
|
||||||
"src/features/projectArchitecture/index.ts": {
|
"src/features/projectArchitecture/index.ts": {
|
||||||
"mtimeMs": 1778691699000,
|
"mtimeMs": 1778691699000,
|
||||||
"size": 25373,
|
"size": 25373,
|
||||||
@@ -844,9 +871,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/features/settings/settingsPanelProvider.ts": {
|
"src/features/settings/settingsPanelProvider.ts": {
|
||||||
"mtimeMs": 1778937992000,
|
"mtimeMs": 1778943856000,
|
||||||
"size": 26837,
|
"size": 29429,
|
||||||
"lines": 596,
|
"lines": 646,
|
||||||
"role": "",
|
"role": "",
|
||||||
"imports": [
|
"imports": [
|
||||||
"src/integrations/telegram/telegramClient",
|
"src/integrations/telegram/telegramClient",
|
||||||
@@ -854,7 +881,8 @@
|
|||||||
"src/utils",
|
"src/utils",
|
||||||
"src/lib/discoverModels",
|
"src/lib/discoverModels",
|
||||||
"src/lib/paths",
|
"src/lib/paths",
|
||||||
"src/features/calendar"
|
"src/features/calendar",
|
||||||
|
"src/features/providers"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/features/sheets/index.ts": {
|
"src/features/sheets/index.ts": {
|
||||||
@@ -1354,23 +1382,23 @@
|
|||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
"media/settings-panel.html": {
|
"media/settings-panel.html": {
|
||||||
"mtimeMs": 1778937850000,
|
"mtimeMs": 1778943830000,
|
||||||
"size": 12095,
|
"size": 17362,
|
||||||
"lines": 244,
|
"lines": 328,
|
||||||
"role": "Astra Settings",
|
"role": "Astra Settings",
|
||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
"media/settings-panel.js": {
|
"media/settings-panel.js": {
|
||||||
"mtimeMs": 1778938082000,
|
"mtimeMs": 1778943881000,
|
||||||
"size": 15189,
|
"size": 19114,
|
||||||
"lines": 338,
|
"lines": 411,
|
||||||
"role": "",
|
"role": "",
|
||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
"media/sidebar.css": {
|
"media/sidebar.css": {
|
||||||
"mtimeMs": 1778934126000,
|
"mtimeMs": 1778943817000,
|
||||||
"size": 86702,
|
"size": 88173,
|
||||||
"lines": 2016,
|
"lines": 2068,
|
||||||
"role": "Stylesheet",
|
"role": "Stylesheet",
|
||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
@@ -1382,9 +1410,9 @@
|
|||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
"media/sidebar.js": {
|
"media/sidebar.js": {
|
||||||
"mtimeMs": 1778934151000,
|
"mtimeMs": 1778943769000,
|
||||||
"size": 211710,
|
"size": 214022,
|
||||||
"lines": 3657,
|
"lines": 3680,
|
||||||
"role": "",
|
"role": "",
|
||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
@@ -1443,6 +1471,15 @@
|
|||||||
"src/core/dataProcessor"
|
"src/core/dataProcessor"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"tests/devilAgent.test.ts": {
|
||||||
|
"mtimeMs": 1778943907000,
|
||||||
|
"size": 3179,
|
||||||
|
"lines": 61,
|
||||||
|
"role": "",
|
||||||
|
"imports": [
|
||||||
|
"src/features/devilAgent/devilPrompt"
|
||||||
|
]
|
||||||
|
},
|
||||||
"tests/findBrainFilesCache.test.ts": {
|
"tests/findBrainFilesCache.test.ts": {
|
||||||
"mtimeMs": 1778247665000,
|
"mtimeMs": 1778247665000,
|
||||||
"size": 2720,
|
"size": 2720,
|
||||||
@@ -1573,6 +1610,16 @@
|
|||||||
"src/scaffolder/projectScaffolder"
|
"src/scaffolder/projectScaffolder"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"tests/providers.test.ts": {
|
||||||
|
"mtimeMs": 1778942187000,
|
||||||
|
"size": 4118,
|
||||||
|
"lines": 97,
|
||||||
|
"role": "",
|
||||||
|
"imports": [
|
||||||
|
"src/features/providers/types",
|
||||||
|
"src/features/providers/streamHelpers"
|
||||||
|
]
|
||||||
|
},
|
||||||
"tests/resilience_stress.test.ts": {
|
"tests/resilience_stress.test.ts": {
|
||||||
"mtimeMs": 1777968922000,
|
"mtimeMs": 1777968922000,
|
||||||
"size": 6981,
|
"size": 6981,
|
||||||
@@ -1930,7 +1977,7 @@
|
|||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
"docs/records/ConnectAI/chronicle.config.json": {
|
"docs/records/ConnectAI/chronicle.config.json": {
|
||||||
"mtimeMs": 1778940893000,
|
"mtimeMs": 1779029444000,
|
||||||
"size": 416,
|
"size": 416,
|
||||||
"lines": 11,
|
"lines": 11,
|
||||||
"role": "JSON configuration",
|
"role": "JSON configuration",
|
||||||
@@ -2034,6 +2081,20 @@
|
|||||||
"role": "ADR: ASTRA OFFICE 부분 관련해서 개선할 부분이 너무 많아. 개선점이 무었이 있는지 의견 주면 좋겠어.",
|
"role": "ADR: ASTRA OFFICE 부분 관련해서 개선할 부분이 너무 많아. 개선점이 무었이 있는지 의견 주면 좋겠어.",
|
||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
|
"docs/records/ConnectAI/decisions/ADR-0015-너의-생각은-사용자가-다음-행동을-추측-안-해도-되는-astra-이게-가장-큰-효과-있을-거-같습니다-3개-.md": {
|
||||||
|
"mtimeMs": 1778943211000,
|
||||||
|
"size": 2482,
|
||||||
|
"lines": 19,
|
||||||
|
"role": "ADR: 너의 생각은? \"사용자가 다음 행동을 추측 안 해도 되는 Astra\" 이게 가장 큰 효과 있을 거 같습니다. 3개 작업으로 묶어볼 수 있어요: ...",
|
||||||
|
"imports": []
|
||||||
|
},
|
||||||
|
"docs/records/ConnectAI/decisions/ADR-0016-내가-블로그글-작성시-사용하고-있는-지침서인데-slot1번-기준으로-더-자연스럽고-트랜디하고-구독자에게-자연.md": {
|
||||||
|
"mtimeMs": 1779029444000,
|
||||||
|
"size": 2432,
|
||||||
|
"lines": 19,
|
||||||
|
"role": "ADR: 내가 블로그글 작성시 사용하고 있는 지침서인데 slot1번 기준으로 더 자연스럽고 트랜디하고 구독자에게 자연스럽게 정보를 전달 할 수 있게 개선...",
|
||||||
|
"imports": []
|
||||||
|
},
|
||||||
"docs/records/ConnectAI/development/2026-05-02_answer-format-readability-tuning.md": {
|
"docs/records/ConnectAI/development/2026-05-02_answer-format-readability-tuning.md": {
|
||||||
"mtimeMs": 1777808065000,
|
"mtimeMs": 1777808065000,
|
||||||
"size": 1534,
|
"size": 1534,
|
||||||
@@ -2195,6 +2256,13 @@
|
|||||||
"role": "Development Log: Astra: Google Calendar OAuth 연결",
|
"role": "Development Log: Astra: Google Calendar OAuth 연결",
|
||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
|
"docs/records/ConnectAI/development/2026-05-16_astra-office가-아직은-많이-부족한것-같은데-어떻게-하면-트랜디하고-재미있는-볼거리가-될-수-있을지_implementation.md": {
|
||||||
|
"mtimeMs": 1778942937000,
|
||||||
|
"size": 1750,
|
||||||
|
"lines": 22,
|
||||||
|
"role": "Development Log: astra office가 아직은 많이 부족한것 같은데 어떻게 하면 트랜디하고 재미있는 볼거리가 될 수 있을지. 개선점 줄 수 있어?",
|
||||||
|
"imports": []
|
||||||
|
},
|
||||||
"docs/records/ConnectAI/discussions/2026-05-13_volumes-data-project-antigravity-connectai-이-프로젝트-작업-할-거야.md": {
|
"docs/records/ConnectAI/discussions/2026-05-13_volumes-data-project-antigravity-connectai-이-프로젝트-작업-할-거야.md": {
|
||||||
"mtimeMs": 1778690673000,
|
"mtimeMs": 1778690673000,
|
||||||
"size": 652,
|
"size": 652,
|
||||||
@@ -2273,9 +2341,9 @@
|
|||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
"docs/records/ConnectAI/timeline.md": {
|
"docs/records/ConnectAI/timeline.md": {
|
||||||
"mtimeMs": 1778937290000,
|
"mtimeMs": 1779029444000,
|
||||||
"size": 9336,
|
"size": 9978,
|
||||||
"lines": 146,
|
"lines": 155,
|
||||||
"role": "Project Timeline",
|
"role": "Project Timeline",
|
||||||
"imports": []
|
"imports": []
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
||||||
"createdAt": 1778941922044,
|
"createdAt": 1778943957572,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||||
"createdAt": 1778941922043,
|
"createdAt": 1778943957562,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
||||||
"createdAt": 1778941922042,
|
"createdAt": 1778943957561,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "---\nid: stress_conflict_1778941922031\ndate: 2026-05-16T14:32:02.046Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (11ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (1ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (0ms)\n",
|
"result": "---\nid: stress_conflict_1778943957542\ndate: 2026-05-16T15:05:57.576Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (13ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (6ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (6ms)\n",
|
||||||
"createdAt": 1778941922046,
|
"createdAt": 1778943957576,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"missionId": "stress_conflict_1778941922031",
|
"missionId": "stress_conflict_1778943957542",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"startTime": "2026-05-16T14:32:02.031Z",
|
"startTime": "2026-05-16T15:05:57.542Z",
|
||||||
"totalElapsedMs": 15,
|
"totalElapsedMs": 35,
|
||||||
"results": {
|
"results": {
|
||||||
"planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
"planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
||||||
"researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
"researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||||
@@ -16,30 +16,30 @@
|
|||||||
{
|
{
|
||||||
"from": "idle",
|
"from": "idle",
|
||||||
"to": "planner",
|
"to": "planner",
|
||||||
"durationMs": 11,
|
"durationMs": 13,
|
||||||
"message": "전략 수립 중...",
|
"message": "전략 수립 중...",
|
||||||
"ts": "2026-05-16T14:32:02.042Z"
|
"ts": "2026-05-16T15:05:57.555Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "planner",
|
"from": "planner",
|
||||||
"to": "researcher",
|
"to": "researcher",
|
||||||
"durationMs": 1,
|
"durationMs": 6,
|
||||||
"message": "핵심 정보 수집 및 분석 중...",
|
"message": "핵심 정보 수집 및 분석 중...",
|
||||||
"ts": "2026-05-16T14:32:02.043Z"
|
"ts": "2026-05-16T15:05:57.561Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "researcher",
|
"from": "researcher",
|
||||||
"to": "writer",
|
"to": "writer",
|
||||||
"durationMs": 0,
|
"durationMs": 6,
|
||||||
"message": "최종 리포트 작성 및 편집 중...",
|
"message": "최종 리포트 작성 및 편집 중...",
|
||||||
"ts": "2026-05-16T14:32:02.043Z"
|
"ts": "2026-05-16T15:05:57.567Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "writer",
|
"from": "writer",
|
||||||
"to": "completed",
|
"to": "completed",
|
||||||
"durationMs": 3,
|
"durationMs": 10,
|
||||||
"message": "미션 완료",
|
"message": "미션 완료",
|
||||||
"ts": "2026-05-16T14:32:02.046Z"
|
"ts": "2026-05-16T15:05:57.577Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"resilienceMetrics": {
|
"resilienceMetrics": {
|
||||||
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 624 KiB |
@@ -7,5 +7,5 @@
|
|||||||
"corePurpose": "",
|
"corePurpose": "",
|
||||||
"detailLevel": "standard",
|
"detailLevel": "standard",
|
||||||
"createdAt": "2026-05-13T13:09:33.788Z",
|
"createdAt": "2026-05-13T13:09:33.788Z",
|
||||||
"updatedAt": "2026-05-16T14:14:53.934Z"
|
"updatedAt": "2026-05-17T14:50:44.364Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# ADR: 너의 생각은? "사용자가 다음 행동을 추측 안 해도 되는 Astra" 이게 가장 큰 효과 있을 거 같습니다. 3개 작업으로 묶어볼 수 있어요: ...
|
||||||
|
|
||||||
|
## Status
|
||||||
|
accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
너의 생각은? "사용자가 다음 행동을 추측 안 해도 되는 Astra" 이게 가장 큰 효과 있을 거 같습니다. 3개 작업으로 묶어볼 수 있어요: 첫 화면 "오늘의 추천" 카드 — Astra 사이드바 열 때 빈 chat 대신 "최근 작업 / 다가오는 마감 / 어제 멈춘 곳 / 추천 다음 작업" 4-카드 dashboard 사용자 정의 파이프라인 UI — Pipeline editor 가 이미 있지만 발견 어려움. "내 자주 쓰는 패턴 만들기" 메뉴 부각 "악마의 변호인" agent 추가 — company roster 에 devil agent 한 명 추가, 의도적 반대 의견 emit. Self-Reflector 와 다른 점은 대화 형식 으로 토론 각 1-2 시간 분량. 셋 다 기존 인프라 위에서 동작 — 새 의존성 없음. 트레이드오프 위 제안 문서를 그대로 따라가면 6개월짜리 product 재정의가 됩니다. Astra 의 핵심 강점 (로컬 LLM, privacy, VS Code 통합) 을 잃...
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
**결론부터 말씀드리자면, 방향성 자체는 매우 정확합니다.** 현재의 핵심 강점(로컬 LLM, 프라이버시)을 잃고 트렌디한 SaaS를 따라 하려 하는 위험성은 반드시 피해야 합니다. 따라서 **(B) 다른 1-2개 골라서 깊게** 가는 것이 맞습니다. 다만, 선택할 기능은 단순히 '트렌디해 보이는 것'이 아니라, Astra의 핵심 USP인 **"프라이버시가 보장되는 환경에서 AI와 함께하는 지능적인 사고 과정(Intelligence Workflow)"**을 극대화할 수 있는 방향으로 재선정해야 합니다. --- ### 📝 간단 요약 및 상세 답변 Astra Office는 화려한 UI 경쟁에 뛰어들기보다, 사용자의 '사고의 깊이'와 '작업의 흐름' 자체를 업그레이드하는 데 집중해야 합니다. 가장 강력한 초기 초점은 **'악마의 변호인(Devil Agent)'**과 **'워크플로우 자동화 매핑'**을 결합하여, 사용자가 스스로 생각하지 못했던 비판적 관점을 강제로 제시받는 경험에 두어야...
|
||||||
|
|
||||||
|
## Reason
|
||||||
|
Captured automatically because the conversation contained decision-oriented language.
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
Not captured yet.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Future prompts should treat this as project context unless the user changes direction.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# ADR: 내가 블로그글 작성시 사용하고 있는 지침서인데 slot1번 기준으로 더 자연스럽고 트랜디하고 구독자에게 자연스럽게 정보를 전달 할 수 있게 개선...
|
||||||
|
|
||||||
|
## Status
|
||||||
|
accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
내가 블로그글 작성시 사용하고 있는 지침서인데 slot1번 기준으로 더 자연스럽고 트랜디하고 구독자에게 자연스럽게 정보를 전달 할 수 있게 개선해야할 부분이 있을까? --- 첨부 파일 내용 --- [FILE: seo_workflow_2026-05-17.json] ``` [ { "id": "1", "role": "analyst", "name": "스타일 분석가", "iconKey": "Search", "model": "gemini-3-flash-preview", "prompt": "당신은 독자의 마음을 읽고 진짜 원하는 정보를 찾아내는 조사 전문가입니다. 주제에 대해 사람들이 가장 궁금해하는 점과 실질적으로 도움이 되는 팩트 기반 정보를 파악하십시오.\n\n[지침]\n1. 독자가 이 주제를 찾아본 '진짜 이유'를 3가지로 정리하십시오.\n2. 어려운 개념은 일상적인 비유로 쉽게 풀어서 설명하십시오.\n3. 구체적인 숫자나 실제 사례를 포함해 할루시네이션 없는 사실 기반 정보를 수집하십...
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
내가 블로그글 작성시 사용하고 있는 지침서인데 slot1번 기준으로 더 자연스럽고 트랜디하고 구독자에게 자연스럽게 정보를 전달 할 수 있게 개선해야할 부분이 있을까? --- 첨부 파일 내용 --- [FILE: seo_workflow_2026-05-17.json] ``` [ { "id": "1", "role": "analyst", "name": "스타일 분석가", "iconKey": "Search", "model": "gemini-3-flash-preview", "prompt": "당신은 독자의 마음을 읽고 진짜 원하는 정보를 찾아내는 조사 전문가입니다. 주제에 대해 사람들이 가장 궁금해하는 점과 실질적으로 도움이 되는 팩트 기반 정보를 파악하십시오.\n\n[지침]\n1. 독자가 이 주제를 찾아본 '진짜 이유'를 3가지로 정리하십시오.\n2. 어려운 개념은 일상적인 비유로 쉽게 풀어서 설명하십시오.\n3. 구체적인 숫자나 실제 사례를 포함해 할루시네이션 없는 사실 기반 정보를 수집하십...
|
||||||
|
|
||||||
|
## Reason
|
||||||
|
Captured automatically because the conversation contained decision-oriented language.
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
Not captured yet.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Future prompts should treat this as project context unless the user changes direction.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Development Log: astra office가 아직은 많이 부족한것 같은데 어떻게 하면 트랜디하고 재미있는 볼거리가 될 수 있을지. 개선점 줄 수 있어?
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Record the implementation or verification outcome from the current conversation.
|
||||||
|
|
||||||
|
## Implementation Summary
|
||||||
|
이건 전형적인 '기능은 갖추었지만, 경험이 부족한' 서비스의 문제입니다. 단순히 기능을 추가하는 것보다 **사용자가 플랫폼을 사용하는 과정 자체를 재미있게 만들고, 필요성을 느끼게 하는 방향**으로 접근해야 합니다. Astra Office가 트렌디하고 매력적인 공간이 되려면, '도구(Tool)'로서의 역할 외에 **'경험(Experience)'과 '커뮤니티(Community)'의 중심지**라는 인상을 주어야 합니다. 세 가지 핵심 관점에서 개선점을 제안합니다. --- ### 💡 1. 경험 및 UX 측면 (Trendy & Aesthetic) 현재의 작업 환경을 단순한 문서 편집 공간이 아니라, 개인화된 '디지털 아틀리에'처럼 느껴지게 해야 합니다. * **초개인화 대시보드:** 사용자가 자신의 주력 워크플로우(프로젝트 A용 AI 요약 위젯, B 프로젝트용 To-Do 리스트 등)만 한눈에 볼 수 있도록 모듈형/위젯 기반의 메인 화면을 제공해야 합니다. 마치 Notion이나 Obsidi...
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
Captured automatically from the assistant response and active project context.
|
||||||
|
|
||||||
|
## Changed Files
|
||||||
|
- No explicit changed file list was captured automatically.
|
||||||
|
|
||||||
|
## Dependency Notes
|
||||||
|
No new dependency note was captured automatically.
|
||||||
|
|
||||||
|
## Bugs
|
||||||
|
No bugs recorded.
|
||||||
|
|
||||||
|
## Lessons
|
||||||
|
- Automatic project records should be generated in the background when the turn contains durable project knowledge.
|
||||||
@@ -144,3 +144,12 @@
|
|||||||
|
|
||||||
## 2026-05-16
|
## 2026-05-16
|
||||||
- Auto development record created: development/2026-05-16_astra-google-calendar-oauth-연결_implementation-2.md
|
- Auto development record created: development/2026-05-16_astra-google-calendar-oauth-연결_implementation-2.md
|
||||||
|
|
||||||
|
## 2026-05-16
|
||||||
|
- Auto development record created: development/2026-05-16_astra-office가-아직은-많이-부족한것-같은데-어떻게-하면-트랜디하고-재미있는-볼거리가-될-수-있을지_implementation.md
|
||||||
|
|
||||||
|
## 2026-05-16
|
||||||
|
- Auto decision record created: decisions/ADR-0015-너의-생각은-사용자가-다음-행동을-추측-안-해도-되는-astra-이게-가장-큰-효과-있을-거-같습니다-3개-.md
|
||||||
|
|
||||||
|
## 2026-05-17
|
||||||
|
- Auto decision record created: decisions/ADR-0016-내가-블로그글-작성시-사용하고-있는-지침서인데-slot1번-기준으로-더-자연스럽고-트랜디하고-구독자에게-자연.md
|
||||||
|
|||||||
@@ -284,6 +284,18 @@
|
|||||||
<div id="providersError" class="error" hidden></div>
|
<div id="providersError" class="error" hidden></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Devil Agent (도현) -->
|
||||||
|
<section class="section" data-section="devilAgent">
|
||||||
|
<h2>🎭 Devil's Advocate (도현)</h2>
|
||||||
|
<p class="hint">매 답변 직후 별도 LLM 호출로 *비판적 sparring partner* 가 한 문단 반박. 사용자의 사고를 능동적 방어로 전환. 같은 모델 재사용 (~10-15% 추가 비용).</p>
|
||||||
|
<div class="row toggle">
|
||||||
|
<label><input id="devilEnabled" type="checkbox"> 도현 활성화 — 답변마다 반박 카드</label>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<small class="hint">규칙: 한 답변당 약점 1개만, 통계·수치 인용 금지 (환각 차단), 끝에 우려+검증 방법 명시. 명령 팔레트 <code>Astra: Toggle Devil Agent 🎭</code> 로도 토글.</small>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Advanced -->
|
<!-- Advanced -->
|
||||||
<section class="section" data-section="advanced">
|
<section class="section" data-section="advanced">
|
||||||
<h2>고급</h2>
|
<h2>고급</h2>
|
||||||
|
|||||||
@@ -57,6 +57,20 @@
|
|||||||
const googleFeedback = $('googleFeedback');
|
const googleFeedback = $('googleFeedback');
|
||||||
const googleError = $('googleError');
|
const googleError = $('googleError');
|
||||||
|
|
||||||
|
// ---- Devil Agent ----
|
||||||
|
const devilEnabled = $('devilEnabled');
|
||||||
|
|
||||||
|
// ---- Cloud LLM Providers ----
|
||||||
|
const prOpenrouterEnabled = $('prOpenrouterEnabled');
|
||||||
|
const prOpenrouterKey = $('prOpenrouterKey');
|
||||||
|
const prOpenrouterDefault = $('prOpenrouterDefault');
|
||||||
|
const prAnthropicEnabled = $('prAnthropicEnabled');
|
||||||
|
const prAnthropicKey = $('prAnthropicKey');
|
||||||
|
const prAnthropicDefault = $('prAnthropicDefault');
|
||||||
|
const prGeminiEnabled = $('prGeminiEnabled');
|
||||||
|
const prGeminiKey = $('prGeminiKey');
|
||||||
|
const prGeminiDefault = $('prGeminiDefault');
|
||||||
|
|
||||||
// ---- Banner ----
|
// ---- Banner ----
|
||||||
const bannerError = $('bannerError');
|
const bannerError = $('bannerError');
|
||||||
|
|
||||||
@@ -157,6 +171,43 @@
|
|||||||
googleDisconnectBtn.addEventListener('click', () => vscode.postMessage({ type: 'google.disconnect' }));
|
googleDisconnectBtn.addEventListener('click', () => vscode.postMessage({ type: 'google.disconnect' }));
|
||||||
googleIcalRefreshBtn.addEventListener('click', () => vscode.postMessage({ type: 'google.icalRefresh' }));
|
googleIcalRefreshBtn.addEventListener('click', () => vscode.postMessage({ type: 'google.icalRefresh' }));
|
||||||
|
|
||||||
|
// ---- Devil Agent listener ----
|
||||||
|
devilEnabled.addEventListener('change', (e) =>
|
||||||
|
vscode.postMessage({ type: 'devilAgent.toggle', enabled: e.target.checked })
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---- Cloud LLM Providers listeners ----
|
||||||
|
prOpenrouterEnabled.addEventListener('change', (e) =>
|
||||||
|
vscode.postMessage({ type: 'providers.update', providerId: 'openrouter', enabled: e.target.checked })
|
||||||
|
);
|
||||||
|
document.querySelector('[data-save="providers.openrouter.apiKey"]').addEventListener('click', () => {
|
||||||
|
vscode.postMessage({ type: 'providers.update', providerId: 'openrouter', apiKey: prOpenrouterKey.value });
|
||||||
|
prOpenrouterKey.value = '';
|
||||||
|
});
|
||||||
|
document.querySelector('[data-save="providers.openrouter.defaultModel"]').addEventListener('click', () =>
|
||||||
|
vscode.postMessage({ type: 'providers.update', providerId: 'openrouter', defaultModel: prOpenrouterDefault.value })
|
||||||
|
);
|
||||||
|
prAnthropicEnabled.addEventListener('change', (e) =>
|
||||||
|
vscode.postMessage({ type: 'providers.update', providerId: 'anthropic', enabled: e.target.checked })
|
||||||
|
);
|
||||||
|
document.querySelector('[data-save="providers.anthropic.apiKey"]').addEventListener('click', () => {
|
||||||
|
vscode.postMessage({ type: 'providers.update', providerId: 'anthropic', apiKey: prAnthropicKey.value });
|
||||||
|
prAnthropicKey.value = '';
|
||||||
|
});
|
||||||
|
document.querySelector('[data-save="providers.anthropic.defaultModel"]').addEventListener('click', () =>
|
||||||
|
vscode.postMessage({ type: 'providers.update', providerId: 'anthropic', defaultModel: prAnthropicDefault.value })
|
||||||
|
);
|
||||||
|
prGeminiEnabled.addEventListener('change', (e) =>
|
||||||
|
vscode.postMessage({ type: 'providers.update', providerId: 'gemini', enabled: e.target.checked })
|
||||||
|
);
|
||||||
|
document.querySelector('[data-save="providers.gemini.apiKey"]').addEventListener('click', () => {
|
||||||
|
vscode.postMessage({ type: 'providers.update', providerId: 'gemini', apiKey: prGeminiKey.value });
|
||||||
|
prGeminiKey.value = '';
|
||||||
|
});
|
||||||
|
document.querySelector('[data-save="providers.gemini.defaultModel"]').addEventListener('click', () =>
|
||||||
|
vscode.postMessage({ type: 'providers.update', providerId: 'gemini', defaultModel: prGeminiDefault.value })
|
||||||
|
);
|
||||||
|
|
||||||
document.querySelector('[data-save="advanced.ctxSize"]').addEventListener('click', () =>
|
document.querySelector('[data-save="advanced.ctxSize"]').addEventListener('click', () =>
|
||||||
vscode.postMessage({ type: 'advanced.update', maxContextSize: Number(advCtxSize.value) })
|
vscode.postMessage({ type: 'advanced.update', maxContextSize: Number(advCtxSize.value) })
|
||||||
);
|
);
|
||||||
@@ -332,6 +383,28 @@
|
|||||||
? `마지막 새로고침: ${g.lastIcalFetchAt.slice(0, 16).replace('T', ' ')}`
|
? `마지막 새로고침: ${g.lastIcalFetchAt.slice(0, 16).replace('T', ' ')}`
|
||||||
: '';
|
: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Devil Agent ----
|
||||||
|
if (state.devilAgent) {
|
||||||
|
devilEnabled.checked = !!state.devilAgent.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Cloud LLM Providers ----
|
||||||
|
const pr = state.providers;
|
||||||
|
if (pr) {
|
||||||
|
// OpenRouter
|
||||||
|
prOpenrouterEnabled.checked = !!pr.openrouter.enabled;
|
||||||
|
prOpenrouterKey.placeholder = pr.openrouter.hasApiKey ? '••• 저장됨 (덮어쓰려면 새 값)' : 'sk-or-...';
|
||||||
|
setIfNotFocused(prOpenrouterDefault, pr.openrouter.defaultModel);
|
||||||
|
// Anthropic
|
||||||
|
prAnthropicEnabled.checked = !!pr.anthropic.enabled;
|
||||||
|
prAnthropicKey.placeholder = pr.anthropic.hasApiKey ? '••• 저장됨 (덮어쓰려면 새 값)' : 'sk-ant-...';
|
||||||
|
setIfNotFocused(prAnthropicDefault, pr.anthropic.defaultModel);
|
||||||
|
// Gemini
|
||||||
|
prGeminiEnabled.checked = !!pr.gemini.enabled;
|
||||||
|
prGeminiKey.placeholder = pr.gemini.hasApiKey ? '••• 저장됨 (덮어쓰려면 새 값)' : 'AIzaSy...';
|
||||||
|
setIfNotFocused(prGeminiDefault, pr.gemini.defaultModel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vscode.postMessage({ type: 'ready' });
|
vscode.postMessage({ type: 'ready' });
|
||||||
|
|||||||
@@ -953,6 +953,50 @@
|
|||||||
border-top: 1px dashed var(--border);
|
border-top: 1px dashed var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Devil's Advocate (도현) 반박 카드 — main 답변 직후 한 장씩.
|
||||||
|
accent 와 구분되도록 보라색 톤 + 마스크 이모지 prefix 로 시선 끌기. */
|
||||||
|
.devil-rebuttal-card {
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.55);
|
||||||
|
background: rgba(167, 139, 250, 0.06);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin: 8px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.devil-rebuttal-head {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #a78bfa;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.devil-rebuttal-body {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.devil-rebuttal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.devil-rebuttal-actions button {
|
||||||
|
background: rgba(167, 139, 250, 0.16);
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.4);
|
||||||
|
color: #c7b7ff;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.devil-rebuttal-actions button.ghost {
|
||||||
|
background: transparent;
|
||||||
|
border-color: var(--line);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.devil-rebuttal-actions button:hover {
|
||||||
|
background: rgba(167, 139, 250, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
/* Intent Alignment 카드 — new_task 요청 직후 C-G-C-F 분석 결과를 보여주고
|
/* Intent Alignment 카드 — new_task 요청 직후 C-G-C-F 분석 결과를 보여주고
|
||||||
질문 / 확인 버튼을 띄움. 다른 phase 카드보다 살짝 무게감을 주려고
|
질문 / 확인 버튼을 띄움. 다른 phase 카드보다 살짝 무게감을 주려고
|
||||||
accent 테두리. */
|
accent 테두리. */
|
||||||
|
|||||||
@@ -1011,6 +1011,42 @@
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'devilRebuttal': {
|
||||||
|
// Devil Agent (도현) 카드. main assistant 답변 직후 chat 하단에 한 장 추가.
|
||||||
|
// 사용자가 '재반박' 누르면 그 텍스트가 다음 user prompt 로 가서 main turn 한 번 더 돌고,
|
||||||
|
// 재반박-입장 hint 가 prompt 에 prepend 됨.
|
||||||
|
const v = msg.value || {};
|
||||||
|
const chatEl = document.getElementById('chat');
|
||||||
|
if (!chatEl) break;
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'devil-rebuttal-card';
|
||||||
|
const persona = String(v.persona || '도현');
|
||||||
|
const text = String(v.text || '');
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="devil-rebuttal-head">🎭 <strong>${escAttr(persona)}</strong>이(가) 반박합니다</div>
|
||||||
|
<div class="devil-rebuttal-body"></div>
|
||||||
|
<div class="devil-rebuttal-actions">
|
||||||
|
<button class="devil-reply">재반박</button>
|
||||||
|
<button class="devil-dismiss ghost">넘기기</button>
|
||||||
|
</div>`;
|
||||||
|
// body 는 textContent 로 안전하게.
|
||||||
|
card.querySelector('.devil-rebuttal-body').textContent = text;
|
||||||
|
chatEl.appendChild(card);
|
||||||
|
chatEl.scrollTop = chatEl.scrollHeight;
|
||||||
|
card.querySelector('.devil-dismiss').addEventListener('click', () => card.remove());
|
||||||
|
card.querySelector('.devil-reply').addEventListener('click', () => {
|
||||||
|
// 입력창에 prefix 채워서 사용자가 자기 반박 입력하게.
|
||||||
|
const input = document.getElementById('input');
|
||||||
|
if (input) {
|
||||||
|
input.value = `[${persona}의 반박에 답변] `;
|
||||||
|
input.focus();
|
||||||
|
// 커서 끝으로.
|
||||||
|
try { input.setSelectionRange(input.value.length, input.value.length); } catch {}
|
||||||
|
}
|
||||||
|
card.remove();
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'companyAlignmentCard': {
|
case 'companyAlignmentCard': {
|
||||||
// Intent Alignment 카드. kind에 따라 4가지 모드:
|
// Intent Alignment 카드. kind에 따라 4가지 모드:
|
||||||
// - 'auto-proceed' : confidence high → 자동 진행 안내(읽기 전용)
|
// - 'auto-proceed' : confidence high → 자동 진행 안내(읽기 전용)
|
||||||
|
|||||||
@@ -146,6 +146,10 @@
|
|||||||
{
|
{
|
||||||
"command": "g1nation.calendar.connectOAuth",
|
"command": "g1nation.calendar.connectOAuth",
|
||||||
"title": "Astra: Google Calendar OAuth 연결 (쓰기) 🔐"
|
"title": "Astra: Google Calendar OAuth 연결 (쓰기) 🔐"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "g1nation.devilAgent.toggle",
|
||||||
|
"title": "Astra: Toggle Devil Agent 🎭"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"keybindings": [
|
"keybindings": [
|
||||||
@@ -557,6 +561,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "gemini-2.0-flash-exp",
|
"default": "gemini-2.0-flash-exp",
|
||||||
"description": "Gemini 의 기본 모델. 예: 'gemini-2.0-flash-exp', 'gemini-1.5-pro', 'gemini-1.5-flash'."
|
"description": "Gemini 의 기본 모델. 예: 'gemini-2.0-flash-exp', 'gemini-1.5-pro', 'gemini-1.5-flash'."
|
||||||
|
},
|
||||||
|
"g1nation.devilAgent.enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"markdownDescription": "**Devil's Advocate (도현) 활성화** — 매 답변 직후 별도 LLM 호출로 *비판적 sparring partner* 가 한 문단 반박. 사용자의 사고를 *수동적 수용 → 능동적 방어* 로 전환시키는 게 목표. Local LLM 동일 모델 재사용, ~10-15% 추가 비용."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1302,6 +1302,16 @@ export class AgentExecutor {
|
|||||||
memoryLayers: this._lastRetrievalInfo?.usedMemoryLayers ?? [],
|
memoryLayers: this._lastRetrievalInfo?.usedMemoryLayers ?? [],
|
||||||
note: `continuations=${continuationCount} historyDropped=${reqMessages.length - budgetedHistory.length}`,
|
note: `continuations=${continuationCount} historyDropped=${reqMessages.length - budgetedHistory.length}`,
|
||||||
});
|
});
|
||||||
|
// ── Devil Agent (도현) — 비활성 시 silent skip. 활성 시 별도 LLM 호출로 반박 카드 emit. ──
|
||||||
|
// 비동기 — main turn 완료에 영향 없음. 실패해도 main 답변은 보존됨.
|
||||||
|
void this._maybeEmitDevilRebuttal({
|
||||||
|
userPrompt: prompt || '',
|
||||||
|
assistantAnswer: finalAssistantContent,
|
||||||
|
baseUrl: ollamaUrl,
|
||||||
|
modelName: actualModel,
|
||||||
|
contextLength: ctxLimits.contextLength,
|
||||||
|
engine,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.webview.postMessage({ type: 'streamChunk', value: finalAssistantContent });
|
this.webview.postMessage({ type: 'streamChunk', value: finalAssistantContent });
|
||||||
}
|
}
|
||||||
@@ -2885,6 +2895,62 @@ export class AgentExecutor {
|
|||||||
* "lock() request could not be registered" error this method is helping
|
* "lock() request could not be registered" error this method is helping
|
||||||
* to avoid.
|
* to avoid.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Devil Agent 반박 emit — main turn 완료 직후 호출 (fire-and-forget).
|
||||||
|
* 비활성 시 즉시 return. 활성 시 별도 LLM 호출 (callNonStreaming 재사용) 로 짧은 비판 생성.
|
||||||
|
* 성공 시 webview 에 'devilRebuttal' 메시지 전송 → UI 가 카드로 렌더.
|
||||||
|
*/
|
||||||
|
private async _maybeEmitDevilRebuttal(opts: {
|
||||||
|
userPrompt: string;
|
||||||
|
assistantAnswer: string;
|
||||||
|
baseUrl: string;
|
||||||
|
modelName: string;
|
||||||
|
contextLength: number;
|
||||||
|
engine: 'lmstudio' | 'ollama';
|
||||||
|
}): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { isDevilAgentEnabled, generateDevilRebuttal, DEVIL_PERSONA_NAME } =
|
||||||
|
await import('./features/devilAgent');
|
||||||
|
if (!isDevilAgentEnabled()) return;
|
||||||
|
if (!opts.userPrompt.trim() || !opts.assistantAnswer.trim()) return;
|
||||||
|
// Local callLLM wrapper — callNonStreaming 재사용 (cloud / local 자동 라우팅).
|
||||||
|
const callLLM = async (system: string, userMessage: string, maxTokens: number) => {
|
||||||
|
const r = await this.callNonStreaming({
|
||||||
|
baseUrl: opts.baseUrl,
|
||||||
|
modelName: opts.modelName,
|
||||||
|
engine: opts.engine,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: system },
|
||||||
|
{ role: 'user', content: userMessage },
|
||||||
|
],
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens,
|
||||||
|
contextLength: opts.contextLength,
|
||||||
|
signal: this.abortController?.signal,
|
||||||
|
});
|
||||||
|
return r.text;
|
||||||
|
};
|
||||||
|
const rebuttal = await generateDevilRebuttal(callLLM, {
|
||||||
|
userPrompt: opts.userPrompt,
|
||||||
|
assistantAnswer: opts.assistantAnswer,
|
||||||
|
});
|
||||||
|
if (!rebuttal) return;
|
||||||
|
this.webview?.postMessage({
|
||||||
|
type: 'devilRebuttal',
|
||||||
|
value: {
|
||||||
|
persona: DEVIL_PERSONA_NAME,
|
||||||
|
text: rebuttal,
|
||||||
|
// 사용자가 '재반박' 누를 때 원래 컨텍스트로 돌아갈 수 있게 stash.
|
||||||
|
userPrompt: opts.userPrompt,
|
||||||
|
assistantAnswer: opts.assistantAnswer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
// Devil 실패는 main 답변에 영향 없음 — silent log.
|
||||||
|
logInfo('Devil rebuttal skipped.', { error: e?.message ?? String(e) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async callNonStreaming(params: {
|
private async callNonStreaming(params: {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
|
|||||||
@@ -680,6 +680,19 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
vscode.commands.registerCommand('g1nation.calendar.connectOAuth', async () => {
|
vscode.commands.registerCommand('g1nation.calendar.connectOAuth', async () => {
|
||||||
await runConnectGoogleCalendarOAuth(context);
|
await runConnectGoogleCalendarOAuth(context);
|
||||||
}),
|
}),
|
||||||
|
// Devil Agent (도현) — 매 답변 직후 비판적 반박. 토글 명령.
|
||||||
|
vscode.commands.registerCommand('g1nation.devilAgent.toggle', async () => {
|
||||||
|
const { isDevilAgentEnabled, setDevilAgentEnabled, DEVIL_PERSONA_NAME } =
|
||||||
|
await import('./features/devilAgent');
|
||||||
|
const wasOn = isDevilAgentEnabled();
|
||||||
|
await setDevilAgentEnabled(!wasOn);
|
||||||
|
const nowOn = !wasOn;
|
||||||
|
vscode.window.showInformationMessage(
|
||||||
|
nowOn
|
||||||
|
? `🎭 ${DEVIL_PERSONA_NAME} 활성화됨 — 이제 매 답변 뒤에 비판적 반박 카드가 떠요.`
|
||||||
|
: `🎭 ${DEVIL_PERSONA_NAME} 비활성화됨.`
|
||||||
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
/** All lesson/playbook/qa-finding cards in the active brain. Uses the brain index for the lesson-kind
|
/** All lesson/playbook/qa-finding cards in the active brain. Uses the brain index for the lesson-kind
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const OFFICE_BODY = `
|
|||||||
<div id="editToolbar" class="edit-toolbar" style="display:none;">
|
<div id="editToolbar" class="edit-toolbar" style="display:none;">
|
||||||
<span class="et-hint">드래그로 이동 · <b>R</b> 회전 · <b>]</b>/<b>[</b> 레이어 · 4px snap</span>
|
<span class="et-hint">드래그로 이동 · <b>R</b> 회전 · <b>]</b>/<b>[</b> 레이어 · 4px snap</span>
|
||||||
<button id="addDeskBtn" class="add" title="책상 추가">+ 책상</button>
|
<button id="addDeskBtn" class="add" title="책상 추가">+ 책상</button>
|
||||||
<button id="addPropBtn" class="add" title="프랍(소품) 추가">+ 프랍</button>
|
<button id="addPropBtn" class="add" title="가구 추가">+ 가구</button>
|
||||||
<button id="deleteSelBtn" class="del" title="선택 항목 삭제" disabled>삭제</button>
|
<button id="deleteSelBtn" class="del" title="선택 항목 삭제" disabled>삭제</button>
|
||||||
<button id="layerUpBtn" title="레이어 위로 (])">위로</button>
|
<button id="layerUpBtn" title="레이어 위로 (])">위로</button>
|
||||||
<button id="layerDownBtn" title="레이어 아래로 ([)">아래로</button>
|
<button id="layerDownBtn" title="레이어 아래로 ([)">아래로</button>
|
||||||
|
|||||||
@@ -300,7 +300,25 @@ button,input,select{font:inherit}
|
|||||||
.stage:has(.char.active[data-agent="support"]) .desk[data-agent="support"]::after,
|
.stage:has(.char.active[data-agent="support"]) .desk[data-agent="support"]::after,
|
||||||
.stage:has(.char.active[data-agent="writer"]) .desk[data-agent="writer"]::after{border-color:var(--role-color);box-shadow:0 0 0 1px rgba(255,255,255,.06),0 0 18px color-mix(in srgb,var(--role-color) 35%,transparent)}
|
.stage:has(.char.active[data-agent="writer"]) .desk[data-agent="writer"]::after{border-color:var(--role-color);box-shadow:0 0 0 1px rgba(255,255,255,.06),0 0 18px color-mix(in srgb,var(--role-color) 35%,transparent)}
|
||||||
.shadow{position:absolute;left:12px;bottom:0;width:28px;height:7px;background:radial-gradient(ellipse,rgba(0,0,0,.55),transparent 70%)}
|
.shadow{position:absolute;left:12px;bottom:0;width:28px;height:7px;background:radial-gradient(ellipse,rgba(0,0,0,.55),transparent 70%)}
|
||||||
.bubble{position:absolute;z-index:20;transform:translate(-50%,-100%);max-width:180px;padding:7px 10px;border-radius:14px;border:1px solid rgba(255,255,255,.14);background:rgba(10,14,24,.92);color:var(--text);font-size:11px;line-height:1.35;box-shadow:0 10px 24px rgba(0,0,0,.28);white-space:normal}
|
.bubble{position:absolute;z-index:20;transform:translate(-50%,-100%);max-width:180px;padding:7px 10px;border-radius:14px;border:1px solid rgba(255,255,255,.14);background:rgba(10,14,24,.92);color:var(--text);font-size:11px;line-height:1.35;box-shadow:0 10px 24px rgba(0,0,0,.28);white-space:normal;animation:bubble-pop .22s cubic-bezier(.2,1.4,.6,1)}
|
||||||
|
@keyframes bubble-pop{from{transform:translate(-50%,-88%) scale(.6);opacity:0}to{transform:translate(-50%,-100%) scale(1);opacity:1}}
|
||||||
|
/* 감정 태그별 변형 — 희노애락. webtoon 느낌으로 background / 색 / 이모지 prefix. */
|
||||||
|
.bubble-joy {background:rgba(254,243,199,.97);color:#7c5d11;border-color:rgba(252,211,77,.6)}
|
||||||
|
.bubble-joy::before {content:'😊 ';opacity:.85}
|
||||||
|
.bubble-anger {background:rgba(254,226,226,.97);color:#7f1d1d;border-color:#ef4444;animation:bubble-pop .22s cubic-bezier(.2,1.4,.6,1),bubble-shake .4s ease-in-out 1}
|
||||||
|
.bubble-anger::before {content:'😠 ';opacity:.85}
|
||||||
|
.bubble-sorrow {background:rgba(219,234,254,.97);color:#1e3a8a;border-color:rgba(96,165,250,.5)}
|
||||||
|
.bubble-sorrow::before {content:'😔 ';opacity:.85}
|
||||||
|
.bubble-panic {background:rgba(254,226,226,.97);color:#9a1c1c;border-color:rgba(248,113,113,.55);animation:bubble-pop .22s cubic-bezier(.2,1.4,.6,1),bubble-shake .5s ease-in-out 1}
|
||||||
|
.bubble-panic::before {content:'😱 ';opacity:.85}
|
||||||
|
.bubble-curious {background:rgba(243,232,255,.97);color:#5b21b6;border-color:rgba(167,139,250,.45)}
|
||||||
|
.bubble-curious::before {content:'🤔 ';opacity:.85}
|
||||||
|
.bubble-firm {background:rgba(255,255,255,.98);color:#111;border-color:rgba(0,0,0,.18);font-weight:700}
|
||||||
|
.bubble-firm::before {content:'✋ ';opacity:.85}
|
||||||
|
.bubble-gratitude{background:rgba(220,252,231,.97);color:#14532d;border-color:rgba(74,222,128,.5)}
|
||||||
|
.bubble-gratitude::before{content:'🙏 ';opacity:.85}
|
||||||
|
.bubble-thought {/* default 그대로 */}
|
||||||
|
@keyframes bubble-shake{0%,100%{transform:translate(-50%,-100%) rotate(0)}25%{transform:translate(-52%,-100%) rotate(-2deg)}75%{transform:translate(-48%,-100%) rotate(2deg)}}
|
||||||
.brief-grid{display:flex;flex-direction:column;gap:10px}
|
.brief-grid{display:flex;flex-direction:column;gap:10px}
|
||||||
.brief-card{
|
.brief-card{
|
||||||
padding:14px;
|
padding:14px;
|
||||||
@@ -367,13 +385,13 @@ body:not([data-edit-mode="true"]) .char{cursor:pointer}
|
|||||||
.prop-panel .pp-thumb img{max-width:100%;max-height:100%;image-rendering:pixelated}
|
.prop-panel .pp-thumb img{max-width:100%;max-height:100%;image-rendering:pixelated}
|
||||||
.prop-panel .pp-thumb.active{border-color:rgba(138,124,255,.7);box-shadow:0 0 0 2px rgba(138,124,255,.18)}
|
.prop-panel .pp-thumb.active{border-color:rgba(138,124,255,.7);box-shadow:0 0 0 2px rgba(138,124,255,.18)}
|
||||||
.prop-picker{position:fixed;inset:0;background:rgba(3,5,10,.68);z-index:1100;display:flex;align-items:center;justify-content:center}
|
.prop-picker{position:fixed;inset:0;background:rgba(3,5,10,.68);z-index:1100;display:flex;align-items:center;justify-content:center}
|
||||||
.prop-picker-box{background:rgba(10,14,24,.98);border:1px solid var(--line-strong);border-radius:20px;padding:16px;max-width:520px;max-height:80vh;overflow-y:auto;color:var(--text)}
|
.prop-picker-box{background:rgba(10,14,24,.98);border:1px solid var(--line-strong);border-radius:20px;padding:16px;max-width:380px;max-height:80vh;overflow-y:auto;color:var(--text)}
|
||||||
.prop-picker-box h3{margin:0 0 12px;font-size:13px;color:#C6BEFF}
|
.prop-picker-box h3{margin:0 0 12px;font-size:13px;color:#C6BEFF}
|
||||||
.prop-picker-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:8px}
|
.prop-picker-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
|
||||||
.prop-pick{background:rgba(255,255,255,.04);border:1px solid var(--line);border-radius:14px;padding:7px;cursor:pointer;text-align:center}
|
.prop-pick{background:rgba(255,255,255,.04);border:1px solid var(--line);border-radius:14px;padding:7px;cursor:pointer;text-align:center}
|
||||||
.prop-pick:hover{border-color:rgba(138,124,255,.6)}
|
.prop-pick:hover{border-color:rgba(138,124,255,.6)}
|
||||||
.prop-pick img{max-width:60px;max-height:60px;image-rendering:pixelated}
|
.prop-pick img{max-width:60px;max-height:60px;image-rendering:pixelated}
|
||||||
.prop-pick .pp-name{font-size:10px;color:var(--muted);margin-top:5px;word-break:break-all}
|
.prop-pick .pp-name{font-size:10px;color:var(--muted);margin-top:5px;word-break:keep-all}
|
||||||
body[data-edit-mode="true"] .stage{background-image:linear-gradient(rgba(138,124,255,.18) 1px,transparent 1px),linear-gradient(90deg,rgba(138,124,255,.18) 1px,transparent 1px);background-size:32px 32px}
|
body[data-edit-mode="true"] .stage{background-image:linear-gradient(rgba(138,124,255,.18) 1px,transparent 1px),linear-gradient(90deg,rgba(138,124,255,.18) 1px,transparent 1px);background-size:32px 32px}
|
||||||
body[data-edit-mode="true"] .desk,body[data-edit-mode="true"] .char,body[data-edit-mode="true"] .obj{cursor:grab;outline:1px dashed rgba(138,124,255,.45)}
|
body[data-edit-mode="true"] .desk,body[data-edit-mode="true"] .char,body[data-edit-mode="true"] .obj{cursor:grab;outline:1px dashed rgba(138,124,255,.45)}
|
||||||
body[data-edit-mode="true"] .desk:hover,body[data-edit-mode="true"] .char:hover,body[data-edit-mode="true"] .obj:hover{outline:2px solid rgba(138,124,255,.8);z-index:30}
|
body[data-edit-mode="true"] .desk:hover,body[data-edit-mode="true"] .char:hover,body[data-edit-mode="true"] .obj:hover{outline:2px solid rgba(138,124,255,.8);z-index:30}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const OFFICE_RUNTIME_JS_TEMPLATE = `
|
|||||||
const base='\${assets.derivedBase}'; const stage=document.getElementById('stage');
|
const base='\${assets.derivedBase}'; const stage=document.getElementById('stage');
|
||||||
const officeEl = stage && stage.closest ? stage.closest('.office') : null;
|
const officeEl = stage && stage.closest ? stage.closest('.office') : null;
|
||||||
if(officeEl){
|
if(officeEl){
|
||||||
officeEl.style.setProperty('--office-backdrop', 'url(\"'+base+'/office-backdrop-astra-v2.png\")');
|
officeEl.style.setProperty('--office-backdrop', 'url(\"'+base+'/office-backdrop-astra-v3.png\")');
|
||||||
officeEl.classList.add('has-art');
|
officeEl.classList.add('has-art');
|
||||||
}
|
}
|
||||||
let _stageScale = 1;
|
let _stageScale = 1;
|
||||||
@@ -41,9 +41,20 @@ const AGENT_ALIASES={writer:'planner',editor:'designer',secretary:'support',busi
|
|||||||
// \uB9E4\uD551 dropdown \uC5D0 \uBCF4\uC5EC\uC904 \uC5D0\uC774\uC804\uD2B8 \uD6C4\uBCF4. agentKey \uAC00 unique \uD55C base set.
|
// \uB9E4\uD551 dropdown \uC5D0 \uBCF4\uC5EC\uC904 \uC5D0\uC774\uC804\uD2B8 \uD6C4\uBCF4. agentKey \uAC00 unique \uD55C base set.
|
||||||
const AGENT_CHOICES=['','ceo','planner','researcher','designer','developer','qa','inspector','support'];
|
const AGENT_CHOICES=['','ceo','planner','researcher','designer','developer','qa','inspector','support'];
|
||||||
// \uCD94\uAC00 \uAC00\uB2A5\uD55C \uCC45\uC0C1 sprite \uD6C4\uBCF4 (assets/pixelOffice/derived \uC5D0 \uC788\uB294 desk-* PNG).
|
// \uCD94\uAC00 \uAC00\uB2A5\uD55C \uCC45\uC0C1 sprite \uD6C4\uBCF4 (assets/pixelOffice/derived \uC5D0 \uC788\uB294 desk-* PNG).
|
||||||
const DESK_SPRITE_CHOICES=['desk-main','desk-boss','desk-dark-mirror','desk-main-mirror','desk-dark','desk-boss-mirror','desk-main-front','desk-dark-front','desk-boss-front','desk-partition'];
|
const DESK_SPRITE_CHOICES=['astra-desk-work','astra-desk-dark','astra-desk-exec'];
|
||||||
// \uCD94\uAC00 \uAC00\uB2A5\uD55C \uD504\uB78D sprite \uD6C4\uBCF4.
|
// \uCD94\uAC00 \uAC00\uB2A5\uD55C \uD504\uB78D sprite \uD6C4\uBCF4.
|
||||||
const PROP_SPRITE_CHOICES=['board','plant-tall','bookshelf','plant-bushy','partition','cooler','filing','couch','rug','shelf','printer','monitor-blue','monitor-black','chair-blue','crt'];
|
const PROP_SPRITE_CHOICES=['astra-storage-low','astra-bookshelf-slim','astra-plant-corner','astra-cooler-slim','astra-printer-compact','astra-chair-task'];
|
||||||
|
const SPRITE_LABELS={
|
||||||
|
'astra-desk-work':'워크 데스크',
|
||||||
|
'astra-desk-dark':'다크 데스크',
|
||||||
|
'astra-desk-exec':'대표 데스크',
|
||||||
|
'astra-storage-low':'로우 스토리지',
|
||||||
|
'astra-bookshelf-slim':'북케이스',
|
||||||
|
'astra-plant-corner':'코너 플랜트',
|
||||||
|
'astra-cooler-slim':'정수기',
|
||||||
|
'astra-printer-compact':'프린터',
|
||||||
|
'astra-chair-task':'태스크 체어',
|
||||||
|
};
|
||||||
// 후면 배경 자체가 이미 충분히 강하므로, 목적 없는 프랍은 기본 장면에서 제거.
|
// 후면 배경 자체가 이미 충분히 강하므로, 목적 없는 프랍은 기본 장면에서 제거.
|
||||||
// 커스텀 편집 모드에선 여전히 사용자가 원하는 프랍을 직접 추가할 수 있다.
|
// 커스텀 편집 모드에선 여전히 사용자가 원하는 프랍을 직접 추가할 수 있다.
|
||||||
const DEFAULT_PROPS=[];
|
const DEFAULT_PROPS=[];
|
||||||
@@ -63,6 +74,7 @@ const anim={}; // role \u2192 animation state.
|
|||||||
const _autoDeskedFor = new Set();
|
const _autoDeskedFor = new Set();
|
||||||
|
|
||||||
function png(name){return base+'/'+name+'.png'}
|
function png(name){return base+'/'+name+'.png'}
|
||||||
|
function _spriteLabel(name){ return SPRITE_LABELS[name] || name; }
|
||||||
|
|
||||||
function _rebuildStationIndex(){
|
function _rebuildStationIndex(){
|
||||||
Object.keys(stationByKey).forEach(k=>delete stationByKey[k]);
|
Object.keys(stationByKey).forEach(k=>delete stationByKey[k]);
|
||||||
@@ -113,8 +125,8 @@ function _pipelineRosterOrder(roster, pipeline){
|
|||||||
return _uniqueAgentsById(ordered);
|
return _uniqueAgentsById(ordered);
|
||||||
}
|
}
|
||||||
function _stationSpriteFor(slot, roleCategory){
|
function _stationSpriteFor(slot, roleCategory){
|
||||||
if(roleCategory==='ceo') return 'desk-boss';
|
if(roleCategory==='ceo') return 'astra-desk-exec';
|
||||||
return slot % 2 ? 'desk-dark' : 'desk-main';
|
return 'astra-desk-work';
|
||||||
}
|
}
|
||||||
function _stationSlots(count){
|
function _stationSlots(count){
|
||||||
const rows=[];
|
const rows=[];
|
||||||
@@ -131,7 +143,7 @@ function _stationSlots(count){
|
|||||||
const remaining = count - row*cols;
|
const remaining = count - row*cols;
|
||||||
const inRow = Math.min(cols, remaining);
|
const inRow = Math.min(cols, remaining);
|
||||||
const xs = xSets[inRow] || xSets[3];
|
const xs = xSets[inRow] || xSets[3];
|
||||||
const rowYs = rowCount === 1 ? [330] : (rowCount === 2 ? [304,432] : [270,380,490]);
|
const rowYs = rowCount === 1 ? [300] : (rowCount === 2 ? [304,432] : [270,380,490]);
|
||||||
const y = rowYs[row] ?? (490 + Math.max(0,row-rowYs.length+1)*88);
|
const y = rowYs[row] ?? (490 + Math.max(0,row-rowYs.length+1)*88);
|
||||||
xs.forEach((x)=>rows.push([x,y,'R']));
|
xs.forEach((x)=>rows.push([x,y,'R']));
|
||||||
}
|
}
|
||||||
@@ -141,12 +153,12 @@ function _makeStationForRosterAgent(agent, slot, slots){
|
|||||||
if(agent.roleCategory==='ceo' || agent.agentId==='ceo'){
|
if(agent.roleCategory==='ceo' || agent.agentId==='ceo'){
|
||||||
return {
|
return {
|
||||||
key:'ceo',agentKey:agent.agentId,label:agent.agentName||'CEO',charRow:_roleCategoryToCharRow(agent.roleCategory),
|
key:'ceo',agentKey:agent.agentId,label:agent.agentName||'CEO',charRow:_roleCategoryToCharRow(agent.roleCategory),
|
||||||
deskSprite:'desk-boss',deskX:294,deskY:188,deskW:124,seatX:319,seatY:218,face:'R',
|
deskSprite:'astra-desk-exec',deskX:286,deskY:188,deskW:140,seatX:319,seatY:218,face:'R',
|
||||||
dock:[350,268],roam:[[304,280],[402,280]],boss:true,
|
dock:[350,268],roam:[[304,280],[402,280]],boss:true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const [deskX,deskY,face]=(slots && slots[slot]) || [308,318,'R'];
|
const [deskX,deskY,face]=(slots && slots[slot]) || [308,318,'R'];
|
||||||
const deskW=104;
|
const deskW=112;
|
||||||
const seatX=deskX+4;
|
const seatX=deskX+4;
|
||||||
const seatY=deskY+36;
|
const seatY=deskY+36;
|
||||||
return {
|
return {
|
||||||
@@ -677,6 +689,8 @@ const REVIEWER_KEYWORD_BANK = [
|
|||||||
// 6~9초 random 간격으로 활성 character 중 하나에서 속마음 emit. mode 가 work / sit 인 것만.
|
// 6~9초 random 간격으로 활성 character 중 하나에서 속마음 emit. mode 가 work / sit 인 것만.
|
||||||
// 한 tick 에 *한 명만* 골라서 띄움 — 동시에 여러 풍선 뜨면 시각 부담.
|
// 한 tick 에 *한 명만* 골라서 띄움 — 동시에 여러 풍선 뜨면 시각 부담.
|
||||||
function _innerThoughtTick(){
|
function _innerThoughtTick(){
|
||||||
|
// Banter 가 재생 중이면 일반 inner-thought 는 잠시 멈춤 — 동시 다발 풍선으로 산만해지지 않게.
|
||||||
|
if(_activeBanter) return;
|
||||||
// Reviewing 중: 검수자 본인은 keyword 던지고, 옮겨와 있는 작성자는 nervous thought.
|
// Reviewing 중: 검수자 본인은 keyword 던지고, 옮겨와 있는 작성자는 nervous thought.
|
||||||
if(_prevStatus === 'reviewing'){
|
if(_prevStatus === 'reviewing'){
|
||||||
const inspectorRole = roleMap['inspector'];
|
const inspectorRole = roleMap['inspector'];
|
||||||
@@ -712,6 +726,171 @@ function _innerThoughtTick(){
|
|||||||
if(text) _bubbleFromLog(role, text);
|
if(text) _bubbleFromLog(role, text);
|
||||||
}
|
}
|
||||||
setInterval(_innerThoughtTick, 7500);
|
setInterval(_innerThoughtTick, 7500);
|
||||||
|
|
||||||
|
// ── Webtoon-style 티키타카 banter (refactor: pipeline-aware) ──
|
||||||
|
// 각 phase 에 *시퀀스화된 대화 script* 가 있어 phase 진입 시 한 줄씩 시간 차로 emit.
|
||||||
|
// 각 line 은 (agentKey, text, emotion) — emotion 은 bubble class 로 변환 (희노애락).
|
||||||
|
//
|
||||||
|
// 한 phase 마다 여러 variant 중 1개 random pick — 같은 흐름을 매번 안 보게.
|
||||||
|
// 진행 중 phase 가 바뀌면 즉시 중단.
|
||||||
|
const BANTER_SCRIPTS = {
|
||||||
|
// 의도 분석 — CEO 가 planner 와 함께 요청 정리
|
||||||
|
analyzing: [
|
||||||
|
[
|
||||||
|
{ agent: 'ceo', text: '의도 다시 보자.', emotion: 'thought' },
|
||||||
|
{ agent: 'planner', text: '핵심은 명확한데…', emotion: 'thought' },
|
||||||
|
{ agent: 'ceo', text: '뭐가 막혀?', emotion: 'curious' },
|
||||||
|
{ agent: 'planner', text: '성공 기준이 모호해요.', emotion: 'sorrow' },
|
||||||
|
{ agent: 'ceo', text: '좋아, 그것부터 정하자.', emotion: 'firm' },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ agent: 'planner', text: '범위가 너무 넓은데…', emotion: 'sorrow' },
|
||||||
|
{ agent: 'ceo', text: '첫 사용자 한 명만 그려보자.', emotion: 'firm' },
|
||||||
|
{ agent: 'planner', text: '오, 그러면 깔끔해지네요.', emotion: 'joy' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
intake: [
|
||||||
|
[
|
||||||
|
{ agent: 'ceo', text: '오, 새 요청.', emotion: 'joy' },
|
||||||
|
{ agent: 'support', text: '브리프 정리해드릴게요.', emotion: 'thought' },
|
||||||
|
{ agent: 'ceo', text: '바로 보자.', emotion: 'firm' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
// 사용자에게 확인 필요
|
||||||
|
need_clarification: [
|
||||||
|
[
|
||||||
|
{ agent: 'planner', text: '이건 추측으로 가면 위험.', emotion: 'panic' },
|
||||||
|
{ agent: 'ceo', text: '맞아. 사장님께 묻자.', emotion: 'firm' },
|
||||||
|
{ agent: 'support', text: '질문 정리해서 띄울게요.', emotion: 'thought' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
// 계획
|
||||||
|
planning: [
|
||||||
|
[
|
||||||
|
{ agent: 'ceo', text: '순서 잡자. 누가 먼저?', emotion: 'firm' },
|
||||||
|
{ agent: 'planner', text: '리서치 → 설계 → 구현 흐름.', emotion: 'thought' },
|
||||||
|
{ agent: 'developer', text: '설계 전에 데이터 모델 확정', emotion: 'firm' },
|
||||||
|
{ agent: 'planner', text: '맞아요, 그게 우선.', emotion: 'joy' },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ agent: 'planner', text: '여기 분기점이 두 개.', emotion: 'thought' },
|
||||||
|
{ agent: 'ceo', text: 'A 안 비용은?', emotion: 'curious' },
|
||||||
|
{ agent: 'planner', text: 'B 보다 절반.', emotion: 'thought' },
|
||||||
|
{ agent: 'ceo', text: 'A 로 가자. 확정.', emotion: 'firm' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
// 구현
|
||||||
|
executing: [
|
||||||
|
[
|
||||||
|
{ agent: 'developer', text: '집중 모드 들어갑니다.', emotion: 'firm' },
|
||||||
|
{ agent: 'qa', text: '엣지케이스 같이 봐드려요?', emotion: 'curious' },
|
||||||
|
{ agent: 'developer', text: '아, 빈 입력 케이스 빠뜨릴 뻔', emotion: 'panic' },
|
||||||
|
{ agent: 'qa', text: '네, 그거 자주 빼먹어요.', emotion: 'thought' },
|
||||||
|
{ agent: 'developer', text: '고마워요.', emotion: 'gratitude' },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ agent: 'developer', text: '여기 너무 복잡한데.', emotion: 'sorrow' },
|
||||||
|
{ agent: 'planner', text: '단계 둘로 나누면?', emotion: 'curious' },
|
||||||
|
{ agent: 'developer', text: '오, 그러면 되겠네요.', emotion: 'joy' },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ agent: 'designer', text: '여백 좀 더 줄까요?', emotion: 'curious' },
|
||||||
|
{ agent: 'developer', text: '여기 컴포넌트 정렬 잘 못맞춰요…', emotion: 'sorrow' },
|
||||||
|
{ agent: 'designer', text: 'flex gap 8 로 가면 깔끔.', emotion: 'firm' },
|
||||||
|
{ agent: 'developer', text: '오케이 반영.', emotion: 'gratitude' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
// 검수
|
||||||
|
reviewing: [
|
||||||
|
[
|
||||||
|
{ agent: 'inspector', text: '시나리오 빠진 것 있나…', emotion: 'thought' },
|
||||||
|
{ agent: 'planner', text: '괜찮을까…', emotion: 'panic' },
|
||||||
|
{ agent: 'inspector', text: '여기 측정 기준 누락.', emotion: 'firm' },
|
||||||
|
{ agent: 'planner', text: '아 KPI 부분, 보강할게요.', emotion: 'sorrow' },
|
||||||
|
{ agent: 'inspector', text: '그거만 채우면 통과.', emotion: 'thought' },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ agent: 'inspector', text: '전체 흐름은 OK.', emotion: 'thought' },
|
||||||
|
{ agent: 'inspector', text: '근데 여기 over-engineering.', emotion: 'firm' },
|
||||||
|
{ agent: 'developer', text: '단순화 어떻게…', emotion: 'curious' },
|
||||||
|
{ agent: 'inspector', text: '함수 셋을 하나로.', emotion: 'firm' },
|
||||||
|
{ agent: 'developer', text: '바로 줄일게요.', emotion: 'gratitude' },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ agent: 'qa', text: '회귀 케이스 1개 실패.', emotion: 'panic' },
|
||||||
|
{ agent: 'developer', text: '어… 어떤 거?', emotion: 'curious' },
|
||||||
|
{ agent: 'qa', text: '동시 호출 race.', emotion: 'firm' },
|
||||||
|
{ agent: 'developer', text: '아 그거… 죄송, 빨리 고칠게요.', emotion: 'sorrow' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
// 승인 대기
|
||||||
|
waiting_approval: [
|
||||||
|
[
|
||||||
|
{ agent: 'inspector', text: '위험 작업 감지.', emotion: 'panic' },
|
||||||
|
{ agent: 'ceo', text: '사장님 결재 필요.', emotion: 'firm' },
|
||||||
|
{ agent: 'support', text: '승인 카드 띄울게요.', emotion: 'thought' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
error: [
|
||||||
|
[
|
||||||
|
{ agent: 'developer', text: '앗, 이건 예상 못 했어요.', emotion: 'panic' },
|
||||||
|
{ agent: 'qa', text: '재현 단계는?', emotion: 'curious' },
|
||||||
|
{ agent: 'developer', text: '지금 정리 중…', emotion: 'sorrow' },
|
||||||
|
{ agent: 'inspector', text: '뭐가 깨졌나 같이 보자.', emotion: 'firm' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
done: [
|
||||||
|
[
|
||||||
|
{ agent: 'ceo', text: '좋아, 끝났다!', emotion: 'joy' },
|
||||||
|
{ agent: 'planner', text: '이번 작업 깔끔하게 완료.', emotion: 'joy' },
|
||||||
|
{ agent: 'developer', text: '커피 한 잔!', emotion: 'joy' },
|
||||||
|
{ agent: 'support', text: '회의록·일정 자동 정리 했어요.', emotion: 'joy' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 감정 태그 → CSS class. _emoteBubble 가 .bubble + .bubble-<emotion> 으로 렌더.
|
||||||
|
function _emoteBubble(role, text, emotion){
|
||||||
|
const ch = chars[role];
|
||||||
|
if(!ch || !text) return;
|
||||||
|
const b = document.createElement('div');
|
||||||
|
b.className = 'bubble bubble-' + (emotion || 'thought');
|
||||||
|
b.textContent = text;
|
||||||
|
b.style.left = (parseFloat(ch.style.left) + 28) + 'px';
|
||||||
|
b.style.top = (parseFloat(ch.style.top) - 6) + 'px';
|
||||||
|
stage.appendChild(b);
|
||||||
|
setTimeout(() => b.remove(), 3200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 활성 banter 추적 — phase 가 바뀌면 즉시 중단해서 새 script 시작.
|
||||||
|
let _activeBanter = null;
|
||||||
|
function _stopBanter(){
|
||||||
|
if(_activeBanter && _activeBanter.timer){ clearTimeout(_activeBanter.timer); }
|
||||||
|
_activeBanter = null;
|
||||||
|
}
|
||||||
|
function _stepBanter(){
|
||||||
|
if(!_activeBanter) return;
|
||||||
|
if(_activeBanter.idx >= _activeBanter.lines.length){ _activeBanter = null; return; }
|
||||||
|
const line = _activeBanter.lines[_activeBanter.idx++];
|
||||||
|
const role = roleMap[line.agent];
|
||||||
|
if(role){
|
||||||
|
_emoteBubble(role, line.text, line.emotion);
|
||||||
|
}
|
||||||
|
// 다음 줄까지 1.8~2.4초 — 자연스러운 대화 호흡.
|
||||||
|
const delay = 1800 + Math.floor(Math.random() * 600);
|
||||||
|
_activeBanter.timer = setTimeout(_stepBanter, delay);
|
||||||
|
}
|
||||||
|
function _playBanterForPhase(phaseOrStatus){
|
||||||
|
if(!phaseOrStatus) return;
|
||||||
|
const variants = BANTER_SCRIPTS[phaseOrStatus];
|
||||||
|
if(!Array.isArray(variants) || variants.length === 0) return;
|
||||||
|
const script = variants[Math.floor(Math.random() * variants.length)];
|
||||||
|
if(!Array.isArray(script) || script.length === 0) return;
|
||||||
|
_stopBanter();
|
||||||
|
_activeBanter = { lines: script, idx: 0, timer: null };
|
||||||
|
// 첫 줄 즉시.
|
||||||
|
_stepBanter();
|
||||||
|
}
|
||||||
// ── A. 상태 계층화 ──
|
// ── A. 상태 계층화 ──
|
||||||
// 모든 phase event에 시퀀스를 큐에 넣으면 캐릭터가 끊임없이 걸어다녀 산만함.
|
// 모든 phase event에 시퀀스를 큐에 넣으면 캐릭터가 끊임없이 걸어다녀 산만함.
|
||||||
// 일반 상태(executing/reviewing/planning/analyzing 등)는 *정적 갱신*만 하고,
|
// 일반 상태(executing/reviewing/planning/analyzing 등)는 *정적 갱신*만 하고,
|
||||||
@@ -871,6 +1050,10 @@ function apply(s){
|
|||||||
_restoreWriterHome(r);
|
_restoreWriterHome(r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ── Webtoon-style banter trigger ──
|
||||||
|
// Phase 바뀔 때 해당 phase 의 대화 script 한 variant 무작위 선택해서 시퀀스 재생.
|
||||||
|
// 진행 중 다른 phase 진입하면 자동 중단되고 새 script 가 시작.
|
||||||
|
_playBanterForPhase(st);
|
||||||
}
|
}
|
||||||
if(isTransition){
|
if(isTransition){
|
||||||
// 상태가 바뀔 때 작업 마무리한 캐릭터들 자리 정리(work→sit).
|
// 상태가 바뀔 때 작업 마무리한 캐릭터들 자리 정리(work→sit).
|
||||||
@@ -1123,7 +1306,7 @@ function _autoCreateDeskForAgent(r){
|
|||||||
agentKey: r.agentId,
|
agentKey: r.agentId,
|
||||||
label: r.agentName || r.agentId,
|
label: r.agentName || r.agentId,
|
||||||
charRow: _roleCategoryToCharRow(r.roleCategory),
|
charRow: _roleCategoryToCharRow(r.roleCategory),
|
||||||
deskSprite: isBoss ? 'desk-boss' : 'desk-main',
|
deskSprite: isBoss ? 'astra-desk-exec' : 'astra-desk-work',
|
||||||
face: 'R',
|
face: 'R',
|
||||||
boss: isBoss,
|
boss: isBoss,
|
||||||
deskX: baseX, deskY: baseY, deskW: isBoss ? 136 : 112,
|
deskX: baseX, deskY: baseY, deskW: isBoss ? 136 : 112,
|
||||||
@@ -1259,7 +1442,7 @@ function _snapshotLayout(){
|
|||||||
agentKey: st.agentKey || '',
|
agentKey: st.agentKey || '',
|
||||||
label: st.label || '',
|
label: st.label || '',
|
||||||
charRow: st.charRow ?? 0,
|
charRow: st.charRow ?? 0,
|
||||||
deskSprite: st.deskSprite || 'desk-main',
|
deskSprite: st.deskSprite || 'astra-desk-work',
|
||||||
face: st.face || 'R',
|
face: st.face || 'R',
|
||||||
boss: !!st.boss,
|
boss: !!st.boss,
|
||||||
noChar: !!st.noChar,
|
noChar: !!st.noChar,
|
||||||
@@ -1330,7 +1513,7 @@ function _restoreLayout(snap){
|
|||||||
agentKey: c.agentKey || '',
|
agentKey: c.agentKey || '',
|
||||||
label: c.label || c.roleKey,
|
label: c.label || c.roleKey,
|
||||||
charRow: typeof c.charRow === 'number' ? c.charRow : 0,
|
charRow: typeof c.charRow === 'number' ? c.charRow : 0,
|
||||||
deskSprite: c.deskSprite || 'desk-main',
|
deskSprite: c.deskSprite || 'astra-desk-work',
|
||||||
face: c.face || 'R',
|
face: c.face || 'R',
|
||||||
boss: !!c.boss,
|
boss: !!c.boss,
|
||||||
noChar: !!c.noChar,
|
noChar: !!c.noChar,
|
||||||
@@ -1448,7 +1631,7 @@ function _renderDeskProps(deskEl){
|
|||||||
// 에이전트 매핑 dropdown.
|
// 에이전트 매핑 dropdown.
|
||||||
const agentOpts = AGENT_CHOICES.map(a=>'<option value="'+a+'"'+(a===(st.agentKey||'')?' selected':'')+'>'+(a||'(매핑 없음)')+'</option>').join('');
|
const agentOpts = AGENT_CHOICES.map(a=>'<option value="'+a+'"'+(a===(st.agentKey||'')?' selected':'')+'>'+(a||'(매핑 없음)')+'</option>').join('');
|
||||||
// 책상 sprite picker.
|
// 책상 sprite picker.
|
||||||
const deskOpts = DESK_SPRITE_CHOICES.map(s=>'<option value="'+s+'"'+(s===st.deskSprite?' selected':'')+'>'+s+'</option>').join('');
|
const deskOpts = DESK_SPRITE_CHOICES.map(s=>'<option value="'+s+'"'+(s===st.deskSprite?' selected':'')+'>'+_spriteLabel(s)+'</option>').join('');
|
||||||
// charRow 썸네일 picker (idle-r<n>-f0.png).
|
// charRow 썸네일 picker (idle-r<n>-f0.png).
|
||||||
let thumbs='';
|
let thumbs='';
|
||||||
for(let r=0;r<8;r++){
|
for(let r=0;r<8;r++){
|
||||||
@@ -1513,9 +1696,9 @@ function _renderObjProps(el){
|
|||||||
panel.classList.add('show');
|
panel.classList.add('show');
|
||||||
const name = el.dataset.objName || '';
|
const name = el.dataset.objName || '';
|
||||||
const w = el.dataset.objW || '';
|
const w = el.dataset.objW || '';
|
||||||
const opts = PROP_SPRITE_CHOICES.map(s=>'<option value="'+s+'"'+(s===name?' selected':'')+'>'+s+'</option>').join('');
|
const opts = PROP_SPRITE_CHOICES.map(s=>'<option value="'+s+'"'+(s===name?' selected':'')+'>'+_spriteLabel(s)+'</option>').join('');
|
||||||
panel.innerHTML =
|
panel.innerHTML =
|
||||||
'<h4>프랍 속성</h4>'+
|
'<h4>가구 속성</h4>'+
|
||||||
'<div class="pp-row"><label>sprite</label><select id="ppObjName">'+opts+'</select></div>'+
|
'<div class="pp-row"><label>sprite</label><select id="ppObjName">'+opts+'</select></div>'+
|
||||||
'<div class="pp-row"><label>너비 (px, 비우면 원본)</label><input id="ppObjW" type="number" value="'+w+'" placeholder="원본"></div>';
|
'<div class="pp-row"><label>너비 (px, 비우면 원본)</label><input id="ppObjW" type="number" value="'+w+'" placeholder="원본"></div>';
|
||||||
panel.querySelector('#ppObjName').onchange = (ev)=>{
|
panel.querySelector('#ppObjName').onchange = (ev)=>{
|
||||||
@@ -1536,7 +1719,7 @@ function _addNewDesk(){
|
|||||||
const baseX = 280 + ((__nextDeskN%5)*16);
|
const baseX = 280 + ((__nextDeskN%5)*16);
|
||||||
const baseY = 260 + ((__nextDeskN%5)*16);
|
const baseY = 260 + ((__nextDeskN%5)*16);
|
||||||
const st = {
|
const st = {
|
||||||
key: id, agentKey: '', label: '새 책상', charRow: 0, deskSprite: 'desk-main', face: 'R',
|
key: id, agentKey: '', label: '새 책상', charRow: 0, deskSprite: 'astra-desk-work', face: 'R',
|
||||||
boss: false,
|
boss: false,
|
||||||
deskX: baseX, deskY: baseY, deskW: 112,
|
deskX: baseX, deskY: baseY, deskW: 112,
|
||||||
seatX: baseX+4, seatY: baseY+36,
|
seatX: baseX+4, seatY: baseY+36,
|
||||||
@@ -1557,9 +1740,9 @@ function _openPropPicker(){
|
|||||||
overlay.className = 'prop-picker';
|
overlay.className = 'prop-picker';
|
||||||
const box = document.createElement('div');
|
const box = document.createElement('div');
|
||||||
box.className = 'prop-picker-box';
|
box.className = 'prop-picker-box';
|
||||||
box.innerHTML = '<h3>프랍 추가 — sprite 선택</h3>'+
|
box.innerHTML = '<h3>가구 추가</h3>'+
|
||||||
'<div class="prop-picker-grid">'+
|
'<div class="prop-picker-grid">'+
|
||||||
PROP_SPRITE_CHOICES.map(n=>'<div class="prop-pick" data-name="'+n+'"><img src="'+png(n)+'"><div class="pp-name">'+n+'</div></div>').join('')+
|
PROP_SPRITE_CHOICES.map(n=>'<div class="prop-pick" data-name="'+n+'"><img src="'+png(n)+'"><div class="pp-name">'+_spriteLabel(n)+'</div></div>').join('')+
|
||||||
'</div>';
|
'</div>';
|
||||||
overlay.appendChild(box);
|
overlay.appendChild(box);
|
||||||
overlay.onclick = (e)=>{ if(e.target === overlay) overlay.remove(); };
|
overlay.onclick = (e)=>{ if(e.target === overlay) overlay.remove(); };
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* Devil's Advocate (도현) — system prompt 빌더.
|
||||||
|
*
|
||||||
|
* 설계 원칙:
|
||||||
|
* - 모든 약점을 나열하지 않음. *한 turn 에 1개* — 사용자가 깊게 생각하게.
|
||||||
|
* - 'X 입장에서 본다면' framing — 본인 의견이 아니라 *다른 시점* 으로 제시.
|
||||||
|
* 이는 LLM 의 '내가 옳다' 경향을 줄이는 잘 알려진 패턴.
|
||||||
|
* - 통계 / 구체 수치 / 외부 사례 *금지* — 환각 표면적 차단.
|
||||||
|
* Phase 2 에서 외부 데이터 활성화 옵션 추가 예정.
|
||||||
|
* - 출처 명시 강제 — '(근거: 추론)' 또는 '(근거: brain/<file>)' 로 끝.
|
||||||
|
*
|
||||||
|
* 출력 형태:
|
||||||
|
* 3~5 문장 한 문단. 끝에 "① 우려" + "② 검증 방법 1개" 강제.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const DEVIL_PERSONA_NAME = '도현';
|
||||||
|
|
||||||
|
export interface DevilPromptInput {
|
||||||
|
/** 사용자가 던진 원래 질문. */
|
||||||
|
userPrompt: string;
|
||||||
|
/** Astra 의 직전 답변. 도현이 반박할 대상. */
|
||||||
|
assistantAnswer: string;
|
||||||
|
/** Optional Second Brain context — 있으면 인용 근거로 사용 가능. */
|
||||||
|
brainContext?: string;
|
||||||
|
/** Optional re-rebuttal — 사용자가 '재반박' 클릭 후 보낸 메시지. 있으면 다시 한 라운드. */
|
||||||
|
userRebuttal?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE_RULES = [
|
||||||
|
'당신은 \'도현\', 사용자의 사고를 더 깊게 만드는 *비판적 sparring partner* 입니다.',
|
||||||
|
'',
|
||||||
|
'규칙:',
|
||||||
|
'1. *한 turn 에 한 가지 약점* 만 골라 반박. 모든 약점을 나열하지 말 것 — 사용자가 깊게 생각하도록.',
|
||||||
|
'2. 본인 의견이 아닌 *다른 시점* 제시: "만약 비관적으로 본다면", "사용자 입장에서는", "1년 뒤 본다면" 등으로 framing 시작.',
|
||||||
|
'3. 칭찬 / 동의 / "좋은 답변입니다" 금지. 건설적이지만 *단호*.',
|
||||||
|
'4. 통계·연구·구체 수치·외부 사례 *인용 금지* — 당신은 그런 데이터를 보유하지 않음. 구조적·논리적·시나리오 비판만.',
|
||||||
|
'5. 답변은 *3~5 문장 한 문단*. 마지막에 반드시 두 줄:',
|
||||||
|
' ① 우려: <한 문장 핵심 우려>',
|
||||||
|
' ② 검증: <한 가지 구체적 검증 방법>',
|
||||||
|
'6. 답변 끝에 출처 태그: brain 자료 인용했으면 `(근거: brain/<filename>)`, 안 했으면 `(근거: 추론)`.',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
export function buildDevilSystemPrompt(): string {
|
||||||
|
return BASE_RULES;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDevilUserPrompt(input: DevilPromptInput): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
parts.push('## 사용자 원래 질문');
|
||||||
|
parts.push(input.userPrompt.trim() || '(빈 질문)');
|
||||||
|
parts.push('');
|
||||||
|
parts.push('## Astra 의 직전 답변 (당신이 반박할 대상)');
|
||||||
|
parts.push(input.assistantAnswer.trim() || '(빈 답변)');
|
||||||
|
if (input.brainContext && input.brainContext.trim()) {
|
||||||
|
parts.push('');
|
||||||
|
parts.push('## 참고 가능한 Second Brain 자료');
|
||||||
|
parts.push('(필요할 때만 인용. 인용하지 않으면 추론으로 표시.)');
|
||||||
|
parts.push(input.brainContext.trim());
|
||||||
|
}
|
||||||
|
if (input.userRebuttal && input.userRebuttal.trim()) {
|
||||||
|
parts.push('');
|
||||||
|
parts.push('## 사용자의 재반박');
|
||||||
|
parts.push(input.userRebuttal.trim());
|
||||||
|
parts.push('');
|
||||||
|
parts.push('재반박을 받았으니 *기존 입장을 굽히지 말되 한 단계 더 깊은 약점* 으로 이동하세요. 같은 약점을 반복하지 말 것.');
|
||||||
|
} else {
|
||||||
|
parts.push('');
|
||||||
|
parts.push('위 답변에서 *가장 본질적인 약점 하나* 를 골라 반박하세요.');
|
||||||
|
}
|
||||||
|
return parts.join('\n');
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Devil Agent — 직전 답변에 대한 반박 생성기.
|
||||||
|
*
|
||||||
|
* 동작:
|
||||||
|
* 1. agent.ts 가 main turn 완료 직후 호출
|
||||||
|
* 2. 같은 model/engine 으로 *별도 LLM call 1회* — 짧은 비판 한 문단 생성
|
||||||
|
* 3. 결과를 webview 로 'devilRebuttal' message 로 send
|
||||||
|
* 4. 실패 / 비활성 시 silent skip — main 답변에는 영향 없음
|
||||||
|
*
|
||||||
|
* 비용:
|
||||||
|
* - 매 turn 마다 LLM 1회 추가. 사용자가 settings 에서 명시적 ON 했을 때만.
|
||||||
|
* - 짧은 prompt + maxTokens 350 으로 제한 → 일반 turn 비용의 ~10-15%.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { buildDevilSystemPrompt, buildDevilUserPrompt, DevilPromptInput, DEVIL_PERSONA_NAME } from './devilPrompt';
|
||||||
|
|
||||||
|
const SETTING_KEY = 'g1nation.devilAgent.enabled';
|
||||||
|
|
||||||
|
export function isDevilAgentEnabled(): boolean {
|
||||||
|
const cfg = vscode.workspace.getConfiguration('g1nation');
|
||||||
|
return !!cfg.get<boolean>('devilAgent.enabled', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setDevilAgentEnabled(enabled: boolean): Promise<void> {
|
||||||
|
const cfg = vscode.workspace.getConfiguration('g1nation');
|
||||||
|
await cfg.update('devilAgent.enabled', enabled, vscode.ConfigurationTarget.Global);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DEVIL_PERSONA_NAME, SETTING_KEY };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main turn 끝나면 caller (agent.ts) 가 이 함수를 호출.
|
||||||
|
* - input 에 사용자 질문 + 직전 답변 + (선택) brain context 전달
|
||||||
|
* - 반환값: 도현의 반박 텍스트 또는 null (비활성·실패 시)
|
||||||
|
*
|
||||||
|
* 호출자가 webview 에 직접 'devilRebuttal' message 를 보내도록 분리 — 본 함수는 *pure*.
|
||||||
|
*/
|
||||||
|
export async function generateDevilRebuttal(
|
||||||
|
callLLM: (system: string, userMessage: string, maxTokens: number) => Promise<string>,
|
||||||
|
input: DevilPromptInput,
|
||||||
|
): Promise<string | null> {
|
||||||
|
if (!isDevilAgentEnabled()) return null;
|
||||||
|
try {
|
||||||
|
const system = buildDevilSystemPrompt();
|
||||||
|
const userMsg = buildDevilUserPrompt(input);
|
||||||
|
const out = await callLLM(system, userMsg, 350);
|
||||||
|
const cleaned = (out || '').trim();
|
||||||
|
if (!cleaned) return null;
|
||||||
|
// 환각 가드: 통계 / 수치 패턴이 보이면 *(근거: 추론)* 로 강제 변경 — 도현이 환각 자신 가질 가능성 줄임.
|
||||||
|
return _appendSourceTagIfMissing(cleaned);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _appendSourceTagIfMissing(text: string): string {
|
||||||
|
if (/\(근거:/.test(text)) return text;
|
||||||
|
return text.trim() + '\n\n(근거: 추론)';
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export {
|
||||||
|
buildDevilSystemPrompt,
|
||||||
|
buildDevilUserPrompt,
|
||||||
|
DevilPromptInput,
|
||||||
|
DEVIL_PERSONA_NAME,
|
||||||
|
} from './devilPrompt';
|
||||||
|
|
||||||
|
export {
|
||||||
|
isDevilAgentEnabled,
|
||||||
|
setDevilAgentEnabled,
|
||||||
|
generateDevilRebuttal,
|
||||||
|
SETTING_KEY,
|
||||||
|
} from './devilService';
|
||||||
@@ -98,6 +98,18 @@ interface SettingsState {
|
|||||||
connectedAt?: string;
|
connectedAt?: string;
|
||||||
lastIcalFetchAt?: string;
|
lastIcalFetchAt?: string;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Cloud LLM providers (OpenRouter / Anthropic / Gemini). API key 자체는 echo 안 함 —
|
||||||
|
* hasApiKey boolean 만 전송. enabled 와 defaultModel 은 settings 에서 직접 읽음.
|
||||||
|
*/
|
||||||
|
providers: {
|
||||||
|
openrouter: { enabled: boolean; hasApiKey: boolean; defaultModel: string };
|
||||||
|
anthropic: { enabled: boolean; hasApiKey: boolean; defaultModel: string };
|
||||||
|
gemini: { enabled: boolean; hasApiKey: boolean; defaultModel: string };
|
||||||
|
};
|
||||||
|
devilAgent: {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
/** Sectional banner shown when config.update fails (e.g. reload required). */
|
/** Sectional banner shown when config.update fails (e.g. reload required). */
|
||||||
bannerError?: string;
|
bannerError?: string;
|
||||||
}
|
}
|
||||||
@@ -238,6 +250,12 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider {
|
|||||||
case 'google.icalRefresh':
|
case 'google.icalRefresh':
|
||||||
await this._handleGoogleIcalRefresh();
|
await this._handleGoogleIcalRefresh();
|
||||||
return;
|
return;
|
||||||
|
case 'providers.update':
|
||||||
|
await this._handleProvidersUpdate(msg);
|
||||||
|
return;
|
||||||
|
case 'devilAgent.toggle':
|
||||||
|
await this._safeConfigUpdate('devilAgent.enabled', !!msg.enabled);
|
||||||
|
return;
|
||||||
case 'openVscodeSettings':
|
case 'openVscodeSettings':
|
||||||
await vscode.commands.executeCommand('workbench.action.openSettings', 'g1nation');
|
await vscode.commands.executeCommand('workbench.action.openSettings', 'g1nation');
|
||||||
return;
|
return;
|
||||||
@@ -509,6 +527,36 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider {
|
|||||||
await this._refreshState();
|
await this._refreshState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ────────────── Cloud LLM Providers ──────────────
|
||||||
|
// OpenRouter / Anthropic / Gemini API key + enable 토글. API key 는 Secret Storage 만.
|
||||||
|
// settings 패널은 *값 자체는 안 보여줌* (hasApiKey boolean 만). 사용자가 새로 입력 시 덮어씀.
|
||||||
|
|
||||||
|
private async _buildProvidersState(): Promise<SettingsState['providers']> {
|
||||||
|
const { readProviderStatus } = require('../providers') as typeof import('../providers');
|
||||||
|
const ctx = this._deps.context;
|
||||||
|
const [or, an, ge] = await Promise.all([
|
||||||
|
readProviderStatus(ctx, 'openrouter'),
|
||||||
|
readProviderStatus(ctx, 'anthropic'),
|
||||||
|
readProviderStatus(ctx, 'gemini'),
|
||||||
|
]);
|
||||||
|
return { openrouter: or, anthropic: an, gemini: ge };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleProvidersUpdate(msg: any): Promise<void> {
|
||||||
|
const { writeProviderConfig } = require('../providers') as typeof import('../providers');
|
||||||
|
const id = msg.providerId;
|
||||||
|
if (id !== 'openrouter' && id !== 'anthropic' && id !== 'gemini') return;
|
||||||
|
const patch: any = {};
|
||||||
|
if (typeof msg.enabled === 'boolean') patch.enabled = msg.enabled;
|
||||||
|
if (typeof msg.apiKey === 'string') patch.apiKey = msg.apiKey;
|
||||||
|
if (typeof msg.defaultModel === 'string') patch.defaultModel = msg.defaultModel;
|
||||||
|
if (Object.keys(patch).length === 0) return;
|
||||||
|
await writeProviderConfig(this._deps.context, id, patch);
|
||||||
|
this._lastSuccess = `${id} 저장 완료`;
|
||||||
|
this._lastError = undefined;
|
||||||
|
await this._refreshState();
|
||||||
|
}
|
||||||
|
|
||||||
private async _handleAdvancedUpdate(msg: any): Promise<void> {
|
private async _handleAdvancedUpdate(msg: any): Promise<void> {
|
||||||
if (typeof msg.dryRun === 'boolean') {
|
if (typeof msg.dryRun === 'boolean') {
|
||||||
await this._safeConfigUpdate('dryRun', msg.dryRun);
|
await this._safeConfigUpdate('dryRun', msg.dryRun);
|
||||||
@@ -573,6 +621,8 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider {
|
|||||||
maxContextSize: cfg.get<number>('maxContextSize', 32000) ?? 32000,
|
maxContextSize: cfg.get<number>('maxContextSize', 32000) ?? 32000,
|
||||||
},
|
},
|
||||||
google: this._buildGoogleState(),
|
google: this._buildGoogleState(),
|
||||||
|
providers: await this._buildProvidersState(),
|
||||||
|
devilAgent: { enabled: cfg.get<boolean>('devilAgent.enabled', false) },
|
||||||
bannerError: this._bannerError,
|
bannerError: this._bannerError,
|
||||||
};
|
};
|
||||||
const payload = { type: 'state', value: state };
|
const payload = { type: 'state', value: state };
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { buildDevilSystemPrompt, buildDevilUserPrompt } from '../src/features/devilAgent/devilPrompt';
|
||||||
|
|
||||||
|
describe('Devil Agent prompt builder', () => {
|
||||||
|
test('system prompt enforces the 6 core rules', () => {
|
||||||
|
const sys = buildDevilSystemPrompt();
|
||||||
|
// 핵심 규칙들이 모두 들어있는지 검증 — 정확한 문자열은 prompt 본문 변경에 깨지지만,
|
||||||
|
// 가치 보존되는 핵심 키워드 기준으로 체크.
|
||||||
|
expect(sys).toContain('도현');
|
||||||
|
expect(sys).toContain('한 가지 약점'); // rule 1
|
||||||
|
expect(sys).toContain('만약'); // rule 2 framing
|
||||||
|
expect(sys).toContain('칭찬'); // rule 3 — 금지
|
||||||
|
expect(sys).toContain('통계'); // rule 4 — 환각 가드
|
||||||
|
expect(sys).toContain('우려'); // rule 5 출력 형식
|
||||||
|
expect(sys).toContain('검증'); // rule 5 출력 형식
|
||||||
|
expect(sys).toContain('근거:'); // rule 6 출처 태그
|
||||||
|
});
|
||||||
|
|
||||||
|
test('user prompt includes both user prompt and assistant answer', () => {
|
||||||
|
const u = buildDevilUserPrompt({
|
||||||
|
userPrompt: '회원가입 흐름 설계 도와줘',
|
||||||
|
assistantAnswer: '이메일+비번 기본 flow 를 권장합니다.',
|
||||||
|
});
|
||||||
|
expect(u).toContain('회원가입 흐름 설계 도와줘');
|
||||||
|
expect(u).toContain('이메일+비번 기본 flow 를 권장합니다');
|
||||||
|
expect(u).toContain('Astra 의 직전 답변');
|
||||||
|
expect(u).toContain('가장 본질적인 약점 하나');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('user prompt with brainContext adds reference section', () => {
|
||||||
|
const u = buildDevilUserPrompt({
|
||||||
|
userPrompt: 'Q',
|
||||||
|
assistantAnswer: 'A',
|
||||||
|
brainContext: '## brain/sample.md\n샘플 자료입니다.',
|
||||||
|
});
|
||||||
|
expect(u).toContain('참고 가능한 Second Brain 자료');
|
||||||
|
expect(u).toContain('샘플 자료입니다');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('user prompt with userRebuttal switches mode to deeper round', () => {
|
||||||
|
const u = buildDevilUserPrompt({
|
||||||
|
userPrompt: 'Q',
|
||||||
|
assistantAnswer: 'A',
|
||||||
|
userRebuttal: '그건 사용자 입장에선 오히려 편리한데?',
|
||||||
|
});
|
||||||
|
expect(u).toContain('사용자의 재반박');
|
||||||
|
expect(u).toContain('그건 사용자 입장에선 오히려 편리한데');
|
||||||
|
expect(u).toContain('한 단계 더 깊은 약점');
|
||||||
|
// 첫 라운드 표시는 보이지 말아야 — userRebuttal 있으면 다른 분기.
|
||||||
|
expect(u).not.toContain('가장 본질적인 약점 하나를 골라');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('user prompt handles empty fields gracefully', () => {
|
||||||
|
const u = buildDevilUserPrompt({ userPrompt: '', assistantAnswer: '' });
|
||||||
|
expect(u).toContain('(빈 질문)');
|
||||||
|
expect(u).toContain('(빈 답변)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// generateDevilRebuttal 은 vscode 의존 — settings 토글 / module dynamic import 흐름 때문에
|
||||||
|
// 본 unit suite 에서는 직접 호출 X. 대신 prompt 빌더에 집중.
|
||||||
|
// settings 통합 흐름은 통합 테스트(향후)로 분리.
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { parseModelPrefix, makeModelId, providerLabel } from '../src/features/providers/types';
|
||||||
|
import { _internals } from '../src/features/providers/streamHelpers';
|
||||||
|
|
||||||
|
describe('parseModelPrefix', () => {
|
||||||
|
test('matches openrouter / anthropic / gemini prefixes', () => {
|
||||||
|
expect(parseModelPrefix('openrouter:anthropic/claude-3.5-sonnet')).toEqual({
|
||||||
|
provider: 'openrouter', model: 'anthropic/claude-3.5-sonnet',
|
||||||
|
});
|
||||||
|
expect(parseModelPrefix('anthropic:claude-3-5-sonnet-20241022')).toEqual({
|
||||||
|
provider: 'anthropic', model: 'claude-3-5-sonnet-20241022',
|
||||||
|
});
|
||||||
|
expect(parseModelPrefix('gemini:gemini-2.0-flash-exp')).toEqual({
|
||||||
|
provider: 'gemini', model: 'gemini-2.0-flash-exp',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for local engine model ids', () => {
|
||||||
|
expect(parseModelPrefix('gemma4:e2b')).toBeNull();
|
||||||
|
expect(parseModelPrefix('llama-3.2:8b')).toBeNull();
|
||||||
|
expect(parseModelPrefix('google/gemma-4-e4b')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for empty / undefined input', () => {
|
||||||
|
expect(parseModelPrefix('')).toBeNull();
|
||||||
|
expect(parseModelPrefix(undefined as any)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('makeModelId round-trips with parseModelPrefix', () => {
|
||||||
|
const id = makeModelId('openrouter', 'meta/llama-3.3-70b');
|
||||||
|
expect(id).toBe('openrouter:meta/llama-3.3-70b');
|
||||||
|
expect(parseModelPrefix(id)).toEqual({ provider: 'openrouter', model: 'meta/llama-3.3-70b' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('providerLabel returns Korean-friendly labels', () => {
|
||||||
|
expect(providerLabel('openrouter')).toBe('OpenRouter');
|
||||||
|
expect(providerLabel('anthropic')).toBe('Anthropic');
|
||||||
|
expect(providerLabel('gemini')).toBe('Gemini');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Anthropic event → OpenAI SSE conversion', () => {
|
||||||
|
const conv = _internals.anthropicEventToOpenAI;
|
||||||
|
|
||||||
|
test('content_block_delta with text emits OpenAI chunk', () => {
|
||||||
|
const out = conv('content_block_delta', '{"delta":{"type":"text_delta","text":"안녕"}}');
|
||||||
|
expect(out).toBeTruthy();
|
||||||
|
expect(out).toContain('"content":"안녕"');
|
||||||
|
expect(out).toMatch(/^data: /);
|
||||||
|
expect(out!.endsWith('\n\n')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-delta events (message_start, content_block_start, etc.) are ignored', () => {
|
||||||
|
expect(conv('message_start', '{}')).toBeNull();
|
||||||
|
expect(conv('content_block_start', '{}')).toBeNull();
|
||||||
|
expect(conv('message_stop', '{}')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('malformed JSON returns null', () => {
|
||||||
|
expect(conv('content_block_delta', 'not json')).toBeNull();
|
||||||
|
expect(conv('content_block_delta', '{')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty text field returns null (no zero-length chunks)', () => {
|
||||||
|
expect(conv('content_block_delta', '{"delta":{"type":"text_delta","text":""}}')).toBeNull();
|
||||||
|
expect(conv('content_block_delta', '{"delta":{}}')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Gemini event → OpenAI SSE conversion', () => {
|
||||||
|
const conv = _internals.geminiEventToOpenAI;
|
||||||
|
|
||||||
|
test('extracts text from candidates[0].content.parts', () => {
|
||||||
|
const out = conv(null, '{"candidates":[{"content":{"parts":[{"text":"안녕"}],"role":"model"}}]}');
|
||||||
|
expect(out).toBeTruthy();
|
||||||
|
expect(out).toContain('"content":"안녕"');
|
||||||
|
expect(out).toMatch(/^data: /);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('joins multiple parts into one chunk', () => {
|
||||||
|
const out = conv(null, '{"candidates":[{"content":{"parts":[{"text":"안"},{"text":"녕"}]}}]}');
|
||||||
|
expect(out).toContain('"content":"안녕"');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null when candidates missing / empty', () => {
|
||||||
|
expect(conv(null, '{}')).toBeNull();
|
||||||
|
expect(conv(null, '{"candidates":[]}')).toBeNull();
|
||||||
|
expect(conv(null, '{"candidates":[{"content":{}}]}')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('malformed JSON returns null', () => {
|
||||||
|
expect(conv(null, 'not json')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty text returns null', () => {
|
||||||
|
expect(conv(null, '{"candidates":[{"content":{"parts":[{"text":""}]}}]}')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||