コンテンツにスキップ

自律ゴールループ

チャットにゴールを投げ込むと、エージェントは計画し、作業し、完了するまで独立に審査されます——あるいはハードな上限に達し、人間が判断します。


ゴールループ(v1.37)は、一発のQ&Aを「ゴールを与える → エージェントが完了までループする → 行き詰まったら人間へエスカレーション」に変えます。接続済みの任意のチャネル(Telegram / Discord / Slack / LINE /…)から、次のように入力します:

/goal <goal description> || <acceptance criteria>
/goal 產出 Q3 月報 || 含每月營收圖表 || outcome:files:report.docx
/goal status

これはタスクボード上にgoal_modeタスクを作成し、送信元のチャネルとチャットが刻印されるため、進捗は開始した会話へプッシュバックされます。||部分を省略すると、ゴールの説明自体が受け入れの基準になります。オプションの第3セグメントoutcome:<spec>は、機械検証可能な出力コントラクト(JSON Schemaサブセットまたは作業ディレクトリのファイルglob)を追加します。これは判定者の前に、決定論的に、LLMコストゼロで実行されます——構造的に不足した出力は、判定呼び出しを消費せずにそのまま修正へ差し戻されます。

受け入れ基準は、ゴール作成の瞬間に不変のベースラインに凍結されます。エージェント身分のtasks_update呼び出しがgoal_modeタスクの受け入れ基準・タイトル・説明の変更を試みると拒否されます(監査レコード付き);ダッシュボードオペレーターのみが表示用のコピーをまだ編集できます。

ディスパッチエンジンはv1.59以降デフォルトで有効です(ゴールタスクが存在するまで定期的なSQLiteポーリングでアイドル)。オプトアウトするにはconfig.tomlで[dispatch] enabled = falseに設定してください。または「設定 → 自動化」の「dispatch engine」スイッチを切ってください(再起動不要)。

GoalLoopDriver(goal_loop.rs)が外側のループです。30秒ごと(tick_secsで設定可能)に実行待ちのgoal_modeタスクを見つけ、既存のメッセージキュー——チャネルメッセージが使うのと同じ起床レール——へ作業メッセージを投入します。そのため、エージェントは変更のない配管を通してクレームし、作業し、完了します。閉ループ:

driver enqueue ─▶ dispatcher ─▶ agent works ─▶ goal task → review
▲ │
└── reject → pending (+judge feedback) ◀── acceptance judge ──▶ pass → done

拒否されると、タスクは判定者のフィードバックを携えてpendingに戻ります。すぐ次のティックで、そのフィードバックを作業メッセージに含めて再ディスパッチされます——Generator-Verifierの再試行ループです。すべての状態遷移は、送信元の会話へ短い(1–3行)進捗メモをプッシュし、状態ごとに重複排除されます。

受け入れ判定 — 自己申告は決して信じない

Section titled “受け入れ判定 — 自己申告は決して信じない”

「完了」を宣言できるのは常にverifierであり、workerではありません。エージェントが完了を報告するとタスクはreviewに入ります。

最初に第1段階評価者が実行されます——ツールなし、1回のLLM呼び出しで3値のJSON判定を返します(continue / candidate_complete / blocked)。continueはパネルをスキップして即座に評価者のnext_stepをフィードバックとして再ディスパッチします(反復上限に計上);blockedは直接needs_humanへ;candidate_completeだけがパネルへ進みます。評価者の失敗(タイムアウト・解析エラー・呼び出しエラー)は全てパネルへ直接降級されます。設定:[dispatch] two_stage_judge(デフォルトtrue;falseで従来の毎回パネル動作に戻る)。

パネルに到達すると、DispatchEngineは3観点のMAVパネル——correctness、completeness、safety——をアカウントローテーター経由の1回のLLM呼び出しで実行します。3つすべての合格が必要です。パース失敗、JSON截断/畸形、または判定エラーはタスクをneeds_humanとして保留し、決して自動承認しません(フェイルクローズ)——截断されたJSON断片がレガシーの単一トークンスキャナーで誤ったpass部分文字列として誤読されることはもはやありません。これは、エージェントが成功を語りシステムがそれを信じるという古典的なループトラップへの防御です。

パネルのプロンプトには4つの立てた規律条項も含まれます(設定ゲートなし、常に有効):no ratcheting — 基準が変わらない場合、ラウンドごとにバーを上げない; audit, don’t author — 判定者はワーカーの提出証拠とツール活動ダイジェストのみ確認でき、自ら証拠を作成しない; no scope creep — 受け入れ基準に記載されていない要件は拒否理由にはならない; 自己申告“完了”は証拠ではない。

各受け入れ基準には、基準ごとの受け入れ台帳(C1、C2……)にステータスも付き、ワーカーが毎ラウンド報告し、ジャッジとタスク詳細ページから読めます。台帳の作られ方、モード([goal_loop] criteria_ledger、デフォルト report)、ジャッジの使い方は、ゴールループガイドの受け入れ台帳のセクションを参照してください。

判定の深さはゴールの難易度に応じてスケールします(ローカルのLLMゼロなヒューリスティック):単純な単一ステップのゴールは2観点チェック(correctness + safety)と低めの反復上限を、難しいゴールはフルパネルを受けます。safety観点は、どの深さでも決して省かれません。

ラウンド履歴と週次レポート(v1.67.0)

Section titled “ラウンド履歴と週次レポート(v1.67.0)”

各ディスパッチは評価器と判定者の判断、派工順、Solo/Team とゲート入力、状態と設定のスナップショット、保留理由を記録します。受理ラウンドには長さ制限付きの作業抜粋を保存します。手動リトライは新しい未確定の記録を追加し、確定済み判断と解除前の保留理由を残します。使用量はタスクとラウンドに紐付きます。

duduclaw weekly-report --days 30 --format json はリトライ上限と Solo/Team 別のゴール統計を含みます。各打ち切り点で失われる受理済みタスク数、節約できるラウンド数、そのラウンドまでに完了した受理済みタスクの割合と 95% Wilson 区間を示します。キャンセル済みと実行中は別集計です。過去の設定スナップショットは不明のまま、小標本は「運と区別できない」と明記します。この記述統計だけでリトライ上限変更の効果を証明することはできません。

duduclaw knobs survival --days 30 --format json は専用の読み取り専用履歴レポートです。記録されたリトライ上限、ディスパッチ時の難易度、Solo/Team、検証済みの手動リトライで分類します。過去のスナップショットが欠落または不一致の場合は不明のままです。人による成果承認は、認証済みの成果判断に基づく別集計です。通常の活動メッセージや開始許可は成果承認として扱いません。承認にラウンドが紐付いていない場合は件数のみ別計し、ラウンドを推測しません。

タスク台帳の非公開コピーを読み、取得前後のソースハッシュとファイル識別情報を照合します。タスク、コスト、監査データベースの移行や更新は行いません。空でない WAL または journal がある場合は読み取れず、通常のサービスで checkpoint を完了してから再実行します。履歴の欠落、破損、取得中の変更、集計上限の超過は明示的なエラーとなり、成功した空レポートには変換しません。既定の出力は stdout です。--output は指定したレポートのみを書き、データベースと sidecar の別名パスを拒否します。表は論理的な判定ラウンドを扱い、総ディスパッチ数やリトライ方針改善の実験ではありません。手動介入や設定不明の履歴にもこの制約が残ります。

上限超過時は切り詰めた部分集合ではなく、レポート全体を拒否します。上限はソース DB 64 MiB、選択期間内のタスク 5,000 件、iteration レコード 2,000 件、文字列フィールドごと 64 KiB、保持文字列合計 8 MiB、論理ラウンド 1,000、両集計の表合計 2,000 行です。取得、SQL、集計は共通の 2 秒期限を使います。Unix のファイル出力はディレクトリ descriptor で保護し、他のプラットフォームでは --output を明示的に拒否して stdout を案内します。DB と 3 種類の sidecar の予約ファイル名は出力名に使用できません。

ハードガード — 上限はドライバーが握る

Section titled “ハードガード — 上限はドライバーが握る”

終了はモデルを信頼することによってではなく、ドライバーによって保証されます:

ガード デフォルト 超過時
反復上限(タスクあたりのディスパッチ数) 5(難しいゴール)、3(単純) needs_human
作成からの実時間 24 h needs_human
同時ゴールタスク数 3 キュー待ち、ディスパッチされない
停滯検出 連続2回の拒否フィードバックで同じgapフィンガープリント(path:line引用とバッククォートキーワード正規化から抽出、逐語的比較ではなく——言い換えられた同一のgapでも一致;引用/キーワードが抽出不可能な場合のみ逐語比較にフォールバック) 早期needs_human
提前完了パターン検出 9つの zh+en アンカード正規表現がエージェントの最後の非空行にマッチ(例: 自署VERDICT:、「あとで確認」、「レビュー準備完了」) 遠測とアクティビティイベント + 次ラウンドの判定/評価入力に提示を折り込む——自身では拒否やブロックしない
実行中の重複排除 ディスパッチ済み・未クレームのタスクは、ストールタイムアウト(600 s)までは再投入されない 再ディスパッチ
派工失敗(作業メッセージがランタイムに届かない:CLI 不在、ローカルエンジン未起動、認証拒否) 次の tick で枠と edition lease を即時解放;バックオフ 60 → 120 → 240 秒;3 回連続で needs_human(infra)、エラー文をタスクのタイムラインへ
プロセス横断サーキットブレーカー(dispatch_guard) 20ディスパッチ / 60 sスライディングウィンドウ クールダウン拒否
委任ホップ深度 5 ディスパッチ拒否
ゲートウェイ再起動時の復活(resume_on_restart) pause(既定)——開機時に全ての in-flight goal_mode タスクをneeds_humanへ転換(理由: gateway_restart) auto の場合、再起動後もin-flightタスクはそのまま継続(この設定が導入される前の唯一の動作);config.toml またはダッシュボードの「設定→自動化」タブで切替可能(system.update_config は "auto"/"pause" のみ受理)
進捗未報告レポート(progress_report_minutes) クレーム済みタスクが10分間沈黙(判定基準はActivity Feedのシグナルで、lease renewerが定期的に更新するupdated_atではない) 「まだ実行中、進捗シグナルなし」の通知をActivity Feed+送信元の会話へ1ラウンドにつき最大1回プッシュ——再ディスパッチ・エスカレーション・キャンセルは一切しない;0で無効化
ツール連打アドバイザリ(tool_streak_advisory) 1ラウンド内で同一ツール・同一(マスク済み)引数を3/5/8回連続呼び出し 段階的に強まるzh-TWのヒントを次ラウンドの<state>ブロックへ注入——LLMコストゼロ、あくまでアドバイザリで呼び出しをブロックも拒否もしない;停滞検出のstate hashからは除外
エフェメラルspawnアドミッション([dispatch] admission) 同時実行上限(ephemeral_max_active、デフォルト32)を超過 "queue"(既定)——即座に拒否せず境界付きFIFOキューへ永続的に積む(キュー深度上限・チケットごとのTTL・すべて監査ログ);"fail"でH19以前の即時拒否に戻す

すべてconfig.tomlの[goal_loop] / [dispatch_guard]で設定でき、セクションがなければ組み込みのデフォルトが使われます。

タスクがneeds_humanとして保留されると、既存の自由記述judge_feedbackに加えて、6値のクローズドな分類pause_reason——no_progress/budget_exhausted/blocked_needs_decision/infra/restart/unknown——がトリガー箇所で静的にスタンプされます(判定者/評価者の文面から逆算することは決してありません)。/goalsボードとタスク詳細では分類チップとして、チャネルの承認メッセージでは「種類」の1行として表示されます;未分類または導入前の既存タスクは安全側の既定値unknownとして扱われます。goal_notify.rsは4つのアクション——retry / mark done / abort / take over——付きの承認メッセージを送信元の会話へプッシュします(なければエージェントの[proactive]コントロールチャネルにフォールバック)。1メッセージの主要アクションは3つまでという上限があるため、retry/mark doneを主要層に残し、abort/take overは各プラットフォーム自身のセカンダリ表現に折りたたまれます:TelegramとDiscordは2段目のボタン行、Slackはネイティブのoverflowメニュー。LINEには対応するセカンダリメニューの仕組みが一切ないため、この2つはクイックリプライから外され、代わりに本文中のプレーンテキストとダッシュボードへのリンクとして表示されます。他のボタン非対応チャネルはテキストフォールバック、ダッシュボードにはneeds_humanのボード列が表示されます。

take overはタスク認領(claimed_by)するだけで解決しません——タスクはneeds_humanのまま残るため、ドライバーのディスパッチ候補クエリがそもそもこの状態を見ないため、状態遷移なしでも自動リトライは既に止まっています。これがループ停止 + マーク + カード収束の第1層です。会話制御権全体の引き継ぎは42-human-takeover.mdの別機能です(日本語訳は未提供)。

決定は冪等でフェイルクローズです:retry/mark done/abortはneeds_humanからのみ遷移するため、古い押下や二重押下はno-opです。take overには比較すべき終端状態がないため、再押下(別の権限を持つ decider による押下でも)は単なる再認領として扱われます。

プランファーストモード(「想一想」、I-1c)

Section titled “プランファーストモード(「想一想」、I-1c)”

ダッシュボードのアサインパネルの3番目のモードで、問一問(ask)と交辦(assign)に並びます。選択するとtasks.goal_createがplan_first: true付きで送信されます:ディスパッチする代わりに、ゲートウェイはゴールと受け入れ基準からユーティリティLLMを同期呼び出しして短い(3〜8項目、プレーンテキストの)実行計画を作成し、タスクは直接needs_humanとして生まれます——既存のblocked_needs_decisionという保留理由のまま、新しい分類は追加されません——人間が承認するまでドライバーのディスパッチ候補クエリには一切現れません。

計画は専用のplan_pendingカラムに保持され、judge_feedbackとは意図的に分離されています:needs_humanタスクの再開に使う既存の「重試(retry)」アクションは、人間自身の承認メモでjudge_feedbackを上書きするため、同じカラムを使うと計画は読まれる前に消えてしまいます。承認は同じ既存のretryアクションであり、新しいボタン種別は追加されていません。承認後、最初のディスパッチラウンドで計画が<execution_plan>ブロックとして作業プロンプトに注入され、その後plan_pendingはクリアされるため、以降のラウンドで繰り返されません。計画はガイダンスであり免除ではありません——実行された作業は他のゴールタスクと同じ2段階/MAVの受け入れ判定を通過する必要があります。

プランナー呼び出し自体が失敗した場合(タイムアウト、通信エラー、空の返信)、タスクはやはりフェイルクローズでneeds_humanのまま保留されますが、保留理由はblocked_needs_decisionではなくinfraになり、plan_pendingは設定されません——これにより、承認が計画のないタスクを黙って開始してしまうことは決してありません。

各エージェントのリードの長さは、1つのダイヤルです:agent.toml [capabilities] autonomy_level。欠落またはパース不能な場合は、保守的なapproverがデフォルトになります。

レベル 挙動
operator ループは決して自動駆動しません。人間がプッシュするまでタスクは待機します。
collaborator / consultant 最初のディスパッチには人間のキックオフ承認が必要です(ApprovalBroker、1 h TTL、期限切れ=拒否)。その後は完了まで自律。
approver デフォルト。 キックオフゲートなし。人間に相談するのはneeds_humanのときだけです。
observer 完全自律。needs_humanは通知はしますが、待ちません。

ドライバとそのヘルパーは crates/duduclaw-gateway/src/goal_loop/ にまとまっています。

モジュール 内容
goal_loop.rs GoalLoopDriver 本体——外側のループそのもの。
goal_loop/signals.rs ラウンド出力を読む、LLMコストゼロの4つの抽出器:gapフィンガープリント(H4)、(state, action)訪問グラフ(A2)、1ラウンド内のツール連打アドバイザリ(H10)、早期打ち切り正規表現パネル(H5)。
goal_loop/state.rs 構造化<state>ブロック(A1)、needs_humanのクローズドな一時停止理由分類(H11)、予算枯渇時の「ベストラウンド」選択器(WP-4F)。
goal_loop/plan.rs ゴール分解と「想一想」プランファーストのプランナー。

2026-09以前は、これらはクレート直下の8つの独立モジュール(goal_state、goal_visit_graph、goal_gap_fingerprint、goal_budget_best_round、goal_bail_detect、goal_tool_streak、goal_plan、pause_reason)でした。2026-09の機能棚卸し(O8)で役割ごとに統合しています。従来のcrate::goal_*/crate::pause_reasonパスは1リリースのあいだre-exportとして残り、挙動・configキー・テストはいずれも変わっていません。

その上に、ツール呼び出しごとに重ねられます(approval.rs、Magentic-UIのActionGuardに倣ったもの):

  • irreversible_tools — 常に人間の承認を要求します。
  • maybe_irreversible_tools — LLM判定者がこの特定の呼び出しについて裁定します。リスクあり(または判定失敗/タイムアウト)は人間へエスカレーション、安全なら自動続行。フェイルクローズです。
  • 未記載 — 既存のallowed/denied/policyフロー。新たな摩擦はありません。

レガシーのapproval_required_toolsとのマージは厳しい方を採るため、既存の設定は正確な意味論を保ちます。

完全なオペレーターリファレンス——設定キー、ディスパッチポリシー、並列サブタスクDAG、outcomeスキーマ——はdocs/guides/goal-loop.mdにあります。