bamboo-tools 2026.10.22

Tool execution and integrations for the Bamboo agent framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
//! Built-in tool guides for the model-visible tool surface.

use std::sync::Arc;

use serde_json::json;

use super::{ToolCategory, ToolExample, ToolGuide, ToolGuideSpec};

pub const BUILTIN_GUIDE_NAMES: [&str; 19] = [
    "exec_command",
    "write_stdin",
    "Edit",
    "ExitPlanMode",
    "GetFileInfo",
    "Glob",
    "Grep",
    "session_note",
    "Plan",
    "Read",
    "request_permissions",
    "Sleep",
    "Task",
    "update_goal",
    "loop_control",
    "ViewImage",
    "WebFetch",
    "Workspace",
    "Write",
];

pub fn builtin_tool_guide(tool_name: &str) -> Option<Arc<dyn ToolGuide>> {
    builtin_guide_spec(tool_name).map(|guide| Arc::new(guide) as Arc<dyn ToolGuide>)
}

pub fn builtin_guide_spec(tool_name: &str) -> Option<ToolGuideSpec> {
    match tool_name {
        // apply_patch is now an alias for Edit; return the Edit guide with
        // the alias name so existing callers still see relevant documentation.
        "apply_patch" => Some(guide(
            "apply_patch",
            ToolCategory::FileWriting,
            "Use for patch-only in-place updates using SEARCH/REPLACE blocks, aligned with Edit patch mode.",
            "Do not use before Read on existing files; do not use for full-file rewrites; do not mix with old_string/new_string style arguments.",
            &["Read", "Edit", "Write"],
            vec![example(
                "Apply a patch block",
                json!({"file_path":"/workspace/project/src/main.rs","patch":"<<<<<<< SEARCH\nlet v = 1;\n=======\nlet v = 2;\n>>>>>>> REPLACE"}),
                "Use when you want a dedicated patch tool call instead of Edit.",
            )],
        )),
        "Read" => Some(guide(
            "Read",
            ToolCategory::FileReading,
            "Use for reading file contents, especially before Edit/Write on existing files.",
            "Do not use for writing or cross-project search; do not substitute exec_command cat/head/tail for this.",
            &["Edit", "Write", "Grep"],
            vec![example(
                "Read a config file",
                json!({"file_path":"/workspace/project/config.toml"}),
                "Use before making any edits.",
            )],
        )),
        "Write" => Some(guide(
            "Write",
            ToolCategory::FileWriting,
            "Use for creating files or replacing full file contents.",
            "Do not use for small in-place updates; prefer Edit after Read.",
            &["Read", "Edit"],
            vec![example(
                "Write a full file",
                json!({"file_path":"/workspace/project/.env.example","content":"API_KEY=\n"}),
                "Suitable for full replacements.",
            )],
        )),
        "Edit" => Some(guide(
            "Edit",
            ToolCategory::FileWriting,
            "Use for precise in-place updates in existing files. Prefer patch mode with SEARCH/REPLACE blocks and enough context to target unique locations.",
            "Do not use before Read on existing files; do not use for full-file rewrites; do not use ambiguous SEARCH blocks that match multiple locations.",
            &["Read", "Write"],
            vec![
                example(
                    "Patch a specific block",
                    json!({"file_path":"/workspace/project/src/main.rs","patch":"<<<<<<< SEARCH\nfn b() {\n    let v = 1;\n}\n=======\nfn b() {\n    let v = 2;\n}\n>>>>>>> REPLACE"}),
                    "Preferred for repeated code because context disambiguates the target.",
                ),
                example(
                    "Legacy single replacement",
                    json!({"file_path":"/workspace/project/src/main.rs","old_string":"foo","new_string":"bar"}),
                    "Use only when old_string is known to match exactly once.",
                ),
                example(
                    "Legacy replacement with line hint",
                    json!({"file_path":"/workspace/project/src/main.rs","old_string":"value = 1","new_string":"value = 2","line_number":42}),
                    "Use line_number when old_string may appear in multiple places.",
                ),
            ],
        )),
        "Glob" => Some(guide(
            "Glob",
            ToolCategory::CodeSearch,
            "Find files by glob pattern before deeper content search.",
            "Do not use for content search; avoid unbounded root patterns like **/* without narrowing path/pattern.",
            &["Grep", "Read"],
            vec![example(
                "Find all Rust files",
                json!({"pattern":"**/*.rs","path":"/workspace/project"}),
                "Use before Grep when scope is unknown.",
            )],
        )),
        "Grep" => Some(guide(
            "Grep",
            ToolCategory::CodeSearch,
            "Search project contents using regex; start with files_with_matches then narrow with Read/content mode.",
            "Do not run broad content or multiline searches across the full workspace; always narrow with path/glob/type first.",
            &["Glob", "Read"],
            vec![example(
                "Find function usages",
                json!({"pattern":"execute_with_context","glob":"**/*.rs","output_mode":"files_with_matches","head_limit":200}),
                "First identify candidate files, then use Read or scoped content mode.",
            )],
        )),
        "exec_command" => Some(guide(
            "exec_command",
            ToolCategory::CommandExecution,
            "Run terminal commands (build/test/git/npm/docker/gh). yield_time_ms waits up to 250–30000 ms (default 10000). Fast commands return output and exit_code directly; longer commands return status=running and session_id. Continue work or pull incremental output and final status with write_stdin. Results normally remain pull-only; when the runtime explicitly hands a quiet command to background monitoring, its terminal result returns automatically and you can continue the conversation while it runs. Output is bounded by max_output_tokens (default 2000, maximum 8192); output_omitted_bytes reports discarded output.",
            "Do not use for file reads/edits/search when Read/Edit/Write/Glob/Grep can handle it, or shell echo/printf to communicate with the user. Expect automatic results only after an explicit runtime background handoff; do not use legacy BashOutput for an exec session. interactive=true supplies a stdin pipe, not a PTY; terminal-only programs may not work.",
            &["write_stdin"],
            vec![example(
                "Start tests with a short initial wait",
                json!({"cmd":"cargo test","yield_time_ms":1000,"max_output_tokens":2000}),
                "Inspect exit_code when completed; save a returned session_id and poll with write_stdin when running.",
            )],
        )),
        "write_stdin" => Some(guide(
            "write_stdin",
            ToolCategory::CommandExecution,
            "Pull output and completion from an exec_command session owned by this conversation. Use chars=\"\" to wait/poll; yield_time_ms is bounded to 250–30000 ms (default 10000). Running results retain session_id; final results omit it and include exit_code when available. Running polling metadata reports state, elapsed_ms, output_idle_ms, consecutive_empty_readouts and exit_observed. False exit_observed means no exit has been observed, not verified liveness; true still waits for terminal output capture. After repeated empty readouts and sustained silence, a polling advisory asks you to do useful work or inspect one command-owned progress signal before polling again. Continue waiting when appropriate, or report a concrete blocker, retaining the handle for final status. In a Human-controlled Root, repeated unchanged observations produce bounded runtime feedback to this model. Decide whether the last evidence supports intentional polling or a stalled approach; use a real progress signal or change method. When durable background resumption is available, repeated quiet polling transfers the exact command to background monitoring: the conversation remains usable and its observed terminal result is delivered automatically. Otherwise finite diagnostic recovery ends with an incomplete-progress notice without a new Human question or cancelling the command. Handles are local to this Host; unread terminal results keep their existing five-minute expiry after completion, including after a bounded diagnostic run ends. Output is incremental and bounded by max_output_tokens (default 2000, maximum 8192); output_omitted_bytes reports discarded bytes. For interactive=true sessions, nonempty chars writes exact bytes to the stdin pipe; include your own newline and use eof=true to close stdin. A lone Ctrl-C (\\u0003) stops the process.",
            "Do not use a conversation ID or legacy Bash ID as session_id. Do not blindly repeat identical empty polls, infer completion from silence or an existing report, or stop/restart a command only because it is quiet. Do not poll a finished session or wait for an automatic completion message. Input requires an interactive stdin pipe and does not provide PTY terminal behavior.",
            &["exec_command"],
            vec![
                example(
                    "Pull a command's next output and exit status",
                    json!({"session_id":"<session_id-from-exec_command>","chars":"","yield_time_ms":1000}),
                    "While status=running, use polling progress and follow any advisory before another empty poll. Preserve the handle until final status; a nonzero exit_code is a command outcome to inspect.",
                ),
                example(
                    "Answer an interactive prompt and close input",
                    json!({"session_id":"<session_id-from-exec_command>","chars":"yes\n","eof":true}),
                    "The command must have been started with interactive=true; bytes are sent exactly as supplied.",
                ),
            ],
        )),
        // Retained for explicit host-side legacy lookups, not default model guides.
        "Bash" => Some(guide(
            "Bash",
            ToolCategory::CommandExecution,
            "Run terminal commands (build/test/git/npm/docker/gh), optionally in background — a backgrounded command runs detached and notifies you with its result when it finishes, so you can keep working.",
            "Do not use for file reads/edits/search when Read/Edit/Write/Glob/Grep can handle it; do not use shell echo/printf to communicate with the user; do not sit polling BashOutput to wait for a background command — the completion notification comes to you.",
            &["BashOutput", "KillShell"],
            vec![example(
                "Run tests",
                json!({"command":"cargo test","timeout":120000}),
                "Use for build/test/CLI operations.",
            )],
        )),
        "BashInput" => Some(guide(
            "BashInput",
            ToolCategory::CommandExecution,
            "Send input to the stdin of an interactive background shell (spawned with Bash interactive=true) to answer a prompt, or close stdin (eof:true) to let a stdin-consumer finish.",
            "Do not use without a bash_id from an interactive Bash shell; the shell must have been spawned with interactive=true (piped stdin), otherwise this returns an error. The input is written as UTF-8 bytes; eof:true sends end-of-input so a consumer that reads until EOF (cat/sort/REPL) terminates instead of hanging.",
            &["Bash", "BashOutput"],
            vec![
                example(
                    "Answer a prompt",
                    json!({"bash_id":"<bash_id-from-Bash-interactive>","input":"y"}),
                    "Use when a background command is waiting for input (e.g. a confirmation prompt).",
                ),
                example(
                    "Send EOF so a consumer finishes",
                    json!({"bash_id":"<bash_id>","input":"data","eof":true}),
                    "Close stdin after writing so a command that reads until EOF terminates instead of running until killed.",
                ),
            ],
        )),
        "BashOutput" => Some(guide(
            "BashOutput",
            ToolCategory::CommandExecution,
            "Read the full/incremental output of a background shell on demand.",
            "Do not poll a background shell to wait for it — you are notified automatically when it completes (with its exit status and an output tail). Use BashOutput only when you want its output BEFORE that notification arrives; needs a bash_id from Bash.",
            &["Bash", "KillShell"],
            vec![example(
                "Inspect output early",
                json!({"bash_id":"abc"}),
                "Use to check on a still-running shell; you don't need it just to learn a shell finished.",
            )],
        )),
        "KillShell" => Some(guide(
            "KillShell",
            ToolCategory::CommandExecution,
            "Terminate a background shell.",
            "Do not use for foreground commands; use the ID returned by Bash(run_in_background=true), not chat session_id.",
            &["Bash", "BashOutput"],
            vec![example(
                "Stop runaway process",
                json!({"shell_id":"<bash_id-from-Bash>"}),
                "Use when process should no longer run.",
            )],
        )),
        "Task" => Some(guide(
            "Task",
            ToolCategory::TaskManagement,
            "Create or update the shared task list for the current root session tree (root + child sessions share the same task list).",
            "Do not use for trivial one-step requests.",
            &["Plan"],
            vec![example(
                "Update shared task statuses",
                json!({"tasks":[{"content":"Run tests","status":"in_progress","activeForm":"Running tests"}]}),
                "Keep exactly one item in_progress whenever possible.",
            )],
        )),
        "Plan" => Some(guide(
            "Plan",
            ToolCategory::TaskManagement,
            "Delegate a complex planning request to one runtime-enforced read-only planner child. The root remains the normal orchestrator and resumes automatically with the planner result.",
            "Do not use for a trivial lookup or as a substitute for implementation after the approach is already clear.",
            &["Task", "SubAgent"],
            vec![example(
                "Delegate an implementation plan",
                json!({"task":"Inspect the current authentication flow and return an ordered migration plan with affected files, tests, risks, and explicit non-goals.","title":"Plan authentication migration","fork_last_messages":2}),
                "The planner runs read-only; the parent waits internally and resumes without a mode-switch question.",
            )],
        )),
        "ExitPlanMode" => Some(guide(
            "ExitPlanMode",
            ToolCategory::UserInteraction,
            "Ask for confirmation before leaving plan mode.",
            "Do not use if implementation can proceed directly.",
            &["Task"],
            vec![example(
                "Plan complete",
                json!({"plan":"1. Do A\n2. Do B"}),
                "Use after producing a concrete implementation plan.",
            )],
        )),
        // FileExists guide moved next to GetFileInfo (see below).
        "ViewImage" => Some(guide(
            "ViewImage",
            ToolCategory::FileReading,
            "Load a local raster image for visual inspection. Vision-capable main models receive multimodal image content. When the main model has Vision disabled, Bamboo uses the explicitly configured Vision model and returns a labeled description as untrusted tool data. This fallback applies only to this tool. Direct conversation image uploads to a main model without Vision remain rejected, even when a Vision model is configured. Missing or unsupported Vision configuration is an actionable tool error. The main model is unchanged.",
            "Do not use for text files, directories, SVG/vector files, or images larger than 10 MB; use Read for textual content.",
            &["Read", "GetFileInfo"],
            vec![example(
                "Inspect a local screenshot",
                json!({"path":"/workspace/project/artifacts/screenshot.png"}),
                "Supports PNG, JPEG, GIF, and WebP content and detects the format from its bytes rather than trusting the extension.",
            )],
        )),
        "WebFetch" => Some(guide(
            "WebFetch",
            ToolCategory::CommandExecution,
            "Fetch a webpage by URL when you need cleaned page text from a known target.",
            "Do not use for broad discovery queries.",
            &[],
            vec![example(
                "Fetch a target page",
                json!({"url":"https://target-host/path","prompt":"Extract setup steps"}),
                "The prompt field is context for downstream handling; WebFetch itself returns cleaned text + metadata.",
            )],
        )),
        // GetCurrentDir is now an alias for Workspace (get mode)
        "GetCurrentDir" => Some(guide(
            "GetCurrentDir",
            ToolCategory::CommandExecution,
            "Retrieve the session's current workspace directory. (Alias for Workspace with no path argument.)",
            "Do not use when absolute paths are already known.",
            &["Workspace", "exec_command", "Read"],
            vec![example(
                "Inspect working directory",
                json!({}),
                "Useful before commands or file ops that rely on relative paths.",
            )],
        )),
        "GetFileInfo" => Some(guide(
            "GetFileInfo",
            ToolCategory::FileReading,
            "Check if a file/dir exists and read metadata (type, size, modified time) without loading content. Returns {exists: false} for missing paths, so this also covers existence checks.",
            "Do not use when you need actual content; use Read instead.",
            &["Read"],
            vec![example(
                "Check metadata before processing",
                json!({"path":"/workspace/project/logs/app.log"}),
                "Use to branch behavior based on file type/size.",
            )],
        )),
        // FileExists is now an alias for GetFileInfo
        "FileExists" => Some(guide(
            "FileExists",
            ToolCategory::FileReading,
            "Check quickly whether a path exists before reading, editing, or writing conditionally. (Alias for GetFileInfo)",
            "Do not use to inspect file content or metadata details.",
            &["GetFileInfo", "Read", "Write"],
            vec![example(
                "Guard before write",
                json!({"path":"/workspace/project/.env"}),
                "Use as a fast existence probe before deciding create vs update.",
            )],
        )),
        "session_note" | "memory_note" => Some(guide(
            if tool_name == "memory_note" {
                "memory_note"
            } else {
                "session_note"
            },
            ToolCategory::TaskManagement,
            "Store durable session-scoped notes and retrieve them across turns. Use it for local context, user preferences, constraints, and compression-resistant reminders within the current workstream.",
            "Do not store secrets/tokens, one-turn scratch text, or use it as the primary long-term knowledge base.",
            &["Task", "session_history_current"],
            vec![
                example(
                    "Persist a durable session constraint",
                    json!({"action":"append","content":"User prefers pnpm and strict TypeScript."}),
                    "Use append for stable session/workstream facts; use replace to compress long notes.",
                ),
                example(
                    "Store notes for a specific topic",
                    json!({"action":"append","topic":"backend-api","content":"REST endpoints finalized: /users, /orders."}),
                    "Use topic to keep separate workstreams isolated from each other.",
                ),
            ],
        )),
        "memory" => Some(guide(
            "memory",
            ToolCategory::TaskManagement,
            "Use Session memory for current-session continuity, Project memory for durable project facts and decisions, and Global memory for cross-project user context. Recall with a short lexical query, use Jiandu's compact default top three IDs and summaries, then get only the selected item that needs full context. Query before writing; store one confirmed atomic fact with a few useful keywords, entities, and tags.",
            "Do not store secrets, unverified claims, live state that you can inspect directly, embeddings, or unrelated facts in one memory. Treat canonical Project memory as trusted durable project authority but verify live state before acting; treat Dream as a low-trust derived orientation snapshot. Do not increase the query limit or get every hit by default, and merge or consolidate only the same confirmed fact.",
            &["session_note", "session_history_current", "Task"],
            vec![
                example(
                    "Read the current session note topic",
                    json!({"action":"session_read","topic":"default","options":{"max_chars":4000}}),
                    "Use for continuity state inside the current session without touching durable project/global memory.",
                ),
                example(
                    "Shortlist durable memories before reading full content",
                    json!({"action":"query","scope":"project","query":"mobile release freeze"}),
                    "Use Jiandu's compact default top three ID/summary results, then select only the item that needs full context.",
                ),
                example(
                    "Get the selected durable memory",
                    json!({"action":"get","id":"mem_20260403_001","options":{"max_chars":5000}}),
                    "Use after query for the one selected ID whose full body or frontmatter is needed.",
                ),
                example(
                    "Recall before answering about the user's own context",
                    json!({"action":"query","scope":"global","query":"preferred testing framework"}),
                    "When the user refers to their own preferences, opinions, or past decisions you don't already know — especially first-person questions ('what do I...', 'did I...', '我...?') — query memory BEFORE answering; do not say you don't know without checking.",
                ),
                example(
                    "Recall a past project decision before acting",
                    json!({"action":"query","scope":"project","query":"HTTP Tauri IPC decision"}),
                    "Treat a matching canonical Project memory as trusted durable project authority, then verify any live implementation state before acting.",
                ),
                example(
                    "Query before writing a durable fact",
                    json!({"action":"query","scope":"project","query":"mobile release freeze Tuesday"}),
                    "Check whether the same fact already exists before write; merge only when the selected existing item is the same confirmed fact.",
                ),
                example(
                    "Write a durable project memory",
                    json!({"action":"write","scope":"project","type":"project","title":"Mobile release freeze is Tuesday","content":"Mobile release freeze begins Tuesday.","keywords":["mobile","release freeze","Tuesday"],"entities":["Mobile"],"tags":["release"]}),
                    "Write one confirmed atomic fact with a specific title and only a few lexical retrieval hints.",
                ),
                example(
                    "Merge follow-up details into an existing durable memory",
                    json!({"action":"merge","id":"mem_20260403_001","content":"The release manager confirmed the Tuesday freeze.","keywords":["release freeze","Tuesday"],"entities":["release manager"],"tags":["confirmed"],"source_memory_ids":["mem_20260403_002"]}),
                    "Use ONLY when the new evidence is the same fact as the target memory and older overlapping items should be superseded. Do not merge unrelated facts together — create a separate memory instead.",
                ),
                example(
                    "Split a multi-topic memory into atomic memories",
                    json!({"action":"split","id":"mem_20260403_001","pieces":[{"title":"User prefers pnpm","type":"user","content":"User prefers pnpm.","keywords":["pnpm","package manager"],"entities":["pnpm"],"tags":["preference"]},{"title":"Mobile release freeze is Tuesday","type":"project","content":"Mobile release freeze begins Tuesday.","keywords":["release freeze","Tuesday"],"entities":["Mobile"],"tags":["release"]}]}),
                    "Use when one memory has accreted several unrelated facts (a 'blob'): split archives the original and creates one atomic memory per fact, preserving lineage via supersedes.",
                ),
                example(
                    "Consolidate near-duplicate memories into one",
                    json!({"action":"consolidate","ids":["mem_20260403_001","mem_20260403_007"],"type":"project","title":"Mobile release freeze is Tuesday","content":"Mobile release freeze begins Tuesday.","keywords":["release freeze","Tuesday"],"entities":["Mobile"],"tags":["release"]}),
                    "Use ONLY when two or more memories are the SAME fact: consolidate archives them all and creates one canonical atomic memory, preserving lineage via supersedes. Confirm sameness (e.g. via scan_duplicates / find_duplicates) before consolidating — never merge distinct facts.",
                ),
            ],
        )),

        // SetWorkspace is now an alias for Workspace (set mode)
        "SetWorkspace" => Some(guide(
            "SetWorkspace",
            ToolCategory::CommandExecution,
            "Change the current session workspace. (Alias for Workspace with path argument.)",
            "Do not use with non-directory or missing paths.",
            &["Workspace", "exec_command", "Read"],
            vec![example(
                "Switch session workspace",
                json!({"path":"/workspace/project"}),
                "Use before running commands or edits in another repo root.",
            )],
        )),
        // Workspace is the unified tool replacing GetCurrentDir + SetWorkspace
        "Workspace" => Some(guide(
            "Workspace",
            ToolCategory::CommandExecution,
            "Get or set the current session workspace directory. Call without 'path' to read the current workspace; call with 'path' to change it.",
            "Do not set workspace to a non-directory or missing path.",
            &["exec_command", "Read"],
            vec![
                example(
                    "Get current workspace",
                    json!({}),
                    "Useful before commands or file ops that rely on relative paths.",
                ),
                example(
                    "Set workspace",
                    json!({"path":"/workspace/project"}),
                    "Use before running commands or edits in another repo root.",
                ),
            ],
        )),
        "request_permissions" => Some(guide(
            "request_permissions",
            ToolCategory::UserInteraction,
            "Request one or more remembered permission scopes through independent typed decisions. Batch entries are reviewed one at a time.",
            "Do not use this for a one-shot operation: invoke the target tool directly so Allow once is bound to that exact tool-call occurrence.",
            &["Write", "Edit", "exec_command", "WebFetch"],
            vec![example(
                "Request remembered file-write permission",
                json!({"reason":"Need to write deployment config to /etc/nginx","permissions":[{"type":"write_file","resource":"/etc/nginx/conf.d/*"}]}),
                "Use when later target operations need a session/workspace/global remembered scope.",
            )],
        )),
        "Sleep" => Some(guide(
            "Sleep",
            ToolCategory::CommandExecution,
            "Pause briefly when waiting for an external state change before polling again.",
            "Do not use for normal reasoning pauses or long waits when another tool can fetch status directly.",
            &["write_stdin", "WebFetch"],
            vec![example(
                "Wait before next poll",
                json!({"seconds":2,"reason":"wait for background process output"}),
                "Use short waits between repeated status checks.",
            )],
        )),
        "loop_control" => Some(guide(
            "loop_control",
            ToolCategory::TaskManagement,
            "Manage one live bounded loop in your current host-admitted Standard ordinary Root execution. Inspect status first; use its generation and control_revision for CAS, preserving operation_id and payload on a retry. Start charges earlier rounds and injects its objective before the next provider call. An accepted objective edit applies only at the next iteration admission; read status to distinguish accepted and applied revisions. Bounds can only tighten. Stop prevents the next provider admission after the current tool batch drains.",
            "Do not infer host capability or permission from this guide. Do not target another Session, pause/resume, add cadence, extend or reset budgets, restart a stopped generation, or treat an accepted edit as already applied. Existing Plan, Child, Ultra and Supervisor gates still apply. User cancellation and host revocation remain authoritative.",
            &["Task"],
            vec![
                example(
                    "Inspect this admitted execution",
                    json!({"action":"status"}),
                    "Observe current generation, control_revision, objective revisions and remaining host bounds.",
                ),
                example(
                    "Start two bounded iterations",
                    json!({"action":"start","operation_id":"00000000-0000-4000-8000-000000000001","expected_revision":0,"objective":"Verify the next item using the accumulated context.","max_iterations":2,"max_total_rounds":6}),
                    "Use a fresh operation UUID. An absent prior loop uses revision zero; a retired prior generation must instead be named with its observed revision. Host ceilings include the round requesting start.",
                ),
                example(
                    "Accept a new objective for the next iteration",
                    json!({"action":"update","operation_id":"00000000-0000-4000-8000-000000000003","generation":"00000000-0000-4000-8000-000000000002","expected_revision":1,"objective":"Verify the remaining item independently."}),
                    "Replace the example generation and revision with current status. At most one objective edit may be pending; its receipt names the effective iteration.",
                ),
                example(
                    "Stop this generation",
                    json!({"action":"stop","operation_id":"00000000-0000-4000-8000-000000000004","generation":"00000000-0000-4000-8000-000000000002","expected_revision":2}),
                    "Use the observed generation and control revision. Stop is terminal for this generation and cannot replenish this activation.",
                ),
            ],
        )),
        "update_goal" => Some(guide(
            "update_goal",
            ToolCategory::TaskManagement,
            "Signal the autonomous session goal's status: status \"complete\" only when the full objective is achieved and you can prove every requirement is satisfied; status \"blocked\" only after the same blocker has persisted for at least three consecutive goal turns.",
            "Do not call to pause/resume or for ordinary task updates; do not mark complete merely because you are stopping or the budget is nearly spent. Completion is reverified by a side-channel check before the run ends.",
            &["Task"],
            vec![example(
                "Mark the goal achieved",
                json!({"status":"complete"}),
                "Call only once every requirement is proven satisfied by current-state evidence.",
            )],
        )),
        "load_skill" => Some(guide(
            "load_skill",
            ToolCategory::TaskManagement,
            "Load a selected skill's SKILL.md instructions and metadata by skill_id.",
            "Do not use for auxiliary resource files; use read_skill_resource for references/assets.",
            &["read_skill_resource"],
            vec![example(
                "Load skill instructions",
                json!({"skill_id":"rust-best-practices"}),
                "Call this before following a matched skill's detailed workflow.",
            )],
        )),
        "read_skill_resource" => Some(guide(
            "read_skill_resource",
            ToolCategory::FileReading,
            "Read auxiliary files under a loaded skill directory with optional offset/limit paging.",
            "Do not use for SKILL.md itself; call load_skill for primary instructions.",
            &["load_skill"],
            vec![example(
                "Read a skill reference file",
                json!({"skill_id":"rust-best-practices","resource_path":"references/chapter_01.md","offset":0,"limit":80}),
                "Use when the loaded instructions point to additional files.",
            )],
        )),
        "session_history_current" | "session_history" | "recall" | "session_inspector" => Some(guide(
            match tool_name {
                "session_inspector" => "session_inspector",
                "recall" => "recall",
                "session_history_current" => "session_history_current",
                _ => "session_history",
            },
            ToolCategory::FileReading,
            "Use the always-resident session_history_current identity to search your current Bamboo Session with search_current, then recover the hit's complete turn and nearby context with read_around or page exact bounded evidence with read_current, including history already compressed out of the model window. The host preserves complete logical turns and tool-call/result chains. The legacy session_history name remains compatible; Root schemas may additionally offer list/get_meta, privileged bounded reads, global search, and same-tree export_context only under that separate legacy identity. When export_context is present, Read its returned status/brief paths with bounded offset/limit and keep the returned revision fixed across continuation reads; exports are last persisted observations, not verified live progress. Raw Session history is transcript authority; memory is selective durable knowledge and may be stale.",
            "Do not pass a Session ID, boundary, or compressed-state mutation to current-Session actions: the host derives identity and the current-call boundary. Treat cursors only as opaque continuations and reuse them unchanged. Do not use history as a broad substitute for code search. Privileged cross-session actions exist only when the supplied schema lists them, and export quota/corruption errors do not authorize deleting snapshots.",
            &["session_note", "Read", "Task"],
            vec![
                example(
                    "Search this Session's stored history",
                    json!({"action":"search_current","query":"release checklist","limit":20}),
                    "Locate exact details omitted from the active model window. Returned excerpts are read-only; archived messages stay archived.",
                ),
                example(
                    "Page older exact turns",
                    json!({"action":"read_current","direction":"backward","limit":10,"max_chars":12000,"archived_only":true}),
                    "Follow next_cursor unchanged when more bounded turns remain.",
                ),
                example(
                    "Recover a search hit in context",
                    json!({"action":"read_around","message_id":"message-id-from-search","before_turns":1,"after_turns":1,"max_chars":12000}),
                    "The anchor and returned neighbors are complete logical turns; unavailable anchors do not reveal why they were excluded.",
                ),
            ],
        )),
        "scheduler" | "schedule_tasks" => Some(guide(
            if tool_name == "schedule_tasks" {
                "schedule_tasks"
            } else {
                "scheduler"
            },
            ToolCategory::TaskManagement,
            "Manage Bamboo scheduled automation jobs for recurring or delayed work. Use it to create, inspect, modify, run, or delete schedules without going through HTTP.",
            "Do not use for normal one-shot task planning inside the current conversation; use Task for active execution tracking instead.",
            &["Task", "Workspace"],
            vec![
                example(
                    "Create a recurring schedule",
                    json!({"action":"create","name":"daily-review","trigger":{"type":"interval","every_seconds":86400},"enabled":true,"run_config":{"auto_execute":true,"task_message":"Review new tickets","workspace_path":"/workspace/project"}}),
                    "Use when work should recur automatically over time.",
                ),
                example(
                    "Inspect schedule sessions",
                    json!({"action":"list_sessions","schedule_id":"sch_123"}),
                    "Use to see the sessions a schedule created or ran.",
                ),
            ],
        )),
        "SubAgent" => Some(guide(
            "SubAgent",
            ToolCategory::TaskManagement,
            "Use only intent, target, role, message, and reply_to; omit unused fields. For chat (the default intent), put the complete assignment and needed paths in message: omit target to create a child, or reuse its returned logical ActorId to correct or continue it. role selects a profile only on creation; tools and permissions come from the runtime. intent=inspect with message=tree shows the owned tree; with target, message selects overview, diagnostics, messages, result, or error. For a page or selected full message, JSON-encode view/cursor/message_id inside message and omit unused query fields. intent=control uses target and message=cancel or retry. Children use intent=ask_parent with a bounded question. Direct parents reply with intent=chat, the exact pending ParentRequest id in reply_to, and the answer in message, omitting target and role; forced permission accepts exactly approve_once or deny. The runtime manages activation and waiting.",
            "For ordinary executing Sessions, do not use proactively for simple one-step tasks. A trusted Supervisor delegates execution even for simple tasks: first use session_history list and bounded inspect to reuse existing managed Sessions for main execution. Use SubAgent only for auxiliary planning, review or verification; report a recoverable blocker if no suitable authorized target is available. Multiple children with distinct, non-overlapping assignments may run in parallel when the work splits into independent slices; never assign overlapping files or unclear shared responsibility. From a child session, create a nested child only when the current assignment explicitly authorizes nested delegation and it is necessary; do not create multiple overlapping children with unclear responsibilities.",
            &["Task"],
            vec![
                example(
                    "Delegate read-only exploration",
                    json!({"intent":"chat","role":"explorer","message":"Scope: inspect parser entrypoints in /workspace/project. Inputs: src/parser.rs and its callers. Allowed actions: read files only. Acceptance: report data flow with file references. Non-goals: edits, commits, or delivery. Stop after reporting evidence and blockers; forked context cannot expand this assignment."}),
                    "Give the child a complete, narrow task and choose its read-only role.",
                ),
                example(
                    "Correct an existing child",
                    json!({"intent":"chat","target":"child_123","message":"Focus on error handling only and report the specific risks."}),
                    "Reuse the returned logical ActorId to keep the correction in the same child.",
                ),
                example(
                    "Inspect a child's result",
                    json!({"intent":"inspect","target":"child_123","message":"result"}),
                    "Read bounded evidence from the returned child identity; follow its cursor using a JSON-encoded query in message.",
                ),
                example(
                    "Inspect the current tree",
                    json!({"intent":"inspect","message":"tree"}),
                    "Omit target to inspect the tree owned by this session.",
                ),
                example(
                    "Read the next transcript page",
                    json!({"intent":"inspect","target":"child_123","message":"{\"view\":\"messages\",\"cursor\":\"returned_cursor\"}"}),
                    "Copy the returned cursor unchanged; view and cursor are inside message, not top-level fields.",
                ),
                example(
                    "Answer a child's clarification",
                    json!({"intent":"chat","reply_to":"request_123","message":"Keep the existing error behavior and add coverage for the missing-input case."}),
                    "Use the exact pending ParentRequest id and omit target and role.",
                ),
                example(
                    "Answer a forced-permission request",
                    json!({"intent":"chat","reply_to":"request_456","message":"approve_once"}),
                    "For the exact pending forced-permission request, message must be approve_once or deny.",
                ),
                example(
                    "Ask the direct parent for clarification",
                    json!({"intent":"ask_parent","message":"Should the missing-input case preserve the current error text?"}),
                    "A child asks a bounded question and omits target, role, and reply_to; the runtime suspends it until its direct parent replies.",
                ),
            ],
        )),
        "deploy_agent" => Some(guide(
            "deploy_agent",
            ToolCategory::TaskManagement,
            "Spin up a new worker agent on demand and manage its lifecycle. action=deploy launches a fresh broker-agent as a local subprocess (env=local), a Docker container (env=docker, needs image), or a remote SSH host (env=ssh, needs host); it joins the same message broker, inherits your MCP servers + skills, and returns an id you then drive with ask_agent. action=list shows running workers; action=stop tears one down. Use it to scale yourself out — parallel hands locally, sandboxed work in Docker, or compute on another machine.",
            "Do not use when an existing worker (deploy_agent action=list) can take the task — reuse it with ask_agent instead of deploying a duplicate; do not use for in-context delegation that does not need a separate process (use SubAgent); always action=stop workers once their work is collected.",
            &["ask_agent", "SubAgent"],
            vec![
                example(
                    "Deploy a local worker",
                    json!({"action":"deploy","env":"local","role":"tester","model":"anthropic:claude-opus-4-8"}),
                    "Use for an extra parallel agent on this machine; the returned id is ask_agent's target.",
                ),
                example(
                    "Deploy a sandboxed Docker worker",
                    json!({"action":"deploy","env":"docker","image":"bamboo:latest","role":"researcher"}),
                    "Use for isolated/clean-env work; your bamboo home is mounted so it shares your config.",
                ),
                example(
                    "Deploy on a remote host",
                    json!({"action":"deploy","env":"ssh","host":"user@build-box","role":"builder"}),
                    "Use to run work near other machines or to borrow remote compute.",
                ),
                example(
                    "List then stop a worker",
                    json!({"action":"stop","id":"agent-7f8e9d"}),
                    "Use action=list to see running workers, then action=stop once a worker's work is done.",
                ),
            ],
        )),
        "ask_agent" => Some(guide(
            "ask_agent",
            ToolCategory::TaskManagement,
            "Command another running agent (deployed locally, in Docker, or on a remote host) over the message broker and get its answer back synchronously. target is the agent's broker id (from deploy_agent, or a peer's session id). mode=query (default) is READ-ONLY — the target summarizes/extracts from its current state without changing course, safe to poll repeatedly. mode=steer is WRITE — your question is injected into the target's live conversation to redirect or advance its work (assign the next task, hand off context).",
            "Do not use to talk to yourself or to an agent that does not exist yet — deploy it with deploy_agent first; do not use mode=steer when you only want to read progress (use query); for in-context delegation without a separate process, use SubAgent instead.",
            &["deploy_agent", "SubAgent"],
            vec![
                example(
                    "Poll a worker's progress (read-only)",
                    json!({"target":"agent-1a2b3c","question":"Summarize the auth flow you found so far.","mode":"query"}),
                    "Use query to pull a result or check status without disturbing the target's work.",
                ),
                example(
                    "Reassign or advance a worker (steer)",
                    json!({"target":"agent-1a2b3c","question":"Now write the fix to src/auth.rs and run the tests.","mode":"steer","timeout_secs":180}),
                    "Use steer to inject a new task into the target's conversation; raise timeout_secs for slow work.",
                ),
            ],
        )),
        _ => None,
    }
}

pub fn builtin_guides() -> Vec<ToolGuideSpec> {
    BUILTIN_GUIDE_NAMES
        .iter()
        .filter_map(|name| builtin_guide_spec(name))
        .collect()
}

fn guide(
    tool_name: &str,
    category: ToolCategory,
    when_to_use: &str,
    when_not_to_use: &str,
    related_tools: &[&str],
    examples: Vec<ToolExample>,
) -> ToolGuideSpec {
    ToolGuideSpec {
        tool_name: tool_name.to_string(),
        when_to_use: when_to_use.to_string(),
        when_not_to_use: when_not_to_use.to_string(),
        examples,
        related_tools: related_tools.iter().map(|name| name.to_string()).collect(),
        category,
    }
}

fn example(scenario: &str, parameters: serde_json::Value, explanation: &str) -> ToolExample {
    ToolExample::new(scenario, parameters, explanation)
}

#[cfg(test)]
mod tests {
    use crate::BUILTIN_TOOL_NAMES;

    use super::{builtin_guide_spec, BUILTIN_GUIDE_NAMES};

    #[test]
    fn every_builtin_tool_has_a_guide() {
        for name in BUILTIN_GUIDE_NAMES {
            assert!(
                builtin_guide_spec(name).is_some(),
                "missing guide for {}",
                name
            );
        }
    }

    #[test]
    fn builtin_guides_cover_all_builtin_tool_names() {
        for name in BUILTIN_TOOL_NAMES {
            assert!(
                builtin_guide_spec(name).is_some(),
                "missing builtin guide coverage for {}",
                name
            );
        }
    }

    #[test]
    fn loop_control_guide_examples_match_the_strict_command_contract() {
        let guide = builtin_guide_spec("loop_control").expect("registered builtin guide");
        for example in &guide.examples {
            crate::tools::loop_control::parse_loop_control_request(&example.parameters.to_string())
                .unwrap_or_else(|error| panic!("invalid loop_control example: {error}"));
        }
    }

    #[test]
    fn default_shell_guides_teach_pull_results_and_keep_legacy_lookup_explicit() {
        let exec = builtin_guide_spec("exec_command").unwrap();
        assert!(exec.when_to_use.contains("session_id"));
        assert!(exec.when_to_use.contains("normally remain pull-only"));
        assert!(exec.when_to_use.contains("background monitoring"));
        assert!(exec.when_to_use.contains("output_omitted_bytes"));
        assert!(exec.when_not_to_use.contains("stdin pipe, not a PTY"));
        assert!(exec.examples[0].parameters.get("cmd").is_some());
        let poll = builtin_guide_spec("write_stdin").unwrap();
        assert!(poll.when_to_use.contains("final results omit it"));
        assert!(poll.when_to_use.contains("include your own newline"));
        assert!(poll.when_to_use.contains("polling advisory"));
        assert!(poll.when_to_use.contains("not verified liveness"));
        assert!(poll
            .when_not_to_use
            .contains("infer completion from silence"));
        assert_eq!(poll.examples[0].parameters["chars"], "");
        for legacy in ["Bash", "BashInput", "BashOutput", "KillShell"] {
            assert!(!BUILTIN_GUIDE_NAMES.contains(&legacy));
            assert!(builtin_guide_spec(legacy).is_some());
        }
    }

    #[test]
    fn memory_server_tool_has_fallback_guide_spec() {
        let guide = builtin_guide_spec("memory").expect("memory guide should exist");
        assert_eq!(guide.tool_name, "memory");
        assert!(guide.examples.iter().any(|example| {
            example
                .parameters
                .get("action")
                .and_then(|value| value.as_str())
                == Some("merge")
        }));
    }

    #[test]
    fn current_history_guide_teaches_exact_retrieval_and_authority_boundaries() {
        let guide = builtin_guide_spec("session_history_current")
            .expect("current-history guide should exist");
        assert_eq!(guide.tool_name, "session_history_current");
        for action in ["search_current", "read_current", "read_around"] {
            assert!(guide.examples.iter().any(|example| {
                example
                    .parameters
                    .get("action")
                    .and_then(|value| value.as_str())
                    == Some(action)
            }));
        }
        assert!(guide.when_to_use.contains("always-resident"));
        assert!(guide.when_to_use.contains("transcript authority"));
        assert!(guide.when_to_use.contains("memory is selective"));
        assert!(guide.when_not_to_use.contains("Session ID"));
    }

    #[test]
    fn memory_guide_teaches_compact_recall_and_small_lexical_hints() {
        let guide = builtin_guide_spec("memory").expect("memory guide should exist");
        let examples_for = |action: &str| {
            guide
                .examples
                .iter()
                .filter(|example| {
                    example
                        .parameters
                        .get("action")
                        .and_then(|value| value.as_str())
                        == Some(action)
                })
                .collect::<Vec<_>>()
        };

        let queries = examples_for("query");
        assert!(!queries.is_empty());
        assert!(queries
            .iter()
            .all(|example| example.parameters.pointer("/options/limit").is_none()));
        assert_eq!(examples_for("get").len(), 1);

        for action in ["write", "merge", "consolidate"] {
            let examples = examples_for(action);
            assert_eq!(examples.len(), 1, "missing {action} example");
            for field in ["keywords", "entities", "tags"] {
                let hints = examples[0].parameters[field]
                    .as_array()
                    .unwrap_or_else(|| panic!("{action} example is missing {field}"));
                assert!(!hints.is_empty() && hints.len() <= 3);
            }
        }

        let split = examples_for("split");
        let pieces = split[0].parameters["pieces"]
            .as_array()
            .expect("split example should contain pieces");
        for piece in pieces {
            for field in ["keywords", "entities", "tags"] {
                let hints = piece[field]
                    .as_array()
                    .unwrap_or_else(|| panic!("split piece is missing {field}"));
                assert!(!hints.is_empty() && hints.len() <= 3);
            }
        }
    }

    #[test]
    fn subagent_guide_makes_nested_delegation_explicit_and_necessary() {
        let guide = builtin_guide_spec("SubAgent").expect("SubAgent guide should exist");
        assert!(guide.when_not_to_use.contains("child session"));
        assert!(guide
            .when_not_to_use
            .contains("explicitly authorizes nested delegation"));
        assert!(guide.when_not_to_use.contains("it is necessary"));
        assert!(!guide
            .when_not_to_use
            .contains("do not spawn children from child sessions"));
        assert!(guide
            .when_not_to_use
            .contains("non-overlapping assignments may run in parallel"));
        assert!(guide.when_to_use.contains("intent=inspect"));
        assert!(guide.when_to_use.contains("intent=control"));
        assert!(guide.when_to_use.contains("intent=ask_parent"));
        assert!(guide.when_to_use.contains("omit unused fields"));
        assert!(guide.when_to_use.contains("ParentRequest"));
        assert!(!guide.when_to_use.contains("action="));
        for example in &guide.examples {
            let fields = example
                .parameters
                .as_object()
                .expect("SubAgent example parameters should be an object");
            assert!(fields
                .keys()
                .all(|field| ["intent", "target", "role", "message", "reply_to"]
                    .contains(&field.as_str())));
            assert!(
                fields
                    .values()
                    .all(|value| { value.as_str().is_some_and(|text| !text.trim().is_empty()) }),
                "unused fields must be omitted, not empty strings or nulls"
            );
            if fields.contains_key("target") {
                assert!(!fields.contains_key("role"));
                assert!(!fields.contains_key("reply_to"));
            }
            if fields.contains_key("reply_to") {
                assert_eq!(fields["intent"], "chat");
                assert!(!fields.contains_key("target"));
                assert!(!fields.contains_key("role"));
            }
        }
    }

    #[test]
    fn subagent_guide_examples_cover_delegation_inspection_and_parent_replies() {
        let guide = builtin_guide_spec("SubAgent").expect("SubAgent guide should exist");
        let examples = guide
            .examples
            .iter()
            .map(|example| &example.parameters)
            .collect::<Vec<_>>();
        assert!(examples.iter().any(|args| {
            args["intent"] == "chat" && args.get("role").is_some() && args.get("target").is_none()
        }));
        assert!(examples
            .iter()
            .any(|args| { args["intent"] == "chat" && args.get("target").is_some() }));
        assert!(examples
            .iter()
            .any(|args| { args["intent"] == "inspect" && args["message"] == "result" }));
        assert!(examples.iter().any(|args| {
            args["intent"] == "inspect" && args["message"] == "tree" && args.get("target").is_none()
        }));
        assert!(examples.iter().any(|args| {
            args["intent"] == "chat"
                && args.get("reply_to").is_some()
                && args["message"] == "approve_once"
        }));
        assert!(examples.iter().any(|args| {
            args["intent"] == "chat"
                && args.get("reply_to").is_some()
                && args["message"] != "approve_once"
        }));

        let query = examples
            .iter()
            .find_map(|args| {
                (args["intent"] == "inspect")
                    .then(|| args["message"].as_str())
                    .flatten()
                    .and_then(|message| serde_json::from_str::<serde_json::Value>(message).ok())
            })
            .expect("a valid JSON-encoded inspection page example");
        assert_eq!(query["view"], "messages");
        assert!(query["cursor"]
            .as_str()
            .is_some_and(|cursor| !cursor.is_empty()));
        assert!(query.get("message_id").is_none());
    }

    #[test]
    fn broker_server_tools_have_fallback_guide_specs() {
        // deploy_agent: covers all three placements + a steer example via ask_agent.
        let deploy = builtin_guide_spec("deploy_agent").expect("deploy_agent guide should exist");
        assert_eq!(deploy.tool_name, "deploy_agent");
        assert!(deploy.related_tools.iter().any(|t| t == "ask_agent"));
        for env in ["local", "docker", "ssh"] {
            assert!(
                deploy.examples.iter().any(|example| example
                    .parameters
                    .get("env")
                    .and_then(|value| value.as_str())
                    == Some(env)),
                "deploy_agent guide should show an env={env} example"
            );
        }

        // ask_agent: must teach both query (read-only) and steer (write) modes.
        let ask = builtin_guide_spec("ask_agent").expect("ask_agent guide should exist");
        assert_eq!(ask.tool_name, "ask_agent");
        assert!(ask.related_tools.iter().any(|t| t == "deploy_agent"));
        for mode in ["query", "steer"] {
            assert!(
                ask.examples.iter().any(|example| example
                    .parameters
                    .get("mode")
                    .and_then(|value| value.as_str())
                    == Some(mode)),
                "ask_agent guide should show a mode={mode} example"
            );
        }
    }
}