このチュートリアルでは、DeepSeek Harness のツールを開発します。defineTool でパラメータスキーマ、execute、出力レンダリングを宣言し、ctx.tools.register で登録して、会話の中でモデルがあなたのコードを呼び出せるようにします。ツールは最もモデルに近いプラグイン機能です。完成すれば、モデルに新しいスキルが加わります。

ツールの構造図:defineTool がツールのフィールドを宣言し、ctx.tools.register が tools サービスに登録する。モデルが呼び出した後、output.render が結果をコンテンツブロックに変換するdefineTool({})name: 'greet'description: …parameters: { name: string }execute(args) {}output.schema: …output.render(value) {}フィールドは契約:モデルは description を読んで呼び出すタイミングを判断するexecute が正規の値を返し、render がブロックに変換する会話中のモデル呼び出すタイミングと渡す引数を判断するctx.tools.register で登録output.render が結果を変換
ツールの構造:defineTool がフィールドを宣言し、ctx.tools.register がツールを登録し、モデルが呼び出した後に output.render がコンテンツブロックを返す。

前提条件

始める前に、以下を確認してください:

  • はじめてのプラグイン開発を完了していること:リポジトリルートに scratch-plugin/ が存在し、cordis.ymlsrc/my-plugin.ts が登録済みであること;
  • pnpm dsh web --patch ./scratch-plugin/cordis.yml で Web UI が起動すること;
  • nameinjectapply の3つのエクスポートを理解していること。

ツールの仕組み

ツールを使うと、会話の中でモデルがあなたのコードを呼び出せるようになります。通常のプラグインは起動時に受動的に読み込まれてバックグラウンドで動作しますが、ツールは構造化された方法でモデルに機能を公開します。モデルが呼び出すタイミングと渡す引数を決め、結果を回答に折り込みます。

ツール呼び出しの流れ:ユーザーが質問すると、モデルは description からツールを選び、検証済みの引数で execute が実行され、render が結果を変換して、モデルが回答する1ユーザーが質問モデルがツールリストを読む2モデルが判断description からツールを選ぶ3execute が実行引数はスキーマで検証済み4render が変換値をコンテンツブロックへ5モデルが回答結果から答えを生成execute の戻り値がチャットに直接入ることはありません。モデルが何を見るかは output.render が決めます
1回のツール呼び出しの全行程:判断、実行、変換、回答。

ツール呼び出しは5つのステップで構成されます。モデルがツールの namedescription を読み、現在の質問に必要かどうかを判断し;必要なら parameters スキーマに従って引数を組み立て;フレームワークが検証して execute に渡し;execute の戻り値を output.render がコンテンツブロックに変換し;モデルが結果を読んで最終的な回答を書きます。

ツールプラグインを書く

scratch-plugin/src/my-plugin.ts を開き、内容を次のコードに置き換えてください:

scratch-plugin/src/my-plugin.ts(全置換)
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet someone by name.',
    parameters: {
      name: { type: 'string', required: true, description: 'The name to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))
}

最初のプラグインと比べて、骨格は変わりません。今でも nameinjectapply の3つをエクスポートします。変わったのは、applydefineTool で定義したツールを ctx.tools.register で登録するようになったことです。これがこのチュートリアルの主役です。

defineTool をフィールドごとに理解する

defineTool の各フィールドは、チェーンの1つの輪に対応しています。順に見ていきましょう:

name と description:モデルのために書かれたマニュアル

name はツールの呼び出し識別子です。モデルはツール呼び出しで名前を指定するので、greetsearch_docs のような小文字の動詞フレーズが最も分かりやすくなります。description はモデル向けの利用ガイドで、モデルはこの文章から「このツールをいつ使うべきか」を判断します。実装ではなく、シナリオを説明してください。

parameters:引数の契約

parameters は各引数を宣言します。typestringnumberboolean のいずれか;required は必須であることを示し;description はモデルにも表示されます。defineTool は実行時にこのスキーマに基づいて args を推論・検証するため、execute には型付けされた検証済みの引数が渡されます。

output と execute:実行と表現を分離する

execute はツールの実際のロジックで、output.schema が宣言する正規の値(ここでは string)を返します。output.render はその値をモデルが読めるコンテンツブロック(例: [{ type: "text", text: value }])に変換します。実行と表現を分けることで、execute は正しい結果の計算だけに集中でき、見せ方は独立して進化させられます。

なぜ tools を inject しなければならないのか

tools サービスは Harness の組み込みサービスです。安全に使うには、プラグインが依存関係を宣言する必要があります:

inject は必須
export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  // the tools service is ready — ctx.tools is guaranteed to exist
  ctx.tools.register(defineTool({ /* ... */ }))
}

再起動して確認

プラグインコードができたら、再起動して読み込みます:

ツールを読み込んだ状態で再起動
pnpm dsh web --patch ./scratch-plugin/cordis.yml
# open http://127.0.0.1:3080

チャットボックスに次のように入力してください:

このプロンプトを試す
Use the greet tool to say hi to Ada.

モデルは greet が適切だと判断し、name: 'Ada' で呼び出し、Hello, Ada! を受け取って、回答に自然に織り込みます。回答に挨拶が含まれていれば、ツールのチェーン全体が動作しています。

良い description を書く

ツールが使われるかどうかは、ひとえに description にかかっています。モデルは毎ターン、登録されているすべてのツールの namedescription を読み、それらを基に判断します。2つの書き方を比べてみましょう:

description の比較
// bad: implementation detail — the model cannot tell when to use it
description: 'calls the greet function and returns a string'

// good: usage guidance — the model knows when to reach for it
description: 'Greet someone by name when the user asks to say hello.'

各パラメータの description もモデルに表示されます。引数の説明が明確なほど、モデルが誤った値を渡す可能性は低くなります。モデルを新しいチームメイトだと思ってください。ツールが何をするのか、いつ使うのか、各引数をどう埋めるのかを説明すれば、正しく使ってくれます。

反復とデバッグ

ツール開発は高速な反復ループです:

反復ループ
# 1. edit scratch-plugin/src/my-plugin.ts
# 2. restart
pnpm dsh web --patch ./scratch-plugin/cordis.yml
# 3. verify at http://127.0.0.1:3080 with different phrasings

1つのプラグインで複数のツールを登録できます。ctx.tools.register は何度でも呼び出せます。1つのドメインの複数の機能をまとめて提供する自然な方法です。

ツール開発のよくある質問

モデルがツールを呼び出してくれません。どうすればよいですか?

まず description が利用シナリオを明確に説明しているか確認してください。モデルはそれを見て判断します。次に、ツール名が直感的か、各パラメータの description が具体的かを確認します。「greet ツールを使って Ada に挨拶して」と明示的にプロンプトを書いてツール自体が動くことを確認してから、徐々にヒントを減らしてモデルの自律的な選択をテストできます。

execute が例外を投げたらどうなりますか?

エラーは失敗したツール呼び出しとしてモデルに返されます。モデルは通常、再試行するか、別の引数を試すか、呼び出しが失敗したことをユーザーに伝えます。開発中は execute 内にログを仕込んで再起動すれば、問題をすばやく特定できます。

output.schema は何を宣言するのですか?

execute の戻り値の正規の型(この例では string)を宣言します。render はその値をモデルが読めるコンテンツブロックに変換します。スキーマはフレームワークとモデルの両方にとっての契約です。フレームワークは結果の形を知り、モデルは何を受け取るかを知ります。

1つのプラグインで複数のツールを登録できますか?

できます。apply の中で ctx.tools.register を複数回呼び出してください。defineTool 定義ごとに1回です。1つのドメインの複数のツールを単一のプラグインで提供するのはよくあるパターンです。

ツールのコードを編集したら再起動が必要ですか?

必要です。プラグインモジュールは起動時に1回だけ読み込まれます。編集のたびに pnpm dsh web --patch ./scratch-plugin/cordis.yml を再実行してください。開発中の patch は設定を提供するだけでグローバル profile には触れないため、再起動に副作用はありません。