---
title: DenoでAIエージェント作ってみたv2 toranoana.deno#25
tags:  #deno #typescript #javascript  
author: [とらのあなラボ](https://image.docswell.com/user/toranoana_lab)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/KE4WGN33J1.jpg?width=480
description: toranoana.deno#25の発表資料です。 https://yumenosora.connpass.com/event/392721/
published: June 05, 26
canonical: https://image.docswell.com/s/toranoana_lab/5R86NG-2026-06-05-140322
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/KE4WGN33J1.jpg)

Scratch AITuber Project
～ Denoでゼロから作る、テキストAIエージェントの今 ～
toranoana.deno#25 / 2026-06 (v2)
発表者: 藤原佳顕
「フレームワークに頼らず、決定論的なコードでLLMを御す」


# Page. 2

![Page Image](https://bcdn.docswell.com/page/L71YD51ZJG.jpg)

自己紹介
藤原 佳顕（ふじわら よしあき） — yoshiaki fujiwara / 8年目
担当: 新規事業（Fantia・Creatia）/ アーキテクトチーム（CSIRTも）/ AI推進チーム
大学: 情報系（数学より）
前職: 独立系ソフトウェア会社 — 主にGISとWeb、ライブラリ開発
言語/FW: TypeScript・Ruby on Rails・C#・C++・React・Vue・Angular
入社理由
自分がスキルアップできそうな場所に行きたい
オタク系の話ができるところに行きたい
好きなモノ: シューティング / 格闘ゲーム / アトラスのゲーム・SF小説・プログラミン
グ
2


# Page. 3

![Page Image](https://bcdn.docswell.com/page/G7WGY586E2.jpg)

本日の流れ
1. Why — なぜスクラッチで、なぜDenoなのか
2. How — アンチ・ブラックボックス と Plan-Act-Verify
3. Deno Inside — Permission / Deno.Command / Sandbox / Deno KV
4. Memory &amp; Persona — 3層メモリ / 自己検証
5. Now &amp; Next — 直近の進捗とこれから
ゴール: YouTube配信まで自走する AITuber「ひなた」。
今日はその 土台のエージェント基盤 と Denoの効きどころ の話。
3


# Page. 4

![Page Image](https://bcdn.docswell.com/page/4JZLXN8RE3.jpg)

なぜ自作するのか？
LangChain・LangGraph・Mastra…フレームワークは溢れている。
それでも スクラッチ を選んだ理由は4つ。
デバッグ可能性 — 抽象化が厚いと、本番で何が起きたか追えない
完全な制御権 — RAG・ローカルLLM・プロンプト形式まで自分で握る
学習価値 — 「12-Factor Agents」を体に染み込ませる
長期ビジョン — 配信する以上、挙動の予測可能性が必須
LLMは &quot;知的判断&quot; のコンポーネント。
制御フローは普通のTypeScriptで書く。
4


# Page. 5

![Page Image](https://bcdn.docswell.com/page/YE6W48P1EV.jpg)

なぜ Deno なのか？
「LLMが暴走しても安全」を、ランタイムが標準で 担保してくれる。
Permissionモデル — --allow-read/write/run/net を粒度で付与。デフォル
ト deny
TypeScriptがそのまま動く — ビルド設定ゼロ、 any 禁止と相性◎
Deno KV が標準同梱 — 状態も長期記憶も外部DBなしで永続化
jsr: と npm: を混在 — OpenAI SDKは npm: 、サンドボックスは jsr:
Web標準API — crypto.randomUUID() / TextDecoder / await using
deno test 同梱 — テストランナーもフォーマッタも追加依存なし
エージェントのセキュリティ境界を アプリ層 + Denoランタイム の二重で。
5


# Page. 6

![Page Image](https://bcdn.docswell.com/page/GE5MQ9KLE4.jpg)

設計の柱：アンチ・ブラックボックス
エージェントを「ブラックボックス」にしないための4原則。
1. 複雑なフレームワークの回避 — 制御フローは決定論的なコードで
2. プロンプトの所有 — プロンプトは第一級のソースコード
3. 透明性と予測可能性 — LLM出力は盲目実行せず、必ずアプリ層でバリデーション
4. 小さく特化した役割 — Planner / Executor / Verifier に分解
外側(アプリ)でループ・リトライ・形式チェックを握り、
内側(LLM)には 思考と判断だけ を委譲する。
6


# Page. 7

![Page Image](https://bcdn.docswell.com/page/9729P2W3JR.jpg)

制御ループ：Plan → Act → Verify
┌─────────┐
┌──────────┐
┌──────────┐
│ Planner │ → │ Executor │ → │ Verifier │
└─────────┘
└──────────┘
└──────────┘
立案者
実行者
検証者
▲
│
└────── 失敗時は再計画 ────┘
Planner: ユーザー意図をサブタスクに分解 ( planner.ts )
Executor: ツールを呼び実行、特権操作は承認を強制 ( executor.ts )
Verifier: 失敗 / ペルソナ逸脱を検知し 再計画・再生成 ( core.ts )
runAgent() が最大50イテレーションでこの3役を回す。
7


# Page. 8

![Page Image](https://bcdn.docswell.com/page/DJY45YL87M.jpg)

ツール群とセキュリティ
LLMが呼べるツールは OpenAI Function Calling 互換 で定義。
ツール
read_file
/ write_file
code_interpreter
exec_command
/ fetch_url
request_human_input / request_approval
web_search
役割
サンドボックス内のファイル読み書き（特権）
Deno Sandboxで安全にTS/JSを実行
ホワイトリスト制のシェル実行（特権）
DuckDuckGo検索とURL本文抽出（ Phase 2）
Human-in-the-loop / 事前承認
二重の境界: アプリ層の承認フロー ＋ Deno Permission モデル
特権ツールは実行前に LLM自身が request_approval を呼ぶ
8


# Page. 9

![Page Image](https://bcdn.docswell.com/page/V7NYNP49E8.jpg)

LLM呼び出しの一元管理（用途駆動）
すべてのLLM呼び出しを createChatCompletion 一本に集約。用途でパラメータを決
める。
// src/agent/config.ts（抜粋）
export type LLMUseCase = &quot;chat&quot; | &quot;planning&quot; | &quot;summary&quot; | &quot;verification&quot;;
export async function createChatCompletion(params: LLMRequestParams) {
let temperature = params.temperature;
if (temperature === undefined) {
// 計画・要約・検証は決定論寄り(0)、対話だけ creative(0.7)
temperature = (params.useCase === &quot;chat&quot;) ? 0.7 : 0;
}
return await _openai.chat.completions.create({ ...params, model: params.model ?? DEFAULT_MODEL, temperature, stream: false });
}
/ planner.ts / memory.ts の呼び出しを 全部この関数経由 に統一
モデル名 ( gemma4:e2b ) もハードコード排除 → 挙動の予測可能性が上がる
core.ts
9


# Page. 10

![Page Image](https://bcdn.docswell.com/page/YJ9PR3Q373.jpg)

Deno Inside ①：Permission モデル
権限は 起動時のフラグ で粒度ごとに渡す。付け忘れれば、そもそも動かない。
// deno.json — tasks
{
&quot;tasks&quot;: {
// 必要な権限だけを明示的に付与（デフォルトは全部 deny）
&quot;dev&quot;: &quot;deno run --allow-env --allow-net --allow-read \
--allow-run --allow-write --unstable-kv main.ts --no-planning&quot;,
&quot;test&quot;: &quot;deno test --allow-env --allow-net --allow-read \
--allow-write --allow-run --unstable-kv tests/&quot;
}
}
LLMがどんなコードを吐こうと、権限のない操作はランタイムが拒否
将来は --allow-read=./workspace のように パス単位 まで絞れる
「アプリ側のガードを抜けても、最後にOSの手前でDenoが止める」
10


# Page. 11

![Page Image](https://bcdn.docswell.com/page/GJ8DWMGLJD.jpg)

Deno Inside ②： Deno.Command × ホワイトリスト
exec_command
は 許可リスト と 標準API の二段構え。
// src/tools/shell_executor.ts（抜粋）
export async function exec_command(command: string, args: string[] = [], cwd?: string) {
// ① アプリ層：ホワイトリストにないコマンドは即拒否
if (!ALLOWED_COMMANDS.includes(command)) {
return `エラー: コマンド &#039;${command}&#039; は実行を許可されていません。`;
}
// ② 実行：シェルを介さず Deno.Command で直接プロセス起動（注入リスク低）
const cmd = new Deno.Command(command, {
args, cwd: cwd ?? Deno.cwd(), stdout: &quot;piped&quot;, stderr: &quot;piped&quot;,
});
const { code, stdout, stderr } = await cmd.output();
return `終了コード: ${code}\nSTDOUT:\n${new TextDecoder().decode(stdout)}`;
}
sh -c &quot;...&quot;
で文字列を渡さない＝シェルインジェクションの面を減らす。
11


# Page. 12

![Page Image](https://bcdn.docswell.com/page/LJLMN3GQER.jpg)

Deno Inside ③： @deno/sandbox で使い捨て実行
code_interpreter
は 隔離環境 でTSを実行し、終わったら自動破棄。
// src/tools/code_interpreter.ts（抜粋）
import { Sandbox } from &quot;@deno/sandbox&quot;; // jsr:@deno/sandbox
export async function code_interpreter(code: string): Promise&lt;string&gt; {
// await using → スコープを抜けると sandbox を自動 dispose（明示的リソース管理）
await using sandbox = await Sandbox.create();
await sandbox.fs.writeTextFile(&quot;main.ts&quot;, code);
await sandbox.sh`deno run main.ts &gt; output.txt 2&gt; error.txt`;
const stdout = await sandbox.fs.readTextFile(&quot;output.txt&quot;).catch(() =&gt; &quot;&quot;);
return stdout || &quot;出力はありませんでした。&quot;;
}
ホスト環境から隔離 → 生成コードが暴れてもホストに影響しない
await using は TC39 Explicit Resource Management（Denoが先行サポート）
12


# Page. 13

![Page Image](https://bcdn.docswell.com/page/47MYXDLK7W.jpg)

Deno Inside ④：Deno KV で状態を永続化
外部DBなし。 Deno.openKv() だけで セッションを止めて再開 できる。
// src/state/agent_state.ts（抜粋）
export async function saveState(state: AgentState): Promise&lt;void&gt; {
const kv = await Deno.openKv();
try {
// キーは配列。[&quot;agent_sessions&quot;, sessionId] で名前空間を切る
await kv.set([&quot;agent_sessions&quot;, state.sessionId], { ...state,
createdAt: state.createdAt.toISOString(),
updatedAt: state.updatedAt.toISOString(),
});
} finally {
kv.close();
}
}
1ターンごとに書き戻し → --session &lt;ID&gt; で計画・承認待ちごと復元
保存先は StateRepository 相当で抽象化（KV / RDB 差し替え可）
13


# Page. 14

![Page Image](https://bcdn.docswell.com/page/P7R9N4K6E9.jpg)

Deno Inside ⑤：同じKVが「長期記憶のベクトルストア」に
状態管理と まったく同じ Deno KV を、記憶の保存・検索に再利用。
// src/agent/memory.ts（抜粋）— KvMemoryRepository
async add(text: string, embedding: number[], metadata?: Record&lt;string, unknown&gt;) {
const id = crypto.randomUUID();
// Web標準のUUID生成
await this.kv.set([...this.prefix, id], { id, text, embedding, metadata });
}
async search(queryEmbedding: number[], limit: number) {
const all = await this.getAll();
// kv.list({ prefix }) で全件取得
return all
.map(e =&gt; ({ e, score: cosineSimilarity(queryEmbedding, e.embedding) }))
.sort((a, b) =&gt; b.score - a.score)
// コサイン類似度で並べ替え
.slice(0, limit).map(s =&gt; s.e);
}
Embeddingは Ollama の nomic-embed-text を OpenAI SDK 経由で生成
専用ベクトルDBを足さず、まず標準機能で動かす（後で差し替え可）
14


# Page. 15

![Page Image](https://bcdn.docswell.com/page/PJXQN2LD7X.jpg)

Deno Inside ⑥：類似度は「数式そのまま」を自前実装
ライブラリに頼らず、コサイン類似度 A・B / (|A|·|B|) を素のTSで。
// src/agent/memory.ts（抜粋）— 0.0〜1.0 を返す（1.0なら完全一致）
export function cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) throw new Error(&quot;Vectors must have the same length&quot;);
let dotProduct = 0; // 内積 (A・B)
let normA = 0;
// |A|^2
let normB = 0;
// |B|^2
for (let i = 0; i &lt; a.length; i++) {
dotProduct += a[i] * b[i]; // 各要素の積の合計
normA += a[i] * a[i];
// 各要素の二乗の合計
normB += b[i] * b[i];
}
const normProduct = Math.sqrt(normA) * Math.sqrt(normB); // |A|·|B|
if (normProduct === 0) return 0;
// ゼロベクトルは類似度0扱い
return dotProduct / normProduct;
// 内積をノルムの積で割る
}
一旦自分で作ることで何が起きているか、何が必要か把握しつつ実装
15


# Page. 16

![Page Image](https://bcdn.docswell.com/page/3JK9NMLDJD.jpg)

Deno Inside ⑦：セッション終了→「事実」を抽出して記憶
Phase 2.2: 会話そのものではなく、要約した 事実 をベクトル化して保存。
// src/agent/memory.ts（抜粋）— summarizeAndStore
const response = await createChatCompletion({
useCase: &quot;summary&quot;, // temperature=0 で決定論的に抽出
messages: [{ role: &quot;system&quot;, content: &quot;あなたは記憶整理のアシスタントです。&quot; },
{ role: &quot;user&quot;, content: prompt }], // 「好み・約束・出来事」を箇条書きで
});
const facts = response.choices[0].message.content.split(&quot;\n&quot;)
.map(l =&gt; l.replace(/^- /, &quot;&quot;).trim()).filter(Boolean);
for (const fact of facts) {
// 事実ごとに…
const embedding = await createEmbedding(fact); // nomic-embed-text でベクトル化
await repository.add(fact, embedding, { type: &quot;episodic&quot; }); // KV へ永続化
}
や ワンショット実行の 終了フックで自動起動（ main.ts の
handleExit ）
「〜と言った」ではなく 「〜が好き」へ変換 → 再利用しやすい知識に
/quit
16


# Page. 17

![Page Image](https://bcdn.docswell.com/page/LE3WVY3PE5.jpg)

3層メモリアーキテクチャ
記憶を役割で分離し、過去への過剰適合（Sycophancy）を防ぐ。
層
短期記
憶
中核記
憶
長期記
憶
役割
実装状況
直近の会話バッファ
js-tiktoken で計数 + スライディングウィンドウ
名前・感情値など絶対忘れない Core ProfileをSysPromptに注入
情報
終了時に要約→Embedding→KV保存 / 検索注入
過去セッションの要約
(recall)
短期: MAX_CONTEXT_TOKENS (4096) 等を定数管理し、古い履歴を自動切り詰め
長期: 書き込みは完成（前頁 summarizeAndStore ）。残るは会話中に search()
した記憶をプロンプトへ注入する配線（RAG recall）
17


# Page. 18

![Page Image](https://bcdn.docswell.com/page/8EDK85437G.jpg)

ペルソナ動的注入と自己検証
キャラ設定はハードコードせず、 persona.json を起動時に注入。
{
&quot;name&quot;: &quot;ひなた (Hinata)&quot;,
&quot;personality&quot;: &quot;明るく元気で好奇心旺盛。マスターと呼ぶ。&quot;,
&quot;rules&quot;: [&quot;広島出身で広島弁で喋る&quot;, &quot;明るく前向きなトーンで返答&quot;, ...]
}
出力は { thought, emotion, response } のJSON
自己検証 (Self-Correction): verifyPersona がキャラ一貫性をLLM自身でチェ
ックし、逸脱時は再生成
ユーザーの明示的な口調指定は尊重しつつ、キャラとして誠実に判断
18


# Page. 19

![Page Image](https://bcdn.docswell.com/page/V7PK8GMPJ8.jpg)

Now：この数週間で進んだこと
Web検索・情報収集 — web_search (DuckDuckGo) + fetch_url を実装
トークン管理の精緻化 — tool_calls含む計数の精度向上、定数管理化
長期記憶の書き込み — summarizeAndStore で終了時に事実を要約→KV保存
（テスト済み）
LLM呼び出しの抽象化 — 用途駆動の createChatCompletion に一元化、モデ
ル名も定数化
ペルソナ自己検証 — 機械的追従ではなく自律的に判断する振る舞いへ
初回起動バグ修正 — sessionId の取り回しを直し、初回でも記憶が保存され
るように
「基盤づくり (Phase 1)」から 「記憶と外部接続 (Phase 2)」 のフェーズへ。
19


# Page. 20

![Page Image](https://bcdn.docswell.com/page/2JVVN69VJQ.jpg)

Next &amp; まとめ
Next Steps（Phase 2 → 3）
[ ] 長期記憶の recall — 会話中に search() した記憶をプロンプトへ注入（RAGト
リガーの軽量判定）
[ ] Slack / Discord アダプター → SNSデビュー
[ ] マルチモーダル（TTS / 表情制御）→ AITuberデビュー
まとめ
今あるもの: Plan-Act-Verify / 3層メモリ / 承認フロー / 状態の中断再開 / Web検索 /
長期記憶の書き込み
これから: 長期記憶のrecall → SNS連携 → マルチモーダル → 配信デビュー
20


