Akan.js
Docs
문서컨벤션레퍼런스Cheatsheet
Akan.js
문서컨벤션레퍼런스Cheatsheet
Akan.js

MIT 라이선스 하에 배포되었습니다.

  • Akan.js 공식 컨설팅 서비스AkansoftCopyright © 2026 Akan.js 모든 권리 보유.시스템 관리자bassman
    사람함께에이전트▾
    사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
    함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
    에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
    소개▾
    시작하기기본 개념실습하기
    튜토리얼▾
    상세하게 보여주기상태 변경하기서비스 내에서 상호작용슬라이스로 표시하기페이지를 통한 UX스칼라 사용하기인사이트 사용하기데이터 연결하기
    핵심 개념▾
    폴더 규칙파일 규칙파일 기반 라우팅데이터 레이어앱 설정Akan 런타임다중 클라이언트
    시스템 아키텍처▾
    아키텍처 개요UI 아키텍처UI 구성비즈니스 서비스인페이지 에이전트CSS와 스타일링UI 레시피 레이어모바일 앱 아키텍처런타임과 인프라
    사람함께에이전트▾
    사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
    함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
    에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
    소개▾
    시작하기기본 개념실습하기
    튜토리얼▾
    상세하게 보여주기상태 변경하기서비스 내에서 상호작용슬라이스로 표시하기페이지를 통한 UX스칼라 사용하기인사이트 사용하기데이터 연결하기
    핵심 개념▾
    폴더 규칙파일 규칙파일 기반 라우팅데이터 레이어앱 설정Akan 런타임다중 클라이언트
    시스템 아키텍처▾
    아키텍처 개요UI 아키텍처UI 구성비즈니스 서비스인페이지 에이전트CSS와 스타일링UI 레시피 레이어모바일 앱 아키텍처런타임과 인프라
    이전비즈니스 서비스다음CSS와 스타일링

    인페이지 에이전트

    모든 Akan 앱에는 화면을 읽고 사용자 대신 조작하는 채팅 에이전트를 둘 수 있습니다. 지금 이 페이지의 어시스턴트가 바로 그것입니다. 에이전트는 화면이 이미 내어 준 컨트롤만 누를 수 있으므로, 사용자에게 없는 레버를 갖는 일은 없습니다.
    할 수 있는 일은 컴포넌트가 선언한 것, 읽을 수 있는 것은 컴포넌트가 구독한 것입니다. 스토어 클래스만으로는 아무것도 공개되지 않습니다.
    한 줄 마운트
    레이아웃의 <Agent.Chat /> 한 줄이 통합의 전부입니다. 런처, 대화창, 승인 카드, 스트리밍 루프까지 들어 있습니다.
    툴은 브라우저에서 실행
    서버는 툴을 실행하지 않는 무상태 릴레이입니다. 모든 동작은 사용자 자신의 세션에서, 가드와 승인 카드를 거쳐 실행됩니다.
    프레임워크 내장
    릴레이 엔드포인트, LLM 어댑터 두 개, 채팅 UI가 모두 akanjs에 들어 있습니다. 따로 붙일 라이브러리가 없습니다.
    툴은 브라우저에서 실행됩니다
    채팅과 모든 툴은 사용자의 브라우저 안에서, 페이지가 이미 보여주는 컨트롤 위에서 실행됩니다. 서버는 LLM 키만 쓰는 릴레이일 뿐이며 툴을 실행하지 않습니다.
    이 페이지에서 쓰는 말
    용어설명
    tool
    컴포넌트가 에이전트에게 공개한 동작 하나입니다. 보통 버튼이 이미 부르는 핸들러입니다.
    surface
    지금 화면에서 에이전트가 하고 읽을 수 있는 것 전부, 즉 마운트된 툴과 키입니다.
    turn
    모델의 응답 한 번과, 그 사이에 한 툴 호출 전부입니다.
    transcript
    지금까지의 대화입니다. 매 턴마다 모델에 다시 전송됩니다.
    approval card
    사용자가 승인할 때까지 호출을 붙잡아 두는 카드입니다.
    relay
    서버 엔드포인트 runAgentTurn입니다. 대화를 LLM에 전달할 뿐 툴은 실행하지 않습니다.
    zone
    Agent.Zone으로 감싼 구획입니다. 자기만의 대화를 가집니다.
    런타임 지도
    구성 요소설명
    Screen
    마운트된 st.use / st.sel / st.ref 키, 훅 툴, Agent.Guide 문구.
    Agent.Chat
    대화 루프, 승인 카드, 그리고 / 메뉴에 오르는 슬래시 커맨드 여섯 개.
    runAgentTurn
    무상태 HTTP 릴레이입니다. LLM 키만 쓰고 툴은 실행하지 않습니다.
    LlmAdaptor.chat
    전체 대화가 들어가고 어시스턴트 응답 하나가 나옵니다. 기본값은 OpenaiLlm입니다.
    HTTP로 도메인을 호출하는 외부 에이전트는 대신 MCP 서버를 씁니다. signal 가드에서 만들어지는 별도의 카탈로그입니다. MCP 서버

    마운트와 보안

    에이전트를 켜는 데는 세 단계면 됩니다. 채팅을 마운트하고, LLM 키를 주고, 누가 쓸 수 있는지 정합니다.
    1. 레이아웃에 <Agent.Chat />을 한 번 마운트합니다. runAgentTurn 릴레이는 모든 앱에 이미 제공됩니다.
    2. lib/option.ts에서 option.setLlm으로 키를 줍니다.
    3. option.setAgentAccess로 가드를 지정합니다. 지정하기 전까지는 모든 호출이 거절됩니다.
    apps/<app>/page/_layout.tsx · apps/<app>/lib/option.ts
    AgentRelayAccess는 가드가 등록되기 전까지 None과 똑같이 모든 호출을 거절하므로, 채팅이 LLM 키를 쓸 수 없습니다. 계정이 있는 제품은 다른 엔드포인트처럼 option.ts에서 자기 가드를 지정합니다. AKAN_AGENT=false는 표면 전체를 내립니다.
    Agent.Chat 옵션
    옵션설명
    persist
    기본값은 꺼짐입니다. 대화를 sessionStorage에 보존하고, { storage: "local" }이면 탭을 닫아도 남습니다.
    streaming
    텍스트가 생성되는 대로 나타납니다. 같은 엔드포인트가 text/event-stream도 답하므로 앱 코드는 없습니다.
    instructions
    앱 전역 지침입니다. 라우트 범위 지침은 마운트된 Agent.Guide가 그 위에 겹칩니다.
    attach
    PDF 본문처럼 파서가 필요한 파일을 처리하거나, 업로드한 뒤 url로 답합니다.
    voice
    눌러서 말하는 마이크와, 음성으로 물은 질문에 대한 음성 응답입니다.
    패널 자체(controlled open 쌍, _overrides.tsx 슬롯 열두 개, card 툴, @ 메뉴, 대기열, 대화 보관)는 Agent Chat에서 다룹니다.
    첨부
    • 이미지와 텍스트 파일은 알아서 첨부됩니다. attach는 파서나 업로드가 필요한 파일에만 씁니다.
    • 저장하지 않습니다. 바이트는 한 턴의 요청에만 실리고, 새로고침한 대화에는 파일 이름만 남습니다.
    • 상한은 메시지 단위입니다. 파일당 4MB, 메시지당 8MB와 5개이고, 같은 파일을 두 번 넣으면 이름을 밝혀 거절합니다. 프로바이더가 거절하는 것은 합계이고, 보낼 수 없는 요청은 사용자가 작성창을 비워야만 빠져나올 수 있기 때문입니다.
    • 상한은 attach가 만든 결과로 잽니다. 그래서 url은 비용이 없고, 더 큰 요청을 받는 프로바이더라면 attachLimits로 올립니다.
    음성
    • 말하고, 고치고. 말한 내용은 작성창에 들어가므로 보내기 전에 고칠 수 있습니다.
    • 음성으로 물었을 때만 읽어 줍니다. 응답은 문장 단위로 읽고, 타이핑한 질문이 스피커를 켜는 일은 없습니다.
    • 엔진은 useSpeech입니다. @libs/util/webkit에 있고 브라우저 내장 인식과 합성을 씁니다. Android·iOS 앱의 WebView에는 둘 다 없고 @akanjs/native에는 아직 음성 플러그인이 없으므로, 앱에서는 채팅에 마이크가 그려지지 않습니다.
    attach와 voice는 함수를 담고 있고, 함수는 RSC 경계를 넘지 못합니다. 그래서 서버 레이아웃에서는 넘길 수 없습니다. 대신 ui/에 작은 클라이언트 컴포넌트를 두고 거기서 채팅을 마운트하세요:
    apps/akan/ui/DocsAgentChat.tsx

    선언하는 표면

    에이전트의 표면은 마운트된 컴포넌트가 선언한 것, 딱 그만큼입니다. 같은 일을 하는 컨트롤 바로 옆에 툴을 선언하면, 에이전트와 사용자가 같은 핸들러 하나를 누릅니다.
    • st.tool은 동작 하나를 공개하고, onClick에 연결할 callable을 돌려줍니다.
    • st.use, st.sel, st.ref는 스토어 키 하나를 읽을 수 있게 합니다. 그 키를 읽는 컴포넌트가 마운트된 동안만입니다.
    • 언마운트되면 둘 다 다음 턴부터 사라집니다.
    <Model>.Zone.tsx — the tool and the button are one declaration
    툴과 상태를 선언하는 API
    st.tool(name).desc(…).arg(…).opt(…).exec(fn)
    • 동작이 에이전트에게 닿는 유일한 길입니다. onClick에 연결할 callable을 돌려줍니다.
    • desc는 필수이고 맨 앞에 옵니다. arg는 반드시 넘길 인자, opt는 생략할 수 있는 인자이며 생략된 opt는 null로 들어옵니다.
    • 둘 다 스칼라, enum, 그리고 그 배열 한 겹([String], [TaskStatus])까지 받습니다. 목록을 문자열 포맷으로 가르칠 일이 없습니다.
    • 세 번째 인자는 렌더 시점에 값을 좁힙니다. .arg("branch", String, { oneOf: branchCodes })는 컴포넌트가 데이터를 받은 뒤에야 아는 값을 위한 enumOf입니다.
    st.tool(name, { confirm, settle })
    • confirm은 호출을 승인 카드에 세웁니다. true면 항상, 인자를 받는 함수면 그럴 만한 호출에만 세웁니다.
    • remove*로 시작하는 이름은 MCP 힌트처럼 이름에서 파괴성을 읽어 기본으로 승인을 받습니다. 빼려면 { confirm: false }를 적습니다.
    • settle: false는 이미 있는 것을 읽는 호출이라는 뜻이라 DOM을 기다리지 않고 보고합니다. 기본값은 기다립니다. 쓰기가 아직 착지 중일 수 있기 때문입니다.
    st.tool(canRefund && "refundOrder")
    • falsy한 이름은 툴을 선언하되 공개하지 않습니다. callable은 사람의 클릭을 그대로 처리하고, 에이전트에게는 아무것도 가지 않습니다.
    • 모든 체인은 훅으로 끝나므로, 조건부 표면은 선언을 건너뛰지 않고 이름을 비웁니다.
    • 이름은 렌더를 따라갑니다. 나중에 나타난 컨트롤은 공개되고, 사라진 컨트롤은 공개를 멈춥니다.
    st.expose(name, Type).desc(…).value(v) · st.useState(name, Type).desc(…).init(v)
    • 파생 값과 로컬 상태이며, 각각 훅 하나로 끝납니다. .value()는 컴포넌트가 쥔 값을 받고(자식이 채우는 ref로 만든 값이면 thunk), .init()은 useState라 같은 쌍을 돌려줍니다.
    • 선언한 타입이 넘기는 값을 검사하고 읽히는 모양을 정합니다. 모델 클래스는 hidden, secret, visual 필드를 벗겨내고, Any는 그대로 통과합니다.
    • set: true가 없으면 읽기 전용이고, set: true는 같은 타입의 set<Name> 툴을 공개합니다. { report: false }는 초마다 바뀌는 키를 변경 보고에서 뺍니다.
    agentAttrs(handler, key)
    • 레퍼런스로 넘긴 핸들러의 data-akan-* 속성이고, 인라인 화살표에는 {}입니다. 클로저는 무엇을 하는지 말해 주지 않고, 추측한 표식은 없느니만 못합니다.
    • akanjs/ui의 모든 컨트롤이 이미 넣어 두므로, 앱은 직접 만든 컨트롤에만 적습니다.
    • key는 같은 이름의 컨트롤 중 어느 것인지를 호출 인자와 같은 말로 알려 줍니다. 없으면 포인터가 탭 메뉴들을 구별하지 못해 아무것도 그리지 않습니다.
    st.use.x({ agent: false })
    • 구독하되 표면에는 넣지 않습니다. 스토어 단위 스위치는 없습니다. 스토어 클래스는 에이전트에 대해 아무것도 말하지 않습니다.
    기본 툴 여섯 가지
    화면이 무엇을 선언하든 항상 실리는 툴입니다. 앞의 다섯은 builtins로 줄일 수 있고, askUser는 세션의 것이라 항상 남습니다.
    툴설명
    navigate
    Link와 같은 라우터로 내부 경로를 엽니다.
    goBack
    이 세션 히스토리의 이전 페이지로 돌아갑니다.
    readScreen(section?, images?)
    렌더된 화면을 압축한 텍스트로 읽습니다.
    readState(key)
    스토어 키 하나를 모델로 마스킹해 읽습니다.
    highlight(target)
    대상을 화면에 스크롤해 깜빡이며, 사용자에게 위치를 직접 보여 줍니다.
    askUser(question, choices?)
    질문 카드로 결정을 사용자에게 되돌립니다.
    • goBack은 navigate처럼 전역입니다. 히스토리는 페이지가 가진 컨트롤이 아니고, 뒤로가기 링크를 그리지 않은 페이지라고 떠날 수 없는 것은 아닙니다.
    • readScreen은 긴 화면도 닿게 합니다. 제목에는 앵커가 붙고, 잘린 읽기는 잘린 아래쪽 섹션 이름을 알려 줍니다. 그 이름이나 제목 텍스트를 section으로 넘기면 됩니다.
    • 이미지는 이름만, 주소는 선택입니다. alt가 없어도 이미지 자리는 남고, images: true를 주면 주소까지 붙습니다. 갤러리 하나가 썸네일마다 긴 URL이 되므로 기본값은 꺼짐입니다.
    • highlight의 대상은 여러 가지입니다. 툴 이름, 상태 키, 스코프 경로, 앵커, 제목 텍스트를 받습니다. 스크롤이 멈추면 깜빡이고, 숨겨진 것은 절대 잡히지 않습니다.
    • askUser는 답을 기다립니다. 사용자가 보기를 고르거나 직접 쓸 때까지 턴이 카드에서 멈춥니다. 건너뛰면 조용한 빈 답이 아니라 에이전트가 읽는 오류가 됩니다.
    에이전트가 읽을 수 있는 것
    • 스토어가 아니라 키 단위입니다. 같은 스토어의 다른 키가 살아 있어도, 화면이 읽지 않는 키는 읽히지 않습니다.
    • 모델로 마스킹됩니다. 모든 읽기는 키가 선언한 모델로 마스킹되므로 hidden, secret 필드는 넘어가지 않습니다.
    • 옵트인이 아니라 옵트아웃입니다. 구독한 키는 { agent: false }로 막지 않는 한 표면에 오릅니다. 라우팅, 호출자의 자격증명, UI operation 같은 base 스토어 배관은 막혀 있습니다. base 키를 읽히고 싶다면 ThemeToggle이 theme에 하듯 평범하게 읽으면 됩니다.
    레코드가 아니라 답을 돌려주기
    • 결과 하나는 20,000자까지입니다. 넘으면 JSON이 구조 중간에서 잘리고, 무슨 일이 있었는지 알려 주는 note가 붙습니다.
    • 결과는 이후 모든 턴에 실립니다. 압축도 구하지 못합니다. 압축은 자른 지점 위를 요약하는데 결과는 그 아래에 도착하기 때문입니다.
    • 덩치 큰 필드는 모델에서 한 번에 처리합니다. field.visual은 저장, 검색, 폼, 화면 렌더는 그대로 두고 모든 에이전트 읽기와 MCP 결과에서만 벗겨냅니다. 비밀이 아니라 비용의 문제라, 그 때문에 거절되는 것은 없습니다.
    턴이 도는 방식
    화면을 기다립니다
    router.push는 페이로드가 오는 중에 반환되므로, navigate와 읽기로 선언되지 않은 모든 툴은 DOM이 멈출 때까지 기다렸다 보고합니다. 읽기 빌트인은 선언돼 있어 둘러보기엔 비용이 없습니다.
    호출은 묶여서 갑니다
    모델이 한 턴에 만든 호출은 순서대로 실행되어 tool 메시지 하나로 돌아옵니다. 왕복 한 번입니다. 턴마다 하나씩 이으면 호출마다 왕복 한 번과 대화 전체 재전송이 듭니다.
    턴 상한은 묻습니다
    maxTurns에 닿으면 에이전트가 계속할지 묻습니다. 사용자가 대신 입력한 말은 그 사용자의 턴으로 들어갑니다.
    긴 작업과 중지
    • 폴링하지 말고 await하세요. 세션은 툴의 promise를 기다립니다. 작업을 끝내는 스토어 액션을 await하는 .exec은 턴을 그만큼 늘릴 뿐이고, 뒤따르는 변경 보고가 결과를 실어 옵니다. 두 번째 호출이 필요 없습니다.
    • 일찍 반환하면 비쌉니다. 에이전트가 한 번 볼 때마다 왕복하며 계속 되묻고, 분 단위 작업에서 maxTurns 예산을 몇 초 만에 태웁니다. desc에 그 사실을 적으세요.
    • 툴이 기다릴 수 없는 작업(이전 턴이나 사람의 클릭으로 시작된 작업)은 그 작업을 시작하는 컨트롤 옆에 기다리는 툴을 직접 선언합니다. 범용 대기 빌트인은 제거했습니다. 어떤 키가 무슨 뜻인지 모르니 그럴듯해 보이는 키에 아무렇게나 쓰였습니다.
    • Stop은 실행 중인 툴에도 닿습니다. 모든 호출은 abort 시그널과 경주하고, 툴은 그 시그널을 AgentAbort.current(AgentProgress와 같은 모듈 슬롯)에서 읽습니다. 존중하는 것은 선택이며, 얻는 것은 툴 자신의 정리입니다. 둘 다 use-agentic이 아니라 akanjs/store에서 가져옵니다.
    • 중지된 턴은 실행하지 못한 호출에 답을 채웁니다. 모든 프로바이더는 결과 없는 tool_calls가 있는 assistant 메시지를 그 턴과 이후 모든 턴에서 거절하므로, 그러지 않으면 더는 아무것도 보낼 수 없습니다.
    슬래시 커맨드
    채팅은 자체 커맨드 여섯 개에 답합니다. 앱이 작성하는 것은 없습니다.
    커맨드설명
    /new (/clear)
    새 대화를 시작합니다.
    /retry
    마지막 메시지를 다시 보냅니다.
    /compact
    지금까지의 대화를 요약합니다.
    /copy
    이 대화를 복사합니다.
    /help
    여기서 할 수 있는 것을 보여 줍니다.
    /tools
    이 화면의 툴 목록을 보여 줍니다.
    • 메뉴는 이 여섯 개가 전부입니다. 앱이 추가할 수 없습니다. 모델이 읽어야 할 화면은 page().prompt()로 공개하며, MCP 클라이언트에 prompt로 전달됩니다.
    • /new와 /copy는 턴 중에도 동작합니다. 질문 카드보다 앞서므로, /new는 답변 텍스트로 들어가지 않고 비우려는 턴을 끝냅니다.
    • 출력은 로컬에 남습니다. 커맨드의 출력은 대화창에 보이지만 전송되지 않습니다. 대화가 곧 모델의 히스토리라, 보내면 모델이 자기가 한 말로 받아들입니다.
    • /copy가 유일한 내보내기입니다. 릴레이는 아무것도 보관하지 않으므로, 잘못된 답이 고칠 수 있는 사람에게 닿는 길은 복사뿐입니다.
    • 키. ↑와 ↓로 보낸 메시지를 오갑니다. 대화 기록에서 채우므로 persist된 대화에서도 남습니다. / 메뉴가 열려 있으면 Enter는 줄을 고르고, Tab은 이름을 완성하며, Escape는 메뉴를 닫고 한 번 더 누르면 패널을 닫습니다.
    긴 대화는 스스로 요약합니다
    압축은 끝부분을 남깁니다
    기준을 넘으면 자른 지점 위의 메시지가 모두 요약 메시지 하나로 바뀌고, 마지막 몇 개는 그대로 남습니다. 자르는 지점은 언제나 user 메시지입니다.
    • 왜 필요한가. 루프는 브라우저에서 돌고 릴레이는 세션이 없으므로, 대화를 모델의 컨텍스트 창 안에 붙잡아 두는 것이 달리 없습니다. 압축하지 않으면 프로바이더가 요청 전체를 거절할 때까지 커집니다.
    • 언제. 매 턴 직전, 둘 중 먼저 닿는 쪽에서 압축합니다. 하나는 대화 추정 토큰이 compact.at을 넘을 때로, 턴마다 드는 비용의 상한입니다. 다른 하나는 프롬프트가 서버가 알려 준 컨텍스트 창에 가까워질 때입니다. 그러면 마지막 keep개 위의 히스토리가 요약 하나로 바뀝니다. 프로바이더는 너무 긴 요청에 짧은 답이 아니라 거절로 답합니다.
    • 컨텍스트 창. option.setLlm({ contextWindow })로 창 크기를 알려 줍니다. 가드는 그 아래로 답변 한도와 13k 버퍼를 비워 두고, 직전 턴에 프로바이더가 직접 센 토큰 수로 잽니다. 그래서 글자 수 추정이 작게 보는 한국어 대화도 놓치지 않습니다. 그래도 거절되면 압축한 뒤 같은 턴을 한 번 더 보내고, 거절 문구에서 창 크기를 알아 둡니다.
    • 어디서 자르나. 언제나 user 메시지에서 자릅니다. 그래서 남는 쪽이 호출은 요약돼 사라지고 결과만 남은 tool 메시지로 시작하지 않습니다.
    • 어떻게 요약하나. 요약 턴은 툴도 화면도 싣지 않고, 대화 자체가 아니라 길이가 제한된 요약본을 읽습니다. 대화는 이미 들어가지 않는다고 알려진 바로 그것이니까요.
    • 조절. Agent.Chat의 compact={{ at, keep, buffer }}로 정하고, { at: Infinity }이면 창 가드만 남기며, { at: 0 }이면 전부 끕니다. /compact로 언제든 남기는 것 없이 실행합니다.

    Zone 에이전트

    Agent.Zone은 화면의 한 구획에 자기만의 대화를 줍니다. 복잡한 페이지의 각 부분이 집중된 에이전트를 하나씩 가질 수 있습니다.
    zone 둘, root 에이전트 하나
    한 화면의 zone 두 개가 각자 자기 구획만 읽는 인라인 채팅을 갖고, root 에이전트의 채팅은 둘 다 계속 봅니다.
    two zones, two parallel conversations
    • 벽이 아니라 뷰입니다. 안에 마운트된 모든 것(구독, 훅 툴, 가이드)은 zone의 대화에 속하면서 root 에이전트에게도 그대로 보입니다.
    • 자기 컨테이너만 읽습니다. zone의 readScreen은 zone 경계에서 멈춥니다.
    • 안쪽 채팅은 알아서 묶입니다. 안에 마운트한 Agent.Chat은 자동으로 그 zone의 세션에 바인딩됩니다.
    • 가이드는 레이아웃처럼 내려옵니다. zone은 조상과 자신의 지침을 읽고, 형제 zone의 것은 읽지 않습니다.
    • root는 잃는 것이 없습니다. zone 밖의 root 채팅은 화면 전체를 계속 봅니다.

    읽지 않을 영역

    Agent.Skip은 푸터, 쿠키 배너, 반복되는 내비게이션 같은 영역을 기본 readScreen에서 뺍니다. 그런 영역도 본문과 똑같이 토큰을 쓰고, 읽은 결과가 대화에 남으므로 이후 모든 턴에서 다시 씁니다.
    a region marked, and what the read prints instead
    • 이름 붙은 표시가 남습니다. 통째로 지우면 없는 영역으로 읽혀, 푸터를 물으면 에이전트가 이 페이지엔 푸터가 없다고 답합니다. 표시의 이름이 곧 section이라, 이름을 넘기면 결국 읽을 수 있습니다.
    • 감추는 것은 텍스트이지 동작이 아닙니다. 툴과 상태 키는 마크업이 아니라 선언이라, 안에서 선언한 st.tool은 그대로 공개되고 highlight도 닿습니다. 한 층 위의 field.visual이고, 비밀이 아니라 비용의 문제입니다.
    • 두 번째로 꺼낼 도구입니다. Agent.Zone과 readScreen({ section })으로 컨테이너 하나만 읽는 편이 영역 다섯 개를 빼는 것보다 낫습니다. 푸터는 문서 맨 끝이라 긴 페이지에서는 이미 잘린 뒤에 있으니, 표시할 만한 영역은 본문 위쪽에 있는 것들입니다.

    작업을 보여주기

    채팅 패널은 열려 있는 만큼 닫혀 있으므로, 에이전트가 하는 일을 페이지가 직접 보여 줍니다. 호출이 나온 컨트롤에 링이 걸리고, 필요하면 화면으로 스크롤되며, 포인터가 그리로 가서 누릅니다.
    앱이 쓸 코드는 없습니다. onChange={st.do.setTitleOnTask}처럼 핸들러를 레퍼런스로 넘기는 것이 툴을 공개하고 컨트롤에 표식을 남기므로, 인라인 화살표 하나가 툴, 표식, 포인터 셋을 한꺼번에 조용히 잃게 합니다.
    아래에서 직접 해 보세요. 두 버튼 모두 st.tool의 callable을 onClick에 그대로 넘깁니다. 에이전트에게 세 번 올린 뒤 초기화해 달라고 하고 어디를 누르는지 보세요.
    0
    데모 카운트
    포인터는 한 턴 동안 머뭅니다
    • 호출이 아니라 턴마다 포인터 하나. 호출 사이에는 모델이 글을 쓰는 몇 초가 끼어 있어서, 호출마다 사는 포인터는 계속 사라졌다 나타났습니다.
    • 누르고, 비켜서고, 기다립니다. 처음 누르는 컨트롤에서 나타나 살짝 비켜선 자리에서 스피너로 기다리다가 턴이 끝나면 사라집니다. 버튼 위에 남은 스피너는 자기가 일으킨 변화를 가리기 때문입니다.
    • 컨트롤이 없으면 포인터도 없습니다. 질문에 답만 한 에이전트는 화면에 있었던 적이 없습니다.
    • 스크롤할 때는 방향을 보여 줍니다. 다음 컨트롤로 스크롤하는 동안 포인터는 제자리에서 화면이 가는 방향의 셰브론을 답니다. 그래야 화면에서 떨어져 나온 포인터가 아니라 스크롤하는 주체로 읽힙니다.
    그리지 않는 경우
    엉뚱한 요소에 걸린 링은 없느니만 못합니다. 방금 일어난 일에 대해 사용자에게 거짓을 말하기 때문입니다. 그래서 다음 경우에는 그리지 않습니다:
    • 같은 이름의 컨트롤이 여럿인데 호출 인자가 어느 것인지 말하지 않을 때. 탭 메뉴들은 툴 하나를 공유하므로 각 메뉴가 자기 키를 달고, 포인터는 실제로 전환된 메뉴를 고릅니다.
    • 승인이나 가드가 호출을 되돌렸을 때.
    • 컨트롤이 실제로 보이지 않을 때. 모달 뒤, 밀려난 서랍 안, 투명해진 것이 그렇습니다.
    • 탭이 백그라운드에 있을 때.
    연출 때문에 기다리는 것은 거의 없습니다. 이벤트를 넘겨받는 순간 호출이 시작되므로, 연출이 에이전트를 느리게 하지 않습니다.
    이동과 링크
    • 요소가 없으면 그리지 않습니다. 화면의 컨트롤에 닿지 않는 호출은 아무것도 그리지 않고, navigate도 대체로 그렇습니다. 페이지 상단에 걸었던 바는 에이전트의 동작이 아니라 페이지의 일부처럼 읽혔습니다.
    • 보이는 링크는 누릅니다. 목적지로 가는 보이는 링크가 정확히 하나면, 라우팅 전에 포인터가 그것을 누릅니다. 런타임이 기다려 주는 유일한 호출이며 상한은 600ms입니다. 라우터가 갈아치운 트리 위의 클릭은 클릭이 아니니까요.
    • 링크는 그릴지를 정할 뿐, 허용을 정하지 않습니다. 에이전트는 사용자가 주소창에 칠 수 있는 곳이면 어디든 갑니다.
    끄려면 visual={false}로 아무것도 그리지 않거나, visual={{ cursor: false }}로 링은 남기고 포인터만 뺍니다.
    apps/<app>/page/_layout.tsx

    모델 교체

    모델은 환경변수가 아니라 option.ts에서 설정합니다. setLlm은 LlmAdaptorRole을 차지한 어댑터에 apiKey, model, host, accepts, maxTokens, contextWindow를 채우므로, 프로바이더를 바꿔도 설정은 그대로입니다.
    어댑터는 와이어마다 하나씩, 두 개가 들어 있습니다:
    어댑터설명
    OpenaiLlm
    기본값입니다. host가 가리키는 곳에 chat-completions로 말합니다. OpenAI, DeepSeek, Groq, OpenRouter, Ollama.
    AnthropicLlm
    Messages API로 말하고, 사진뿐 아니라 PDF도 읽습니다.
    • model은 필수입니다. 기본값을 두면 언젠가 404가 되고, 이미지를 볼 수 있는지를 앱 대신 정해 버립니다.
    • accepts는 모델이 읽는 것을 덮어씁니다. 어댑터는 API 하나를 대변하고, 한 API가 서로 다른 모델을 섬기기 때문입니다.
    • 추가 필드도 함께 실립니다. setLlm은 건네받은 나머지 필드도 보관하므로, 직접 쓴 어댑터는 use<MyLlmOption>()로 자기 설정을 읽습니다.
    어댑터 직접 쓰기
    어댑터가 구현할 것은 chat(request, onDelta?) 하나입니다. 전체 대화가 들어가고 어시스턴트 응답 하나가 나옵니다. applyAdaptor로 끼우며, applyMiddleware처럼 마지막에 쓴 쪽이 이깁니다.
    apps/<app>/lib/option.ts
    akanjs/service — LlmAdaptor

    이 페이지

    인페이지 에이전트
    마운트와 보안
    선언하는 표면
    Zone 에이전트
    읽지 않을 영역
    작업을 보여주기
    모델 교체