SNA

One-shot jobs

completion, runOnce, runOnceStream 호출을 제품 기능에 쓰는 패턴입니다.

사용자 장기 chat thread에 들어가면 안 되는 작업에는 one-shot 호출을 쓰는 구조가 잘 맞습니다. 예: session title, query parsing, retrieval filtering, runtime health check, 작은 background decision.

가장 작은 surface 선택하기

작업 형태API이유
Text transform, classification, JSON extractionagent.completion()가장 빠른 경로입니다. 임시 session lifecycle이 없습니다.
Agent가 tool을 쓰거나 runtime별 behavior가 필요함agent.runOnce()임시 session을 만들고 완료를 기다린 뒤 정리합니다.
Browser/Electron UI가 one-shot 진행 상황을 live로 보여줘야 함agent.runOnceStream()HTTP SSE stream입니다. 장기 chat session은 만들지 않습니다.

One-shot prompt는 좁게 유지합니다. 이것은 제품 plumbing이지 open-ended conversation이 아닙니다.

completion으로 text-only utility 만들기

Title generation, intent classification, 작은 parser call에 유용한 패턴입니다.

import type { SnaClient } from "@sna-sdk/client";

export async function generateSessionTitle(client: SnaClient, firstMessage: string) {
  const result = await client.agent.completion({
    // Prompt를 한 가지 일만 하게 유지하면 실패 원인을 로그에서 찾기 쉽습니다.
    prompt: `Create a 2-5 word title for this first user message:\n\n${firstMessage}`,

    // 앱의 model registry가 고른 cheap/fast model을 사용합니다.
    model: "claude-haiku-4-5-20251001",

    // Label을 넣으면 trace와 log를 제품 기능별로 검색할 수 있습니다.
    label: "session-title",

    // One-shot UX는 빨리 실패하고 기본 title로 fallback하는 편이 낫습니다.
    timeout: 30_000,

    // 순수 text transform에는 tool permission을 열지 않습니다.
    permissionMode: "bypassPermissions",
  });

  if (!result.text?.trim()) return null;

  // Title은 UI string이므로 quote/newline/prose를 정리합니다.
  return result.text.trim().replace(/^["']|["']$/g, "").split("\n")[0].slice(0, 50);
}

Fallback이 있는 structured extraction

Model은 JSON을 fence로 감싸거나 malformed output을 낼 수 있습니다. Parse failure는 정상적인 branch로 다룹니다.

type ParsedQuery = {
  subject: string | null;
  predicate: string | null;
  object: string | null;
  mode: "exact" | "broad";
};

const FALLBACK_QUERY: ParsedQuery = {
  subject: null,
  predicate: null,
  object: null,
  mode: "broad",
};

export async function parseQuery(client: SnaClient, userText: string): Promise<ParsedQuery> {
  const result = await client.agent.completion({
    // Schema는 prompt에 넣되, log에서 검사할 수 있을 정도로 짧게 유지합니다.
    prompt: [
      "Return ONLY JSON matching this TypeScript type:",
      "{ subject: string|null; predicate: string|null; object: string|null; mode: 'exact'|'broad' }",
      "",
      userText,
    ].join("\n"),
    model: "claude-haiku-4-5-20251001",
    label: "query-parse",
    timeout: 30_000,
    permissionMode: "bypassPermissions",
  });

  if (!result.text) return FALLBACK_QUERY;

  // 흔한 fenced output은 허용하지만, 임의 prose를 조용히 통과시키지는 않습니다.
  const cleaned = result.text
    .replace(/^```(?:json)?\s*/i, "")
    .replace(/\s*```\s*$/i, "")
    .trim();

  try {
    const parsed = JSON.parse(cleaned);
    return {
      subject: typeof parsed.subject === "string" ? parsed.subject : null,
      predicate: typeof parsed.predicate === "string" ? parsed.predicate : null,
      object: typeof parsed.object === "string" ? parsed.object : null,
      mode: parsed.mode === "exact" ? "exact" : "broad",
    };
  } catch {
    // Malformed one-shot 결과는 앱을 crash시키지 말고 기능만 degrade합니다.
    return FALLBACK_QUERY;
  }
}

runOnce로 runtime health check 하기

실제 runtime path와 agent bootstrap을 실행해야 할 때는 runOnce가 맞습니다. 선택된 CLI path는 먼저 host settings나 SNA launcher environment에 반영해두고, runOnce는 설정된 runtime이 실제로 응답하는지 확인하는 역할로 두는 구조가 안전합니다.

export async function testRuntime(client: SnaClient, runtimeId: string) {
  const result = await client.agent.runOnce({
    provider: runtimeId,

    // Runtime cache나 project file이 dev mode를 오염시키지 않도록 cwd를 격리합니다.
    cwd: `/tmp/my-app-runtime-tests/${runtimeId}`,

    // Health check는 permission prompt를 띄우거나 tool을 호출하면 안 됩니다.
    permissionMode: "bypassPermissions",
    systemPrompt: "Reply exactly APP_RUNTIME_OK and do not use tools.",
    message: "Reply exactly: APP_RUNTIME_OK",

    // Runtime first-run setup은 느릴 수 있으므로 충분한 timeout을 둡니다.
    timeout: 120_000,
  });

  if (!result.result?.includes("APP_RUNTIME_OK")) {
    return { ok: false, reason: "unexpected-response", response: result.result };
  }
  return { ok: true };
}

Streaming one-shot UI

기존 session에 붙지 않는 작업이지만 UI가 live progress를 보여줘야 하면 runOnceStream이 맞습니다.

export async function previewPlan(client: SnaClient, request: string, append: (text: string) => void) {
  for await (const event of client.agent.runOnceStream({
    // 임시 one-shot run입니다. 종료 후 session은 유지되지 않습니다.
    message: request,
    provider: "codex",
    model: "gpt-5.4-mini",
    permissionMode: "bypassPermissions",
    timeout: 60_000,
  })) {
    if (event.type === "assistant_delta") {
      // 도착하는 token delta를 그대로 UI에 추가합니다.
      append(String(event.delta ?? ""));
    }
    if (event.type === "error") {
      throw new Error(String(event.error ?? "runOnceStream failed"));
    }
  }
}

Guardrails

  • 모든 call에 label을 넣으면 제품 trace를 읽기 쉬워집니다.
  • Timeout과 명시적인 fallback을 함께 둡니다.
  • Prompt가 tool을 쓰면 안 되는 작업일 때만 permissionMode: "bypassPermissions"를 쓰면 안전합니다.
  • User가 명시적으로 요청한 출력이 아니라면 one-shot 결과를 user conversation에 넣지 않는 쪽이 낫습니다.
  • 반복되는 background work는 runtime을 hardcode하지 않고 runtime/model registry를 통해 model을 고릅니다.

목차