워크스페이스
Markdown 기반 Agent ID 및 메모리와, 지속적인 대화 콘텐츠를 담는 아티팩트 및 파일 스토어
Minara는 ID, 페르소나, 큐레이션된 메모리, 세션 간 상태를 워크스페이스 디렉터리(기본값: ~/.minara/workspace/)안의 평범한 Markdown 파일로 저장합니다.
워크스페이스는 Agent의 사람이 직접 관리하는 단일 진실 공급원입니다. 추론된 사용자 선호, 학습된 방법론, 시나리오 플레이북이 MEMORY.md / SOUL.md / AGENTS.md의 내용과 충돌하면 워크스페이스가 우선합니다. 에이전트 루프가 시스템 프롬프트를 조립하는 순서에 대해서는 에이전트 루프 문서에서 동적 블록과 캐시 블록의 구성 방식을 확인하십시오.
온디스크 레이아웃은 OpenClaw 호환입니다. 파일 이름, 섹션, 프런트매터는 OpenClaw의 AGENTS.default 스키마와 일치하므로, 어느 쪽 도구로 작성한 워크스페이스도 다른 도구로 이식할 수 있습니다(아래 OpenClaw 호환성 참조).
파일 구성
| 파일 | 역할 | 생명 주기 |
|---|---|---|
SOUL.md | ID, 어조, 경계 | 장기; 편집 시 사용자에게 일회성 공시 |
AGENTS.md | 세션 시작 규칙, 안전 기본값 | 장기 |
IDENTITY.md | 표시 이름, 이모지, 분위기(UI 표면) | 장기 |
USER.md | 운영자 프로필(시간대, 집중 영역, 위험 허용 범위, 관심 목록) | 장기 |
MEMORY.md | 큐레이션된 사실 / 선호 / 결정 | 장기; 드리밍 태스크가 ## Dreamed YYYY-MM-DD 섹션을 추가 |
BOOTSTRAP.md | 2단계 최초 실행 온보딩 플레이북 | Agent가 BOOTSTRAP_DONE을 출력한 후 .done.<timestamp>로 아카이브됨 |
TOOLS.md | 환경별 도구 / 스킬 메모 | 장기; 운영자 직접 관리 |
HEARTBEAT.md | 세션 간 메모(last_seen, 미결 루프, 일정) | 매 턴 종료 시 Agent가 상태 섹션을 덮어씀; ## Schedule은 사용자 소유 |
memory/YYYY-MM-DD-<session>.md | 세션별 일일 저널 | N 턴마다 추가; 드리밍 태스크가 통합 |
HEARTBEAT.md만 Agent가 매 턴 덮어씁니다. 나머지 파일은 운영자가 편집하거나(드리밍 태스크는 제안만 합니다).
부팅 시 자동 시드
createApp()은 워크스페이스를 로드하기 전에 seedWorkspaceIfMissing(workspaceDir)을 호출하여, apps/agent/src/workspace/templates/에서 누락된 템플릿을 런타임 디렉터리로 복사합니다. 시드는 멱등 방식으로 동작합니다. 운영자가 편집했거나 이미 존재하는 파일은 절대 덮어쓰지 않습니다. 신규 설치 시에는 minara setup을 별도로 실행하지 않아도 전체 파일 세트가 준비됩니다.
HEARTBEAT.md는 의도적으로 시드 대상에서 제외됩니다. Agent가 첫 번째 턴 종료 시 새로 작성하며, 오래된 상태를 시드하면 세션 연속성에 대해 거짓 정보를 제공하게 됩니다. 템플릿 파일은 apps/agent/src/workspace/templates/HEARTBEAT.md에 있으며, Web UI의 restore-template 소스와 스키마 참조 용도로만 사용됩니다.
워크스페이스 편집
워크스페이스 파일을 편집하는 방법은 세 가지입니다.
- 디스크에서 직접 편집. Agent가 세션 사이에 있을 때 원하는 에디터로 파일을 엽니다. 다음 세션에서 저장된 내용을 읽습니다.
- REPL 슬래시 명령.
/soul,/identity,/heartbeat,/workspace [soul|agents|identity|user|memory|heartbeat|bootstrap]는 현재 파일 내용을 출력합니다. 전체 목록은 슬래시 명령 참조를 확인하십시오. - Web UI 설정 → 워크스페이스. 허용 목록에서 파일을 선택하고 텍스트 영역에서 편집한 후 수동으로 저장합니다. 저장은 sha256 낙관적 동시성을 통해 게이트웨이를 거칩니다. 모든
PUT요청에는 편집기가 로드할 때의 sha256이 포함되며, 게이트웨이에서 409가 반환되면 "덮어쓰기 또는 새로 고침" 모달이 열려 브라우저 탭이나 세션 간 동시 편집이 조용히 충돌하는 상황을 방지합니다.
Web UI 편집기는 파일 시스템에 직접 접근하지 않습니다. 모든 작업은 /v1/workspace/files*를 통해 라우팅됩니다(HTTP API 참조 참조).
세 가지 메모리 계층
워크스페이스는 SQLite 메모리 스토어(메모리 시스템 참조)와 세 가지 시간 지평을 기준으로 협력합니다.
- 단기, 일일 저널.
memory/YYYY-MM-DD-<session>.md는 일일 로그 작성기가WORKSPACE_DAILY_LOG_INTERVAL턴마다 내용을 추가합니다. 세션별 파일 구조 덕분에 REPL과 HTTP 게이트웨이 간의 추가 경쟁 조건을 방지합니다. 레거시 집계 형식인YYYY-MM-DD.md와 새로운 세션별 형식 모두 다음 세션 로더에서 인식됩니다. - 장기, 큐레이션.
MEMORY.md에는 운영자가 직접 관리하는## Facts,## Preferences,## Decisions세 개의 하위 섹션이 있습니다. 드리밍 태스크는## Dreamed YYYY-MM-DD섹션으로 추가만 합니다(기존 내용은 절대 덮어쓰지 않습니다). - 세션 간 메모, 하트비트.
HEARTBEAT.md에는last_seen,session_id,surface,turn_count, 최근open_loops, 사용자 소유의## Schedule이 포함됩니다. 작성기는## Schedule섹션을 매번 그대로 보존합니다(schedule_raw라운드트립). 따라서 운영자 주석, 빈 줄 그룹, 알 수 없는 필드도 모두 유지됩니다.
드리밍 통합
WORKSPACE_DREAM_ENABLED=1일 때 setInterval 태스크가 WORKSPACE_DREAM_INTERVAL_HOURS마다 깨어나 다음을 수행합니다.
.dreamed-state.json의 워터마크보다 새로운 일일 로그를 읽습니다.- 해당 로그와 현재
MEMORY.md를 활성 LLM에 전달하며, "지속 가능한 신호와 노이즈를 구분하고 기존 사실을 중복 추가하지 말 것"이라는 프롬프트를 함께 제공합니다. MEMORY.md에## Dreamed YYYY-MM-DD섹션을 추가합니다(추가 전용이며 운영자 편집 내용은 절대 덮어쓰지 않습니다).- 다음 실행 시 같은 로그가 재처리되지 않도록 워터마크를 갱신합니다.
LLM이 문자 그대로 NOTHING_TO_PROMOTE를 반환하면 섹션은 기록되지 않지만 워터마크는 여전히 전진하여 같은 로그가 재처리되지 않습니다.
SOUL.md 변경 공시
런타임은 부팅 시 SOUL.md를 해싱하여 결과를 <workspaceDir>/.soul-state.json에 저장합니다. 세션 간에 해시가 변경되면 에이전트 루프는 다음 응답에서 운영자에게 변경 사항을 알리도록 프롬프트 수준의 일회성 알림을 수신합니다. 단 한 번의 확인 후에는 더 이상 언급되지 않으며, 감지기는 매번 읽을 때마다 기준선을 재설정합니다.
BOOTSTRAP.md 2단계 온보딩
신규 워크스페이스에는 BOOTSTRAP.md가 포함되어 있습니다. 흐름은 다음과 같습니다.
- 1번째 턴. Agent가
BOOTSTRAP.md를 읽고 운영자에게 온보딩 질문(이름, 시간대, 집중 영역, 위험 허용 범위, 기본 거래소)을 합니다.BOOTSTRAP_DONE은 출력하지 않습니다. - 2번째 턴. 운영자가 답변하면 Agent는
write_file을 사용해USER.md를 작성하고, 작성된 내용을 간략히 확인한 후, 응답의 마지막에BOOTSTRAP_DONEtoken을 단독 줄로 출력합니다.
부트스트랩 핸들러 훅(턴 종료 시)이 해당 token을 감지하면 BOOTSTRAP.md를 BOOTSTRAP.md.done.<timestamp>로 이름을 변경합니다(감사 추적 보존). 다음 턴에는 파일이 사라지므로 동적 프롬프트 블록에서 부트스트랩 지침이 자동으로 제거됩니다.
워크스페이스를 단일 진실 공급원으로(우선순위)
에이전트 루프는 파생 계층보다 워크스페이스 Markdown을 먼저 시스템 프롬프트에 배치합니다.
identity블록(캐시됨).SOUL.md+AGENTS.md.memorySnapshot(동적, 동적 블록의 선두).USER.md,MEMORY.md,HEARTBEAT.md, 최근 일일 저널 항목.bootstrapInstructions(동적,BOOTSTRAP.md가 존재할 때만). 최초 실행 플레이북.- 스킬 카탈로그, 시나리오 플레이북, 개인화, 방법론 힌트, 기타 파생 계층은 그 이후에 배치됩니다.
이 순서는 의도된 것입니다. 예를 들어 MEMORY.md에 "사용자는 BTC 거래 시 무기한 선물보다 현물을 선호한다"고 명시되어 있는데 개인화 계층이 "사용자는 무기한 선물을 선호한다"고 추론하면 MEMORY.md가 우선합니다. 이 우선순위는 apps/agent/src/core/prompt-builder.ts에 적용된 계약이며 AGENTS.md의 Soul 섹션에도 명문화되어 있습니다.
설정
기본값은 신규 설치 후 바로 유용하게 사용할 수 있도록 조정되어 있습니다.
- 하트비트: 활성화(
WORKSPACE_HEARTBEAT_ENABLED=1) - 일일 로그: 비활성화(
WORKSPACE_DAILY_LOG_ENABLED=0) - 드리밍: 비활성화(
WORKSPACE_DREAM_ENABLED=0)
기본값과 효과를 포함한 전체 목록은 환경 변수 참조를 확인하십시오.
아티팩트와 파일
워크스페이스 Markdown은 지속적인 상태입니다. 그 옆에는 지속적인 콘텐츠를 담는 두 개의 스토어가 있습니다. 아티팩트(Agent가 턴 중에 생성하는 차트, 스프레드시트, 보고서)와 파일(사용자가 업로드하는 바이너리)입니다. 둘 다 워크스페이스 및 샌드박스와 나란히 있지만 계약은 다릅니다. 샌드박스는 Agent가 턴 중에 자유롭게 덮어쓰는 스크래치 공간인 반면, 아티팩트와 파일은 세션 경계를 넘어 유지되어야 하고, 안정적인 id를 가지며, 부주의한 write_file로 변경되어서는 안 됩니다. 이들은 샌드박스와 경로를 공유하지 않으므로, 스크래치 출력을 쓰는 도구가 저장된 차트나 사용자 업로드를 덮어쓸 수 없습니다.
아티팩트 스토어
아티팩트는 chat_artifacts SQLite 테이블에 저장되며 artifacts/artifact-store.ts가 소유합니다. 세 가지 유형이 동일한 행 구조를 공유합니다.
| 유형 | id 접두사 | data 페이로드 | 빌더 |
|---|---|---|---|
chart | x- | { charts: [...] } ECharts 옵션 | chart-builder.ts |
spreadsheet | x- | { csvContent, title?, description? } | CSV, .csv로 구체화 |
report | r- | 전체 HTML / markdown 보고서 | 딥 리서치 |
각 빌더는 동일한 상태 경로를 따릅니다. insert()(running) → markCompleted(data)(completed) 또는 markError(msg)(error). REPL은 (running) 자리표시자를 렌더링하고 제자리에서 갱신하며, 감사 로그가 모든 전환을 기록합니다.
모델은 완료된 아티팩트를 id를 추측하지 않고 URI로 참조합니다. chart://x-…, report://r-…, spreadsheet://x-…입니다. 존재하지 않는 id는 구조화된 ArtifactNotFoundError로 해석되며, 모델은 이를 보고 스스로 수정합니다. 턴 종료 시 gateway/render/artifact-materializer.ts가 최종 메시지에서 이러한 URI를 스캔하여 탐색 가능한 파일(자체 완결형 HTML 뷰어, 원본 JSON, 차트당 2배 PNG, 스프레드시트당 .csv)을 $dataDir/artifacts/<id>/에 씁니다. PNG 렌더링은 최선의 노력입니다. Playwright Chromium이 설치되어 있지 않으면 머티리얼라이저는 chart_png_skipped를 기록하고 계속 진행합니다.
파일 스토어
파일은 Agent가 저장하고 참조하지만 파싱하지는 않는 불투명한 바이트이며, files/file-store.ts가 소유합니다. 기본 LocalFileStore는 $dataDir/uploads/에 쓰며, 키는 chat/files/<userId>/<unix_ms>-<random_id>.<ext>입니다(unix_ms 접두사 덕분에 업로드가 자연스럽게 시간순으로 정렬됩니다).
StoredFile은 key(채팅 메시지에 기록되는 안정적인 id)와 url(LLM 제공자가 멀티모달 메시지를 위해 가져오는 주소)을 가집니다. 업로드(POST /v1/files)는 fileStore.put(...)을 거쳐 key를 대기 중 메시지에 첨부합니다. 기본 20 MB 제한은 제공자의 멀티모달 한도에 맞춰집니다. files/message-translator.ts는 루프로 들어갈 때 첨부 key를 각 제공자가 선호하는 멀티모달 형태로 매핑하고, 스토리지로 돌려보낼 때 다시 key로 매핑합니다. 메시지 텍스트는 sessions 행에, 바이너리 콘텐츠는 파일 스토어에 저장되며, 트랜슬레이터가 둘을 잇습니다. sessions에는 원본 바이너리가 저장되지 않습니다. 업로드된 스프레드시트는 files/spreadsheet-parser.ts에 의해 구조화된 텍스트로 변환되어, 모델이 전체 첨부 왕복 없이 그 내용을 추론할 수 있습니다.
보안 태세
- 파일은 절대 실행되지 않습니다. 이 스토어는 blob 캐시이며, 업로드된 콘텐츠를 eval하는 코드 경로가 없습니다.
- key와 아티팩트 id는 비밀이 아니라 안정적인 참조입니다. URL이나
chart://id는 소지자 권한으로 취급하십시오. 게이트웨이가 그 위에 세션 수준 인증을 추가합니다. - 삭제는 명시적입니다.
LocalFileStore.delete(key)가 로컬 파일을 제거합니다. GDPR 방식의 삭제는 이를 통해 수행하여 제거가 의도적이고 추적 가능하도록 하십시오.
이를 반환하는 HTTP 엔드포인트는 API → /files 및 아티팩트 가져오기 라우트를 참조하십시오.
OpenClaw 호환성
워크스페이스 형식은 OpenClaw의 AGENTS.default 스키마와 바이트 수준에서 호환됩니다. 파일 이름, 섹션 제목, 프런트매터 구조가 동일합니다. 어느 쪽 도구로 작성한 워크스페이스도 별도의 변환 없이 다른 도구로 이식할 수 있습니다. 각 도구가 별도로 소유하는 런타임 전용 아티팩트만 성격이 다릅니다.
기존 OpenClaw 워크스페이스를 Minara로 가져오는 방법은 세 가지입니다.
- 실행 시
--workspace ~/.openclaw/workspace를 전달합니다. - 환경 변수에
MINARA_WORKSPACE_DIR=~/.openclaw/workspace를 설정합니다. OpenClawWorkspaceImporter를 실행하여 파일을 Minara의 기본 위치로 복사합니다.
BOOTSTRAP.md와 HEARTBEAT.md는 임포터에서 의도적으로 건너뜁니다. 전자는 일회성 온보딩 플레이북으로 Minara가 자체 시드를 제공하고, 후자는 Agent가 매 턴 종료 시 새로 작성하므로 오래된 하트비트는 세션 연속성에 대해 거짓 정보를 제공하기 때문입니다.