Hikigai

@hikigai/app-sdk

Application SDK

Official Node.js SDK for invoking healthcare AI agents in your applications. Full streaming, WebSocket, and session support with type-safe responses.

v0.2.2Node.js 18+StreamingWebSocketLive · Rooms · SpeechCloud · Events · SONA

Installation

Choose your preferred package manager:

npm

npm install @hikigai/app-sdk

yarn

yarn add @hikigai/app-sdk

pnpm

pnpm add @hikigai/app-sdk

Quick Start

import { AppClient } from '@hikigai/app-sdk'

// Initialize client
const client = new AppClient({
  apiKey: process.env.HIKIGAI_API_KEY,
  projectId: process.env.HIKIGAI_PROJECT_ID,
})

// Get an agent
const agent = await client.agent('medical-coder')

// Basic invocation
const response = await agent.invoke('Patient presents with fever and cough...')
console.log(response.content)

// Streaming responses
for await (const chunk of agent.stream('Tell me about type-2 diabetes')) {
  process.stdout.write(chunk)
}

// Session-based conversations
const sessionAgent = agent.withSession('patient-123')
await sessionAgent.invoke('What is the treatment?')
await sessionAgent.invoke('Any contraindications?') // Remembers context

Core Concepts

AppClient

Main interface for agent invocation and management.

class AppClient {
  constructor(options?: {
    apiKey?: string          // API key (or HIKIGAI_API_KEY env var)
    projectId?: string       // Project ID (or HIKIGAI_PROJECT_ID env var)
    baseUrl?: string         // API endpoint
    timeout?: number         // Request timeout in milliseconds
    sonaUrl?: string         // Optional SONA service URL
    sonaApiKey?: string      // Optional SONA API key
  })

  readonly storage: StorageClient
  readonly identity: IdentityClient
  readonly sona: SONAClient
  readonly cloud: CloudClient
  readonly events: EventsClient

  agent(agentId: string): Promise<RuntimeAgent>
  listAgents(options?: {
    category?: string; tags?: string[]; search?: string
  }): Promise<RuntimeAgent[]>
  invoke(agentId: string, input: string | Record<string, unknown>,
    options?: InvokeOptions): Promise<InvokeResponse>
  stream(agentId: string, input: string | Record<string, unknown>,
    options?: StreamOptions): AsyncIterable<string>
  live(agentId: string, sessionId?: string, userId?: string,
    context?: Record<string, unknown>): StreamSession
  createSession(options?: {
    externalUserRef?: string; ttlSeconds?: number;
    appId?: string; metadata?: Record<string, unknown>
  }): Promise<SessionContext>
  session(sessionId: string, options?: { appId?: string }): SessionContext
  listSessions(options?: {
    userRef?: string; limit?: number; cursor?: string
  }): Promise<{ sessions: unknown[]; nextCursor?: string }>
  getAuthToken(): Promise<AuthTokenResponse>
  close(): void
}

RuntimeAgent

Represents a deployed agent ready for invocation.

interface RuntimeAgent {
  id: string
  name: string
  slug: string
  version?: string
  description?: string
  status: 'active' | 'pending' | string
  endpoint?: string

  invoke(input: string | Record<string, unknown>,
    options?: InvokeOptions): Promise<InvokeResponse>
  stream(input: string | Record<string, unknown>,
    options?: StreamOptions): AsyncIterable<string>
  withSession(sessionId: string): RuntimeAgent
  liveSession(sessionId?: string): StreamSession
}

// InvokeOptions / StreamOptions
// sessionId?, provider?, model?, timeout?, connectors?, pluginContext? (invoke only)

Invocation Methods

Multiple ways to interact with agents.

Basic Invocation

const response = await agent.invoke(
  'Clinical note to analyze',
  {
    sessionId: 'patient-123',
    timeout: 60000,
  }
)

console.log(response.content)           // Agent response
console.log(response.metadata.latencyMs) // Performance info

MCP Connectors

Pass linked MCP connector slugs in connectors. The platform injects apiKey and projectId from your AppClient credentials automatically.

import type { McpConnectors } from '@hikigai/app-sdk'

const connectors: McpConnectors = {
  'idexx-mcp-server': {},  // platform auth injected automatically
}

const response = await agent.invoke('Prepare patient chart', { connectors })

// Optional HTTP headers (Claude Desktop style)
const withHeaders: McpConnectors = {
  'idexx-mcp-server': {
    headers: {
      'X-API-Key': process.env.HIKIGAI_API_KEY!,
      'X-Project-Id': process.env.HIKIGAI_PROJECT_ID!,
    },
  },
}

MCP on Workflow Agents

After deploying a Sequential / Parallel / Loop agent with root connectors (Node AgentSDK → MCP on Sub-Agents), pass the same map on invoke/live. Tools attach to every LlmAgent leaf. Confirm with metadata.toolsCalled (from top-level gateway tools_called).

import type { McpConnectors } from '@hikigai/app-sdk'

const connectors: McpConnectors = {
  'idexx-mcp-server': {},
}

const agent = await client.agent('vitals-fetch-subagent')
const response = await agent.invoke(
  { site_id: 1, patient_id: 'PATIENT001' },
  { connectors, timeout: 120_000 },
)

console.log(response.content)                 // summary / { response: '...' }
console.log(response.metadata.toolsCalled)    // ['get_vitals_for_today']
console.log(response.metadata.latencyMs)

// Live — same connectors for leaf CallTool
// const session = await client.live('vitals-fetch-subagent', {
//   connectors: { 'idexx-mcp-server': {} },
// })

Agent Discovery

// Get specific agent
const agent = await client.agent('medical-coder')

// List all agents
const agents = await client.listAgents()

// Search agents
const filtered = await client.listAgents({
  category: 'medical-coding',
  tags: ['icd-10'],
  search: 'coding',
})

Streaming Responses

Process responses in real-time as they arrive.

// Stream text responses
for await (const chunk of agent.stream('Analyze this report...')) {
  process.stdout.write(chunk)
}

// Collect streamed data
let fullResponse = ''
for await (const chunk of agent.stream(input)) {
  fullResponse += chunk
}

console.log('Complete response:', fullResponse)

Session Management

Multi-turn conversations with persistent context via withSession / sessionId.

const sessionAgent = agent.withSession('patient-789')

// First turn - establish context
await sessionAgent.invoke('I have been experiencing chest pain')

// Second turn - knows about chest pain
await sessionAgent.invoke('Is this serious?')

// Third turn - maintains full conversation history
await sessionAgent.invoke('What tests should I get?')

Session Context Store

Server-side key/value context (SessionContext), separate from conversation memory on invoke.

const ctx = await client.createSession({
  externalUserRef: 'dr-smith',
  ttlSeconds: 3600,
})
await ctx.set('patient_id', 'MRN-12345')
console.log(await ctx.get('patient_id'))
console.log(await ctx.getAll())

const again = client.session(ctx.sessionId)
await again.update({ chief_complaint: 'chest pain' })
await again.touch(7200)

WebSocket Live Sessions

Deploy first with AgentSDK. For Sequential / Parallel / Loop live agents, set liveHandoff on leaves when deploying (see Node AgentSDK → Workflow Agents). To reuse a marketplace agent as a leaf, deploy with source: 'marketplace' + name (see reuse marketplace). AppSDK only opens the session.

Real-time bidirectional communication with agents via StreamSession. Pass connectors (same shape as invoke) to attach MCP tools on live.

const session = agent.liveSession(undefined, {
  connectors: { 'epic-ehr': {} },
})

await session.connect()
await session.start('text') // or 'audio'
await session.sendText('Look up the patient chart')
// await session.sendAudio(pcmBuffer, 'audio/pcm;rate=16000')

for await (const event of session) {
  console.log(event)
  if (event.type === 'turn_complete') break
}

await session.end()
await session.close()

Speech: Translate & Transcribe

Real-time speech-to-speech translation and speech-to-text transcription via client.speech. These are not agents — nothing to deploy, and no agent slug. Available to team organizations with GCP connected, billed to your own cloud project. Full protocol: API Reference → Speech.

Translate

import { AppClient } from '@hikigai/app-sdk'

const client = new AppClient({
  apiKey: process.env.HIKIGAI_API_KEY!,
  projectId: process.env.HIKIGAI_PROJECT_ID!,
})

const session = await client.speech.translate({
  targetLanguage: 'es',          // bare tags: es, ja, pt-BR
  echoTargetLanguage: true,      // default: echo when input is already Spanish
  inputTranscription: true,      // default
  outputTranscription: true,     // default
})

// Any source format — converted to 16 kHz mono PCM for you.
await session.sendAudio(muLawChunk, { encoding: 'mulaw', sampleRate: 8000 })

for await (const event of session) {
  if (event.type === 'audio') speaker.write(event.audio)         // 24 kHz PCM
  if (event.type === 'transcript') console.log(event.source, event.text, event.language_code)
}

Transcribe

const session = await client.speech.transcribe({
  languages: ['en-US', 'es-ES'],             // optional; omit to auto-detect
  customVocabulary: ['Hikigai', 'Kubernetes'],
})

await session.sendAudio(pcm16k)              // already 16 kHz mono PCM

for await (const event of session) {
  if (event.type === 'transcript') {
    console.log(event.text, event.language_code, event.final ? 'final' : 'interim')
  }
}

Code shapes differ: translate takes bare subtags (es), transcribe takes full locales (es-ES). Google caps transcription at 10 minutes; the gateway rotates the upstream transparently, so long sessions need no handling.

Changing language mid-conversation

Returns once the new language is live. A replacement model session is brought up before the old one is retired, so audio keeps flowing.

await session.setLanguage('fr')   // no reconnect, no new token

Audio helpers

import { toModelAudio, chunkAudio, pcmToWav } from '@hikigai/app-sdk'

const pcm = toModelAudio(raw, { encoding: 'mulaw', sampleRate: 8000 })
const pcm = toModelAudio(raw, { encoding: 'f32', sampleRate: 48000 })
const pcm = toModelAudio(raw, { sampleRate: 48000, channels: 2 })

for (const chunk of chunkAudio(pcm)) {
  await session.sendAudio(chunk)
}

fs.writeFileSync('out.wav', pcmToWav(translated))  // 24 kHz by default

Browser and mobile clients

Mint a short-lived token server-side so your API key never reaches the client.

const token = await client.speech.mintToken({
  endpoint: 'translate',
  config: { targetLanguage: 'ja' },
  locked: true,   // pin the config — client cannot call configure
})
// Hand token.speechToken to the browser; it connects directly.
Try it first: the Voice Lab tab in your project console runs both endpoints from the browser and generates this code from whatever config you settle on.

Rooms

Create and use multi-party realtime rooms with RoomSession. Call join(name) — first join creates the room. Then publish, listen for frames, and optionally use rtcJoin / rtcOffer / rtcAnswer for WebRTC signalling. Same surface as Python AgentSDK / AppSDK.

import { RoomSession } from '@hikigai/app-sdk'

const room = new RoomSession({
  baseUrl: 'https://backend.hikigaiplatform.io',
  apiKey: process.env.HIKIGAI_API_KEY!,
  projectId: process.env.HIKIGAI_PROJECT_ID!,
})

await room.connect()
await room.join('care-team-7')  // creates the room if new
await room.publish('care-team-7', 'note', { text: 'Labs ready' })

for await (const frame of room) {
  if (frame.type === 'message') console.log(frame.event, frame.data)
  if (frame.type === 'error') break
}

await room.close()

Even Realities G2 Glasses Integration

Complete guide to building apps for Even Realities G2 smart glasses powered by Hikigai agents. G2 apps run inside the Even phone app WebView and drive the glasses display (640×350 green monochrome) via Bluetooth with @evenrealities/even_hub_sdk. Keep Hikigai credentials on a server proxy — the page is public.

Architecture & Communication Flow

  1. Phone WebView: Loads your HTTPS page inside the Even mobile app.
  2. Agent Proxy: Receives questions from the WebView, holds credentials server-side, invokes via @hikigai/app-sdk.
  3. Formatting: normalizeForGlasses() + paginateForGlasses() strip Markdown and chunk answers (starter uses 420 chars; SDK default is 2000).
  4. Bluetooth Display: Even Hub SDK sends UI containers to the lenses and receives temple/mic events.

Starter template

Copy templates/even-g2 for a full Vite + Express + glasses UI starter. Useful scripts: npm run check (validate app.json), npm run qr / npm run pack (Even Hub CLI), npm run simulator when the Even Hub simulator is installed.

Environment variables

VariableRequiredNotes
HIKIGAI_API_KEYYesInjected automatically on Hikigai deploy; set locally in .env.
HIKIGAI_PROJECT_IDYesInjected on deploy; set locally in .env.
HIKIGAI_AGENT_IDYesAgent slug or id. Set under Environment in the console (not auto-injected).
HIKIGAI_PLATFORM_URLNoOptional baseUrl override; injected on deploy.
GLASSES_PAGE_CHARSNoChars per glasses page (starter default 420; SDK default is 2000).

Local / deploy wiring

  • Set Vite base: './' so assets resolve under a path prefix or subdomain.
  • In dev, proxy /api from Vite to Express (e.g. localhost:3000).
  • Call the proxy with relative URLs (./api/ask or new URL('api/ask', document.baseURI)) so the page works wherever Hikigai hosts it.
// vite.config.ts
export default defineConfig({
  base: './',
  server: { proxy: { '/api': 'http://localhost:3000' } },
})

// src/api.ts — relative so path-prefix and subdomain deploys both work
await fetch(new URL('api/ask', document.baseURI), {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ question, sessionId }),
})

app.json gotchas

  • package_id — reverse-DNS, unique on Even Hub (e.g. ai.hikigai.myapp).
  • name ≤ 20 chars, tagline ≤ 50, description ≤ 1024.
  • Typical permissions: network (reach your proxy) + g2-microphone (hold-to-talk). Drop mic if you only use list/text.
  • Validate with validateEvenG2Manifest / npm run check before deploy.

SDK helpers (pure, no network)

import {
  validateEvenG2Manifest,   // same app.json rules as the deploy pipeline
  paginateForGlasses,       // split agent answers into display-sized pages
  normalizeForGlasses,      // strip markdown decorations agents emit
  evenG2DevModeUrl,         // validate the live URL used in the dev-mode QR
  EVEN_G2_DISPLAY,          // { width: 640, height: 350 }
  EVEN_G2_MAX_TEXT_CHARS,   // 2000 (official text-heavy template cap)
  EVEN_G2_PERMISSIONS,      // network, g2-microphone, phone-microphone, ...
  EVEN_G2_LANGUAGES,        // en, de, fr, es, it, zh, ja, ko
} from '@hikigai/app-sdk'

const check = validateEvenG2Manifest(JSON.parse(fs.readFileSync('app.json', 'utf8')))
if (!check.valid) throw new Error(check.error)

const pages = paginateForGlasses(agentAnswer, { maxChars: 420 })
const qrPayload = evenG2DevModeUrl(liveUrl)

1. Backend Proxy (Express Example)

Create an API endpoint that receives queries from the Even App WebView and proxies them to your Hikigai agent. The starter template at templates/even-g2 ships this pattern.

import express from 'express'
import { AppClient, paginateForGlasses, normalizeForGlasses } from '@hikigai/app-sdk'

const app = express()
app.use(express.json())

const client = new AppClient({
  apiKey: process.env.HIKIGAI_API_KEY,
  projectId: process.env.HIKIGAI_PROJECT_ID,
})

const PAGE_CHARS = Number(process.env.GLASSES_PAGE_CHARS || 420)

app.post('/api/ask', async (req, res) => {
  try {
    const agent = await client.agent(process.env.HIKIGAI_AGENT_ID)
    const response = await agent.invoke(req.body.question, {
      sessionId: req.body.sessionId,
    })

    const cleanText = normalizeForGlasses(response.content)
    const pages = paginateForGlasses(cleanText, { maxChars: PAGE_CHARS })

    res.json({
      answer: response.content,
      pages,
      sessionId: response.sessionId,
    })
  } catch (err) {
    res.status(502).json({ error: err.message })
  }
})

2. Audio Input (Voice Queries)

Temple long-press records PCM on the glasses; wrap it as WAV (audio/wav) and send via attachments. The agent must allow audio in its input modalities — text-only agents reject voice. List/preset text questions work with any text agent.

app.post('/api/ask-audio', async (req, res) => {
  const { audio_base64, mime_type, sessionId } = req.body

  const agent = await client.agent(process.env.HIKIGAI_AGENT_ID)
  const response = await agent.invoke('Answer the question in this recording.', {
    sessionId,
    attachments: [{ data_base64: audio_base64, mime_type: mime_type || 'audio/wav' }],
  })

  const cleanText = normalizeForGlasses(response.content)
  res.json({
    pages: paginateForGlasses(cleanText, { maxChars: PAGE_CHARS }),
    sessionId: response.sessionId,
  })
})

3. Glasses interaction loop

Use @evenrealities/even_hub_sdk in the WebView. First paint uses createStartUpPageContainer; later screens use rebuildPageContainer.

import {
  AudioInputSource,
  OsEventTypeList,
  waitForEvenAppBridge,
  RebuildPageContainer,
  TextContainerProperty,
  CreateStartUpPageContainer,
} from '@evenrealities/even_hub_sdk'

const bridge = await waitForEvenAppBridge()
bridge.onEvenHubEvent(handleEvent)

// First draw → createStartUpPageContainer; later → rebuildPageContainer
await bridge.createStartUpPageContainer(/* menu with list of questions */)

async function handleEvent(event: EvenHubEvent) {
  // list click      → ask(selectedQuestion)
  // text click      → show next page from pages[]
  // text double-click → back to menu
  // temple long-press → audioControl(true, AudioInputSource.Glasses) + record PCM
  // temple release    → WAV → POST /api/ask-audio
  if (event.sysEvent?.eventType === OsEventTypeList.LONG_PRESS_EVENT) {
    await bridge.audioControl(true, AudioInputSource.Glasses)
  }
}

async function showAnswerPage(heading: string, pageText: string, pageIndex: number, totalPages: number) {
  const label = totalPages > 1 ? `${heading} (${pageIndex + 1}/${totalPages})` : heading
  await bridge.rebuildPageContainer(new RebuildPageContainer({
    containerTotalNum: 2,
    textObject: [
      new TextContainerProperty({
        xPosition: 0, yPosition: 0, width: 640, height: 40,
        containerID: 1, content: label, zOrderIndex: 1,
      }),
      new TextContainerProperty({
        xPosition: 0, yPosition: 44, width: 640, height: 306,
        containerID: 2, content: pageText, zOrderIndex: 2, isEventCapture: 1,
      }),
    ],
  }))
}

Deploy & console APIs

Detect even-g2 via app.json + @evenrealities/even_hub_sdk. The console shows a Dev-Mode QR and .ehpk download. Full HTTP reference: API Reference → Even G2.

Multi-Cloud

client.cloud — deployment catalog and BYOC credentials for apps, agents, and MCP servers (GCP / AWS).

const catalog = await client.cloud.catalog({ workload: 'app' })
for (const service of catalog.services) {
  console.log(service.id, service.displayName, service.regions[0]?.id)
}

const result = await client.cloud.validateTarget({
  workload: 'app',
  serviceId: 'gcp-cloud-run',
  region: 'europe-west1',
  config: { cpu: '2', memory: '1Gi' },
})

const creds = await client.cloud.listCredentials()

Events & Webhooks

client.events — HMAC-signed webhooks (at-least-once) and a live event stream.

const sub = await client.events.createWebhook({
  url: 'https://ci.example.com/hooks/hikigai',
  eventTypes: ['agent.deployed', 'agent.deleted'],
  description: 'deploy notifications',
})
console.log(sub.secret) // shown once — store it

const stream = client.events.stream(['job.*', 'agent.deployed'])
await stream.connect()
for await (const event of stream) {
  console.log(event.type, event.data)
}
await stream.close()

Storage

client.storage — platform object storage scoped to project + appId. Methods: upload, signedUrl, list, get, delete.

const uploaded = await client.storage.upload('app_123', fileBuffer, 'audio.wav', {
  contentType: 'audio/wav',
  metadata: { session: 's-9' },
  ttl: 86400,
})
console.log(uploaded.objectId, uploaded.signedUrl)

const { signedUrl, expiresAt } = await client.storage.signedUrl(
  'app_123',
  uploaded.objectId,
  3600,
)

const info = await client.storage.get('app_123', uploaded.objectId)
console.log(info.originalFilename, info.metadata)

const page = await client.storage.list('app_123', {
  contentType: 'audio/wav',
  status: 'active',
  limit: 20,
  page: 1,
})
for (const obj of page.objects) {
  console.log(obj.objectId, obj.sizeBytes)
}

await client.storage.delete('app_123', uploaded.objectId)

SONA Personalization

Access via client.sona (requires sonaUrl / sonaApiKey on AppClient). Pass pluginContext: { sona: { userId: '...' } } on invoke to get SONA metadata on response.plugins.sona.

const response = await agent.invoke('Draft discharge summary', {
  pluginContext: { sona: { userId: 'dr-smith-uuid' } },
})
const outputId = (response.plugins?.sona as { outputId?: string })?.outputId

await client.sona.approveOutput(outputId!, { userId: 'dr-smith-uuid' })
await client.sona.submitEdit(outputId!, 'Revised text...', { userId: 'dr-smith-uuid' })
const prefs = await client.sona.getPreferences('dr-smith-uuid')
const suggestions = await client.sona.getSuggestions('dr-smith-uuid')

Response Models

interface InvokeResponse {
  content: string
  agentId: string
  agentVersion?: string
  sessionId?: string
  status: string
  output?: Record<string, unknown>
  confidence?: ClinicalConfidence      // score: 0–1
  safetyFlags: SafetyFlag[]            // severity: low|medium|high|critical
  citations: ClinicalCitation[]
  metadata: InvocationMetadata
  message?: string
  plugins?: Record<string, unknown>    // e.g. plugins.sona
}

interface InvocationMetadata {
  invocationId: string
  latencyMs?: number
  timestamp: Date
  status: string
  tokensUsed?: number
  toolsCalled: string[]
  phiRedacted: boolean
  traceId?: string
}

interface SafetyFlag {
  category: string
  severity: 'low' | 'medium' | 'high' | 'critical'
  message: string
  mitigation?: string
}

End-User Identity

Add signup, login, MFA, QR badge login, and SSO for your app's end users. Each app is backed by a dedicated AWS Cognito pool the platform provisions for you; login returns standard OIDC tokens. Reach it all through client.identity.

IdentityClient

interface IdentityClient {
  // Accounts & auth
  signup(appId: string, email: string, password: string,
    opts?: { firstName?: string; lastName?: string; role?: string; attributes?: Record<string, string>; metadata?: Record<string, unknown> }): Promise<Record<string, unknown>>
  confirm(appId: string, email: string, code: string): Promise<Record<string, unknown>>
  login(appId: string, email: string, password: string): Promise<TokenResult>
  refresh(appId: string, refreshToken: string): Promise<TokenResult>
  logout(appId: string, email: string): Promise<Record<string, unknown>>
  forgotPassword(appId: string, email: string): Promise<Record<string, unknown>>
  resetPassword(appId: string, email: string, code: string, newPassword: string): Promise<Record<string, unknown>>
  changePassword(appId: string, accessToken: string, currentPassword: string, newPassword: string): Promise<Record<string, unknown>>
  createUser(opts: { appId: string; email: string; password: string; firstName: string; lastName: string; role?: string; metadata?: Record<string, unknown> }): Promise<Record<string, unknown>>
  getUser(appId: string, email: string): Promise<Record<string, unknown>>
  listUsers(appId: string, opts?: { limit?: number; offset?: number }): Promise<Record<string, unknown>[]>
  updateUser(appId: string, email: string, fields: Record<string, unknown>): Promise<Record<string, unknown>>
  listAppUsers(appId: string, opts?: { limit?: number; offset?: number }): Promise<Record<string, unknown>[]>
  deleteAppUser(appId: string, email: string): Promise<void>

  // Runtime verification (Cognito/GCP)
  verifyQr(appId: string, qrPayload: string): Promise<VerifiedUser>
  verifyPin(appId: string, email: string, pin: string): Promise<VerifiedUser>
  verifySso(appId: string, token: string): Promise<VerifiedUser>

  // MFA (TOTP)
  mfaAssociate(appId: string, accessToken: string): Promise<Record<string, unknown>>
  mfaVerify(appId: string, accessToken: string, code: string, opts?: { deviceName?: string }): Promise<Record<string, unknown>>
  mfaSetPreference(appId: string, accessToken: string, opts?: { enabled?: boolean }): Promise<Record<string, unknown>>
  mfaRespond(appId: string, email: string, session: string, code: string): Promise<TokenResult>

  // QR badge & SSO
  issueQr(appId: string, email: string): Promise<Record<string, unknown>>
  issueQrLogin(appId: string, email: string): Promise<Record<string, unknown>>
  issuePin(appId: string, email: string, pin: string): Promise<Record<string, unknown>>
  qrLogin(appId: string, qrPayload: string): Promise<TokenResult>
  configureSso(appId: string, opts: { providerName: string; providerType: string; /* ... */ }): Promise<Record<string, unknown>>
  ssoAuthorizeUrl(appId: string, redirectUri: string, opts?: { provider?: string }): Promise<Record<string, unknown>>
  ssoExchange(appId: string, code: string, redirectUri: string): Promise<TokenResult>

  // Setup (one-time, per app)
  enableIdentity(appId: string, opts?: { mode?: string; enabledMethods?: string[] }): Promise<Record<string, unknown>>
  getIdentityConfig(appId: string): Promise<Record<string, unknown>>
  byoSetup(): Promise<Record<string, unknown>>
  byoTest(roleArn: string, opts?: { region?: string }): Promise<Record<string, unknown>>
}

Sign up, confirm, log in

attributes are Cognito profile fields (string values — e.g. phone_number in E.164). metadata is opaque app JSON stored as Cognito custom:metadata. Cognito/GCP is the source of truth (no EndUser DB index for profile).

import { AppClient } from '@hikigai/app-sdk'

const client = new AppClient({
  apiKey: process.env.HIKIGAI_API_KEY,
  projectId: process.env.HIKIGAI_PROJECT_ID,
})

// 1. Register the end user
await client.identity.signup('app_123', 'clinician@hospital.com', 'Temp#Pass2026', {
  firstName: 'Asha',
  lastName: 'Mehta',
  role: 'clinician',
  // attributes → Cognito fields (strings only; e.g. phone_number in E.164)
  attributes: { phone_number: '+14155552671' },
  // metadata → Cognito custom:metadata JSON
  metadata: { doctorID: '6278383837' },
})

// 2. Confirm (skip if the pool auto-confirms)
await client.identity.confirm('app_123', 'clinician@hospital.com', '123456')

// 3. Log in -> OIDC tokens
const tokens = await client.identity.login('app_123', 'clinician@hospital.com', 'Temp#Pass2026')
console.log(tokens.accessToken)

Forgot password

await client.identity.forgotPassword('app_123', 'clinician@hospital.com')
await client.identity.resetPassword('app_123', 'clinician@hospital.com', '123456', 'NewTemp#Pass2026')

Change password (logged in)

const tokens = await client.identity.login('app_123', 'clinician@hospital.com', 'Temp#Pass2026')
await client.identity.changePassword(
  'app_123', tokens.accessToken!, 'Temp#Pass2026', 'NewTemp#Pass2026',
)

Update role / profile

await client.identity.updateUser('app_123', 'clinician@hospital.com', { role: 'doctor' })

Multi-factor login

const result = await client.identity.login('app_123', 'clinician@hospital.com', 'Temp#Pass2026')

if (result.status === 'challenge') {
  const code = await promptForTotpCode()
  const tokens = await client.identity.mfaRespond(
    'app_123', 'clinician@hospital.com', result.session!, code,
  )
  console.log(tokens.accessToken)
}

QR badge login

// Admin: issue a badge for the user (HKG2 payload shown once)
const badge = await client.identity.issueQrLogin('app_123', 'clinician@hospital.com')
console.log(badge.qr_payload)   // encode into the printed badge

// Kiosk: scan the badge, exchange it for tokens
const tokens = await client.identity.qrLogin('app_123', scannedPayload)

// Or verify without minting tokens
const user = await client.identity.verifyQr('app_123', scannedPayload)
console.log(user.firstName, user.role)

Complete Example

import { AppClient } from '@hikigai/app-sdk'

const client = new AppClient()

async function clinicalWorkflow() {
  const diagnosisAgent = await client.agent('diagnosis-assistant')
  const codingAgent = await client.agent('clinical-coder')

  const sessionId = 'patient-dr-smith-2026'

  // Initial assessment
  const assessment = await diagnosisAgent.invoke(
    'Fever 38.5°C, cough, shortness of breath for 3 days',
    { sessionId }
  )
  console.log('Assessment:', assessment.content)

  // Follow-up question - remembers context
  const followUp = await diagnosisAgent.invoke(
    'Any chest pain or sputum production?',
    { sessionId }
  )
  console.log('Follow-up:', followUp.content)

  // Extract medical codes
  const coding = await codingAgent.invoke(assessment.content)
  console.log('Codes:', coding.content)

  // Stream treatment plan
  for await (const chunk of diagnosisAgent.stream(
    'What is the recommended treatment?',
    { sessionId }
  )) {
    process.stdout.write(chunk)
  }
}

Error Handling

import {
  HikigaiError,
  AuthenticationError,
  RateLimitError,
  AgentNotFoundError,
  InvocationError,
} from '@hikigai/app-sdk'

try {
  const response = await agent.invoke('...')
} catch (error) {
  if (error instanceof AuthenticationError) {
    console.error('Invalid credentials')
  } else if (error instanceof RateLimitError) {
    console.error(`Rate limited. Retry after ${error.retryAfter}ms`)
  } else if (error instanceof AgentNotFoundError) {
    console.error('Agent not found')
  } else if (error instanceof InvocationError) {
    console.error(`Invocation failed: ${error.message}`)
  } else if (error instanceof HikigaiError) {
    console.error(`API error: ${error.message}`)
  }
}