Skip to content

KumikiAI の、AI による、AI のための Web フレームワーク

定義同士は組木のように噛み合うから、AI が並列にアプリを書き・直し・組み替えられる。

KumikiKumiki

同じUI、違う思想

同じアプリを 2 つの世界で。左は Kumiki での fetch example、右はその慣用的な React 等価実装。

Kumiki — 30 行 · 278 トークン

kumiki
type Quote = {text: Text, author: Text}
type Load  = Idle | Loading | Loaded(Quote) | Failed(Text)

slot state : Load = Idle

effect fetchQuote cap=http.get
                  in=Unit
                  out=Result(Quote, HttpError)
                  policy=latest
                  map-request={url: "/api/quote", decode: Decoder.Json(Quote)}

reducer load   on=ui.click(LoadBtn)        do= state := Loading
                                              emit fetchQuote()
reducer loaded on=fetchQuote.ok($q, _)     do= state := Loaded($q)
reducer failed on=fetchQuote.err($e, _)    do= state := Failed("request failed")

tile LoadBtn = button(text="Load quote", onClick=load)
tile App = column(
             LoadBtn,
             match state with
               | Idle        -> text("Click to load.")
               | Loading     -> spinner()
               | Loaded(q)   -> card(text(q.text), text("— " + q.author)) {pad: "md"}
               | Failed(msg) -> text(msg) {color: "danger"})
           {pad: "lg", gap: "md"}

app EffectHttp
    caps   = [http.get]
    routes = {"/" -> App, "/404" -> App}
    init   = []

React — 40 行 · 328 トークン

tsx
import { useRef, useState } from "react";

type Quote = { text: string; author: string };
type Load =
  | { tag: "idle" } | { tag: "loading" }
  | { tag: "loaded"; quote: Quote }
  | { tag: "failed"; message: string };

export function App() {
  const [state, setState] = useState<Load>({ tag: "idle" });
  const ctrl = useRef<AbortController | null>(null);

  async function load() {
    ctrl.current?.abort(); // "latest" policy を手書き
    const c = (ctrl.current = new AbortController());
    setState({ tag: "loading" });
    try {
      const res = await fetch("/api/quote", { signal: c.signal });
      setState({ tag: "loaded", quote: await res.json() });
    } catch {
      if (!c.signal.aborted)
        setState({ tag: "failed", message: "request failed" });
    }
  }

  return (
    <div>
      <button onClick={load}>Load quote</button>
      {state.tag === "idle" && <p>Click to load.</p>}
      {state.tag === "loading" && <Spinner />}
      {state.tag === "loaded" && (
        <blockquote>
          <p>{state.quote.text}</p>
          <footer>— {state.quote.author}</footer>
        </blockquote>
      )}
      {state.tag === "failed" && <p className="error">{state.message}</p>}
    </div>
  );
}

loading demo…

書き換えて試したくなったら Playground へ。

注目すべき点:

  • policy=latest はアノテーション 1 つ。 React で古いレスポンスを捨てるには AbortController の手書きの儀式が要る。まさに「テキストの外」にあって AI の編集で壊れる類の機構。
  • 副作用は隠さず宣言する。 caps = [http.get] はコンパイラが検査し、未宣言 capability の effect はコンパイルエラーになる。
  • すべての定義がフラットで独立。 AI は reducer failedtile App だけを差し替えられる。他は一切動かさなくてよい。

差はアプリの規模とともに開く。ベンチマークでは、Kumiki は React 等価実装よりトークン約 1.4× 減・行数約 2× 減