> ## Documentation Index
> Fetch the complete documentation index at: https://arkor-92aeef0e-eng-615.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Slack / Discord 通知

> 学習が完了 or 失敗したときに、trainer.ts の中から Webhook を叩く。

# Slack / Discord 通知

学習は長いので、ずっと Studio を見続ける人はいません。終端の `onCompleted` と `onFailed` コールバックは、チームが普段いる場所にステータスメッセージを流すのにうってつけです。

このレシピは Slack incoming Webhook を使います。Discord、Microsoft Teams、任意の HTTP エンドポイントも同じやり方で動きます。`fetch` できるものなら何でも通知先になります。

## パターン

```ts theme={null}
// src/arkor/trainer.ts
import { createTrainer } from "arkor";

const WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;

async function postSlack(payload: Record<string, unknown>): Promise<void> {
  if (!WEBHOOK_URL) return;
  try {
    const res = await fetch(WEBHOOK_URL, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(payload),
    });
    if (!res.ok) {
      console.warn(`slack webhook ${res.status} ${res.statusText}`);
    }
  } catch (err) {
    // 通知失敗をコールバックの外に逃がさない。
    console.warn("slack webhook failed:", err);
  }
}

export const trainer = createTrainer({
  name: "support-bot-v1",
  model: "unsloth/gemma-4-E4B-it",
  dataset: { type: "huggingface", name: "arkorlab/triage-demo" },
  lora: { r: 16, alpha: 16 },
  maxSteps: 100,
  callbacks: {
    onCompleted: async ({ job, artifacts }) => {
      await postSlack({
        text: `:white_check_mark: *${job.name}* finished (${artifacts.length} artifact${artifacts.length === 1 ? "" : "s"}). Job \`${job.id}\`.`,
      });
    },
    onFailed: async ({ job, error }) => {
      await postSlack({
        text: `:x: <!here> *${job.name}* failed: ${error}\nJob \`${job.id}\`.`,
      });
    },
  },
});
```

`<!here>` メンションは失敗時にしか発火しないので、成功時は誰も呼び出しません。チームの学習ジョブが実際にどれだけ失敗するかに合わせて緊急度を調整してください。

## なぜ内側の `try / catch` が大事か

Webhook リクエストが throw すると（Slack 障害、DNS の不調、コードが 200 番台以外を再 throw するなど）、コールバックは reject します。コールバックからの未捕捉の throw は、そのエラーで `trainer.wait()` を即座に reject させます（[SDK § ライフサイクルコールバック](/ja/sdk/callbacks)）。これは SSE 再接続ループには送られず、`maxReconnectAttempts` の下でリトライされることもありません。つまり `onCompleted` から発火した不安定な Webhook が `wait()` 全体を中断させてしまい、それを待っている CI や cron のオーケストレーターは、本来は成功した学習を失敗として扱ってしまいます。

Webhook は学習成功の判定基準ではなく副作用として扱ってください。内側で catch し、必要に応じてログに残します。

## バリエーション

**ステップごとの進捗 ping。** `onLog` と組み合わせて N ステップごとに 1 行投稿:

```ts theme={null}
onLog: async ({ step, loss }) => {
  if (step % 100 !== 0 || loss === null) return;
  await postSlack({ text: `step=${step} loss=${loss.toFixed(4)}` });
},
```

うるさいので、重要な学習だけにしたいなら `process.env.NOTIFY_PROGRESS === "1"` でゲートしてください。

**学習中のサンプル共有。** [学習中の評価レシピ](/ja/cookbook/mid-run-eval) と組み合わせる: 各チェックポイントのサンプルをレビューチャネルに投稿し、学習が続いている間にチームメンバーがリアクションで反応できます。

```ts theme={null}
onCheckpoint: async ({ step, infer }) => {
  try {
    const res = await infer({
      messages: [{ role: "user", content: "Can't log in" }],
      stream: false,
      maxTokens: 80,
    });
    const data = (await res.json()) as {
      choices: Array<{ message: { content?: string | null } }>;
    };
    const sample = data.choices[0]?.message.content ?? "(empty)";
    await postSlack({ text: `step=${step} → ${sample}` });
  } catch (err) {
    console.warn("checkpoint sample failed:", err);
  }
}
```

**他の宛先。** PostHog の `capture()`、Datadog のイベント、DB への insert: パターンは同じです。副作用をエラーを飲み込む `async` ヘルパーの内部に閉じ込め、ライフサイクルコールバックから呼び出します。トレーナーファイルに追加のオーケストレーションは不要です。

## 心に留めておくこと

* **内側の `try / catch` は必須。** 通知はあれば便利ですが、Webhook の障害が、本来は成功した学習の `wait()` を中断させるのは望ましくありません。
* **シークレットをトレーナーファイルに置かない。** 例では `SLACK_WEBHOOK_URL` を `process.env` から読み、Webhook が `git` に入らないようにしています。トークンベースの宛先全般に同じ考え方を適用してください。
* **`error` は `string` であることを忘れない。** `onFailed` の `error` 引数はバックエンドが送った文字列で（[SDK § ライフサイクルコールバック](/ja/sdk/callbacks)）、`Error` インスタンスではありません。そのまま埋め込んでください。`.message` を参照しないでください。
