# AICC Runtime API

게시한 에이전트에 사용자 메시지와 최근 대화 상태를 전달하고 JSON 또는 SSE로 답변을 받습니다.

## 에이전트 대화

API Key와 Agent ID는 서버에만 보관하고 브라우저에서는 고객 서버 프록시를 호출하세요.

```http
POST https://console.humelo.com/api/v1/agents/chat
```

### 인증 및 헤더

```http
Authorization: Bearer {YOUR_API_KEY}
Content-Type: application/json
Accept: application/json # SSE는 text/event-stream
```

### 요청 필드

| 필드 | 타입 | 필수 여부 | 기본값 | 설명 |
| --- | --- | --- | --- | --- |
| `agent_id` | UUID | 필수 | - | 호출할 에이전트 ID입니다. 에이전트 빌더에서 확인할 수 있습니다. |
| `message` | string | 조건부 | - | 이번 턴의 사용자 입력입니다. 생략하거나 공백이면 history 또는 messages의 마지막 비어 있지 않은 user 메시지를 사용합니다. 대체할 user 메시지가 없으면 E4201/400; 최대 4,000자 |
| `session_id` | string | 선택 | - | 운영 로그에서 같은 상담을 묶을 고객 측 세션 ID입니다. 영문, 숫자, `.`, `_`, `:`, `-`만 사용; 1–160자 |
| `history` | Turn[] | 선택 | [] | 최근 대화입니다. 각 항목은 role과 content를 가지며 messages보다 우선합니다. 마지막 16턴 사용; role은 user 또는 assistant; content는 턴당 최대 4,000자 |
| `messages` | Turn[] | 선택 | [] | history와 같은 형식의 별칭입니다. history가 없을 때만 사용됩니다. 마지막 16턴 사용 |
| `state` | object \| null | 선택 | null | 이전 응답의 state를 그대로 전달하면 멀티턴 상태가 이어집니다. JSON 문자열 기준 최대 16,000자 |
| `stream` | boolean | 선택 | false | true면 Server-Sent Events로 응답합니다. Accept 헤더로도 활성화할 수 있습니다. |

### 요청 예시

```json
{
  "agent_id": "00000000-0000-0000-0000-000000000000",
  "message": "예약 취소 수수료가 어떻게 되나요?",
  "session_id": "customer-42:chat-7",
  "history": [],
  "state": null,
  "stream": false
}
```

### 응답

#### 200 · JSON 응답

| 필드 | 타입 | 필수 여부 | 기본값 | 설명 |
| --- | --- | --- | --- | --- |
| `agent_id` | UUID | 필수 | - | 응답한 에이전트 ID입니다. |
| `agent_name` | string | 필수 | - | 에이전트 이름입니다. |
| `response` | string | 필수 | - | 최종 답변입니다. |
| `matched_intent` | string \| null | 필수 | - | 매칭된 의도 이름입니다. |
| `handoff_required` | boolean | 필수 | - | 상담원 연결이 필요한지 나타냅니다. |
| `retrieved_context` | object[] | 필수 | - | 답변에 사용한 지식 항목입니다. |
| `actions` | object[] | 필수 | - | 실행한 에이전트 액션입니다. |
| `state` | object | 필수 | - | 다음 요청에 다시 보낼 멀티턴 상태입니다. |
| `end_call` | boolean | 필수 | - | 음성 채널에서 통화 종료가 필요한지 나타냅니다. |
| `runtime` | object | 필수 | - | 응답 채널 메타데이터입니다. |

```json
{
  "agent_id": "00000000-0000-0000-0000-000000000000",
  "agent_name": "라온투어 고객센터",
  "response": "예약 조건에 따라 취소 수수료가 달라질 수 있습니다.",
  "matched_intent": "패키지 취소료 기준",
  "handoff_required": false,
  "retrieved_context": [],
  "actions": [],
  "state": { "topic": "cancellation_fee", "handoff": false },
  "end_call": false,
  "runtime": { "mode": "text", "voice_engine": "DIVE" }
}
```

#### 200 · SSE 응답

stream이 true이거나 Accept가 text/event-stream이면 start, chunk, 선택적인 tool_call, done 순서로 전송됩니다. 실패 시 error 이벤트로 종료됩니다.

```http
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache, no-transform
X-Accel-Buffering: no
```

| 필드 | 타입 | 필수 여부 | 기본값 | 설명 |
| --- | --- | --- | --- | --- |
| `start` | event | 필수 | - | 의도, 핸드오프, 검색 지식, 초기 state입니다. |
| `chunk` | event | 필수 | - | 화면에 이어 붙일 delta 문자열입니다. |
| `tool_call` | event | 선택 | - | 도구 실행이 공개될 때의 이름과 인자입니다. |
| `done` | event | 필수 | - | 사용량 정산까지 성공한 뒤 보내는 최종 response, actions, state, end_call입니다. |
| `error` | event | 조건부 | - | 스트림 처리 실패 시 오류 메시지입니다. |

#### 400 · E4201 · 잘못된 요청

agent_id가 없거나, message와 history/messages의 user 입력이 모두 비어 있거나, 필드 형식이 잘못된 경우입니다. 예약·과금·provider 호출 전에 거절됩니다.

```json
{
  "error": "message or a user message in history is required",
  "code": "E4201"
}
```

#### 401 · E4001 / E4002 · 인증 오류

Bearer API Key가 없거나 유효하지 않습니다.

#### 402 · E4601 · 크레딧 부족

최대 예상 사용량을 예약할 크레딧이 부족합니다.

#### 404 · E4301 · 에이전트 없음

API Key 조직에서 agent_id를 찾을 수 없습니다.

#### 409 · E4302 · 게시 버전 없음

에이전트에 사용할 수 있는 게시 버전이 없습니다.

#### 413 · E4204 · 요청 크기 초과

요청 body 또는 state가 허용 크기를 초과했습니다.

#### 429 · E4403 · 호출 제한 초과

API Key 기준 분당 60회 제한을 초과했습니다.

#### 500 / 503 · E5001 / E5002 · 서버 또는 일시적 서비스 오류

E5002는 backoff 후 제한적으로 재시도할 수 있습니다. 실패한 요청의 최대 예약분은 정산 전에 환급되며, SSE는 정산 실패 시 done 대신 error 이벤트로 종료됩니다.

### 주의사항

- 요청 body는 최대 64,000 bytes이며 API Key별 호출 제한은 분당 60회입니다.
- message와 각 history content는 앞뒤 공백을 제거한 뒤 최대 길이까지만 사용합니다.
- message와 history/messages의 user 입력이 모두 비어 있으면 E4201/400이며 사용량 예약, provider 호출, 런타임 로그가 발생하지 않습니다.
- session_id가 허용 패턴과 맞지 않으면 요청은 처리되지만 세션 ID는 기록되지 않습니다.

## Next.js Server Proxy

Use this shape when you want a ChannelTalk-style widget on your website. The browser talks to your backend. Your backend calls Prosody.

```ts
// app/api/agent/chat/route.ts
export async function POST(request: Request) {
  const body = await request.json();

  const response = await fetch("https://console.humelo.com/api/v1/agents/chat", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.PROSODY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      agent_id: process.env.PROSODY_AGENT_ID,
      message: body.message,
      history: body.history ?? [],
      state: body.state ?? null,
      stream: false,
    }),
  });

  return new Response(await response.text(), {
    status: response.status,
    headers: { "Content-Type": response.headers.get("Content-Type") ?? "application/json" },
  });
}
```

## Browser Widget Pattern

A production widget usually needs four pieces:

1. A launcher button fixed to the bottom corner.
2. A chat panel that stores local draft messages.
3. A server-side proxy endpoint.
4. A small session store that preserves `history` and Prosody `state`.

Minimal client shape:

```ts
type Turn = { role: "user" | "assistant"; content: string };

let history: Turn[] = [];
let state: Record<string, unknown> | null = null;

async function sendToAgent(message: string) {
  const response = await fetch("/api/agent/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message, history, state }),
  });

  if (!response.ok) {
    throw new Error("Agent is temporarily unavailable");
  }

  const result = await response.json();
  history = [
    ...history,
    { role: "user", content: message },
    { role: "assistant", content: result.response },
  ].slice(-16);
  state = result.state ?? state;
  return result;
}
```

## Multi-Turn Rules

Prosody does not require you to resend the full conversation forever. Send a short recent history window and the returned `state`. The state is designed to preserve compact task context such as current topic, handoff flow, and useful runtime hints.

Recommended defaults:

- Keep the last 8 to 16 turns in `history`.
- Persist `state` per user conversation.
- Start a new state when the user intentionally starts a new support session.
- Do not include sensitive internal notes that should not be used by the agent.

## Handoff Handling

If `handoff_required` is `true`, show a human-support path. You can still display `response`; it is written to explain what information is needed before escalation.

```ts
if (result.handoff_required) {
  openHumanSupport({
    transcript: history,
    state: result.state,
    matchedIntent: result.matched_intent,
  });
}
```

## Security Checklist

- Keep `PROSODY_API_KEY` on the server only.
- Restrict your proxy with your own session, origin, or rate limit policy.
- Never put the API key in a script tag, public environment variable, or mobile bundle.
- Log Prosody request IDs or your own correlation IDs for support.
- Redact PII before storing transcripts if your policy requires it.

## Error Handling

| Status | Meaning | Recommended handling |
| --- | --- | --- |
| 400 | Missing or malformed request | Fix request shape before retrying. |
| 401 | Invalid API key | Rotate or reconfigure the key. |
| 402 | Insufficient credits | Add credits before retrying. |
| 404 | AICC not found or not owned by the API key organization | Check `agent_id` and organization ownership. |
| 409 | No published agent version | Publish a version before retrying. |
| 413 | Request body or state too large | Reduce the request size. |
| 429 | Per-key rate limit exceeded | Back off until the next rate-limit window. |
| 500 | Internal error | Show a fallback and do not blindly retry. |
| 503 | Runtime temporarily unavailable | Retry with bounded exponential backoff. |

AICC errors are flat, for example `{"error":"Too many requests","code":"E4403"}`. For SSE, a successfully settled turn ends with `done`; a runtime or settlement failure ends with an `error` event and no `done` event.

## LLM/Agent Context

Use the following files when asking ChatGPT, Claude, Codex, or another coding agent to integrate Prosody AICC:

- `/llms.txt`: documentation index.
- `/llms-full.txt`: all available documentation in one text file.
- `/docs-md/aicc`: this page as Markdown.

Prompt example:

```txt
Use the Prosody AICC documentation from /docs-md/aicc.
Implement a server-side proxy and a browser chat widget.
Do not expose PROSODY_API_KEY to the browser.
```
