theway-daemon 0.1.21

theway daemon — the single agent-runtime kernel (bin `thewayd`): harness assembly, local/sandbox tool policy, triggers/cron/session/DAG runtime, skills, MCP/LSP wiring, serving the gRPC/HTTP/MCP transports from theway-transport. Terminal UI lives in the theway-tui crate.
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
//! Tool ASSEMBLY layer — the application-layer half of the session tool set.
//!
//! All tool BODIES (harness-runtime tools AND local-execution tools) live HERE, in
//! the daemon (daemon-kernel-layers: the daemon is the single kernel; the engine
//! crate `theway_core` keeps only the `AgentTool` / `ToolExecutor` / `ExecutionEnv`
//! trait families and the agent runtime):
//!
//! - **Harness-runtime tools** (graph/DAG, subagents, skills, memory, MCP — formerly
//!   `theway_core::tools`) sit next to the local bodies: `assembly` / `subagent` /
//!   `dag_tools` / the skill family / `memory` / `mcp_adapter` / `exec_shell`.
//! - **Local-execution tool bodies** (bash / fs / git / grep / find / ls / outline /
//!   truncate) are environment-specific agent capabilities.
//! - **Web tool** (`web_fetch`) is an app-layer capability too (it needs external
//!   credentials/configuration).
//! - **Assembly**: [`local_tools`] / the subagent tool-set resolver / `session_tool_set`
//!   wire engine tools + local tools together, then append the server-side
//!   trigger/cron family.
//!
//! Subagent tool sets are injected into the engine (specs carry no tool
//! factory): [`subagent_tool_sets`] builds the ONE resolver both `subagent_tool` and the
//! DAG node launcher are constructed with — every spec gets the same uniform set (engine
//! tools minus `subagent`/`dag_*` plus local tools); behavior is prompt-defined.

use std::path::PathBuf;
use std::sync::Arc;

use async_trait::async_trait;
use serde_json::Value;
use theway_core::executor::ToolExecutor;
use theway_core::multiagent::graph::engine::DagEngine;
use theway_core::multiagent::graph::node_launcher;
use theway_core::multiagent::jobs::SubagentJobRegistry;
use theway_core::multiagent::types::ToolSetResolver;
use theway_core::{
    AgentTool, AgentToolError, AgentToolResult, AgentToolUpdate, PermissionClassification,
};
use theway_llm_provider::Tool;
use tokio_util::sync::CancellationToken;

use crate::runtime_storage::SessionRepository;
use crate::tools::skill::SkillHarnessCell;

// ── tool bodies (harness-runtime + app-layer) ───────────────────────────────
//
// Plain module declarations: `tests/tools.rs` pulls in only `triggers/tool_assembly.rs`
// by `#[path]`, so nothing here needs `#[path]` re-routing — a plain `pub mod bash;`
// resolves `src/tools/bash.rs` and lets `bash.rs`'s own `mod tests;` find
// `src/tools/bash/tests/` (a `#[path]`-included module would break that resolution).

// Harness-runtime tools (moved from theway-core, daemon-kernel-layers).
pub mod assembly;
pub mod dag_tools;
pub mod exec;
pub mod exec_shell;
pub mod install_skill;
pub mod mcp_adapter;
pub mod memory;
pub mod remove_skill;
pub mod session_graph;
pub mod set_skill_state;
pub mod skill;
pub mod skill_builder;
pub mod subagent;

// App-layer local-execution + web tools.
pub mod bash;
pub mod edit;
pub mod find;
pub mod git;
pub mod grep;
pub mod ls;
pub mod outline;
pub mod read;
pub mod session_tool_result;
pub mod truncate;
pub mod web_fetch;
pub mod write;

// Trigger/cron model-facing constructors live in `triggers::tool_assembly` (kept
// out of this file so `tests/tools.rs` can `#[path]`-include just those).
pub use crate::triggers::tool_assembly::{
    list_cron_jobs_tool, list_triggers_tool, new_cron_job_tool, new_trigger_tool,
    remove_cron_job_tool, remove_trigger_tool, set_cron_job_state_tool, set_trigger_state_tool,
};

/// Tool names that bypass the [`ToolExecutor`] seam and touch the host OS directly:
/// `bash` spawns `sh -c` process groups (setsid/killpg), the `exec_shell` family owns
/// `tokio::process` children, and `ls` / `grep` / `find` walk the local filesystem
/// with `tokio::fs` / `ignore`. They are registered ONLY in `local` execution mode.
/// In sandbox mode (issue #123: selected at runtime via `[executor] kind` in
/// config.toml) they are omitted from the tool set (fail closed, issue #64) —
/// the [`crate::executor::sandbox::SandboxExecutor`] seam covers only the
/// executor-backed tools, so these bodies would otherwise keep acting straight on the
/// host even in sandbox mode.
pub const LOCAL_ONLY_TOOL_NAMES: &[&str] = &[
    "bash",
    "exec",
    "get_output",
    "kill_shell",
    "write_to_process",
    "ls",
    "grep",
    "find",
];

/// Scopes a direct-OS tool to an owning cwd unless the caller supplies one.
pub struct CwdScopedTool {
    inner: Arc<dyn AgentTool>,
    cwd: PathBuf,
}

impl CwdScopedTool {
    pub fn new(inner: Arc<dyn AgentTool>, cwd: PathBuf) -> Self {
        Self { inner, cwd }
    }

    fn scope_args(&self, mut args: Value) -> Value {
        match self.inner.definition().name.as_str() {
            "bash" | "exec" | "ls" | "grep" | "find" if args.get("cwd").is_none() => {
                if let Some(obj) = args.as_object_mut() {
                    obj.insert("cwd".into(), self.cwd.to_string_lossy().into_owned().into());
                }
            }
            _ => {}
        }
        args
    }
}

#[async_trait]
impl AgentTool for CwdScopedTool {
    fn definition(&self) -> &Tool {
        self.inner.definition()
    }

    fn label(&self) -> &str {
        self.inner.label()
    }

    fn execution_mode(&self) -> Option<theway_core::ToolExecutionMode> {
        self.inner.execution_mode()
    }

    fn prepare_arguments(&self, args: Value) -> Value {
        self.inner.prepare_arguments(self.scope_args(args))
    }

    fn permission_classification(&self, prepared_args: &Value) -> PermissionClassification {
        self.inner.permission_classification(prepared_args)
    }

    async fn execute(
        &self,
        tool_call_id: &str,
        params: Value,
        cancel: CancellationToken,
        on_update: Option<AgentToolUpdate>,
    ) -> Result<AgentToolResult, AgentToolError> {
        self.inner
            .execute(tool_call_id, self.scope_args(params), cancel, on_update)
            .await
    }
}

/// Compatibility wrapper that uses the process cwd as the owning cwd.
pub fn local_tools(executor: Arc<dyn ToolExecutor>) -> Vec<Arc<dyn AgentTool>> {
    local_tools_for_cwd(executor, std::env::current_dir().unwrap_or_default())
}

/// Local-execution tool set (the app-layer half of the session tool set): shell / fs /
/// git / grep / web — everything that depends on the execution environment. No engine
/// tools here (DAG / subagent / skills / memory come from the kernel's own assembly,
/// [`crate::tools::assembly`]); no duplicate-memory risk.
///
/// Executor binding (sdk-split-local-sandbox node 8): file-content and process tools
/// (read / write / edit / outline / git) dispatch their effects through the injected
/// [`ToolExecutor`] — the local executor for local editing mode; a sandbox
/// executor swaps the execution environment without touching tool definitions. The
/// remaining local tools are not yet wired through the executor: `bash` keeps its
/// process-group-kill + cancel semantics (the executor's `run_command` kills only the
/// direct child), and `ls` / `grep` / `find` / the `exec_shell` family use richer
/// directory/walk surfaces than the executor trait's first cut exposes.
pub fn local_tools_for_cwd(
    executor: Arc<dyn ToolExecutor>,
    cwd: PathBuf,
) -> Vec<Arc<dyn AgentTool>> {
    local_tools_for_cwd_with_tgrep(executor, cwd, None)
}

/// [`local_tools_for_cwd`] with the daemon-wide tgrep registry attached to the
/// grep tool (issue #121). `None` keeps the grep tool on its in-process walker.
pub fn local_tools_for_cwd_with_tgrep(
    executor: Arc<dyn ToolExecutor>,
    cwd: PathBuf,
    tgrep: Option<crate::tgrep_server::TgrepServerRegistry>,
) -> Vec<Arc<dyn AgentTool>> {
    vec![
        Arc::new(read::ReadTool::new(executor.clone())),
        Arc::new(write::WriteTool::new(executor.clone())),
        Arc::new(edit::EditTool::new(executor.clone())),
        Arc::new(CwdScopedTool::new(Arc::new(bash::BashTool), cwd.clone())),
        Arc::new(CwdScopedTool::new(
            Arc::new(exec_shell::ExecTool),
            cwd.clone(),
        )),
        Arc::new(exec_shell::GetOutputTool),
        Arc::new(exec_shell::KillShellTool),
        Arc::new(exec_shell::WriteToProcessTool),
        Arc::new(CwdScopedTool::new(Arc::new(ls::LsTool), cwd.clone())),
        Arc::new(CwdScopedTool::new(
            Arc::new(grep::GrepTool::new(tgrep, cwd.clone())),
            cwd.clone(),
        )),
        Arc::new(CwdScopedTool::new(Arc::new(find::FindTool), cwd)),
        Arc::new(outline::OutlineTool::new(executor.clone())),
        Arc::new(git::GitTool::new(executor)),
        Arc::new(web_fetch::WebFetchTool),
    ]
}

/// Sandbox tool set: only the executor-backed tools and the network-only web
/// tool are registered (issue #64, fail closed). The omitted direct-OS tools
/// are named explicitly in a `tracing::warn` so the degraded tool set is never
/// silent.
pub fn sandbox_tools_for_cwd(executor: Arc<dyn ToolExecutor>) -> Vec<Arc<dyn AgentTool>> {
    tracing::warn!(
        omitted = ?LOCAL_ONLY_TOOL_NAMES,
        "sandbox execution mode: local-only tools bypass the ToolExecutor seam and touch \
         the host FS/process table directly, so they are NOT registered (fail closed); \
         executor-backed tools (read/write/edit/outline/git) and the network-only
         web_fetch tool remain"
    );
    vec![
        Arc::new(read::ReadTool::new(executor.clone())),
        Arc::new(write::WriteTool::new(executor.clone())),
        Arc::new(edit::EditTool::new(executor.clone())),
        Arc::new(outline::OutlineTool::new(executor.clone())),
        Arc::new(git::GitTool::new(executor)),
        Arc::new(web_fetch::WebFetchTool),
    ]
}

/// Runtime execution-environment tool set (issue #123): `local` registers the
/// full local tool set (including direct-OS tools), `sandbox` registers only
/// the executor-backed tools plus `web_fetch`. The daemon's session/subagent
/// assembly calls this with the `[executor] kind` selected at startup; the
/// older [`local_tools_for_cwd_with_tgrep`] entry points stay local for
/// compatibility with tests and embedders.
pub fn tools_for_cwd_with_tgrep(
    executor: Arc<dyn ToolExecutor>,
    cwd: PathBuf,
    tgrep: Option<crate::tgrep_server::TgrepServerRegistry>,
    kind: theway_core::executor::ExecutorKind,
) -> Vec<Arc<dyn AgentTool>> {
    match kind {
        theway_core::executor::ExecutorKind::Local => {
            local_tools_for_cwd_with_tgrep(executor, cwd, tgrep)
        }
        theway_core::executor::ExecutorKind::Sandbox => sandbox_tools_for_cwd(executor),
    }
}

/// Build the `Subagent` tool. Separate from `local_tools` because the tool needs the model handle to
/// spawn its inner harness; the caller wires it in at construction time. `session_id`
/// (session-resource-model) stamps the owning session on every spawned job — each harness
/// build gets its own SubagentTool stamped with that harness's session.
///
/// The subagent tool set comes from [`subagent_tool_sets`] — the SAME resolver the DAG
/// node launcher gets (one uniform set per spec: engine tools minus subagent/dag_* plus
/// local tools). The main agent picks the spec (explorer / planner / executor-coder /
/// checker / general) and defines in the prompt what the subagent may do.
pub fn subagent_tool(
    model: impl Into<Option<theway_llm_provider::Model>>,
    stream_fn: Option<theway_core::StreamFn>,
    registry: SubagentJobRegistry,
    memory_dir: PathBuf,
    base_dir: PathBuf,
    skill_harness_cell: SkillHarnessCell,
    session_id: Option<String>,
    executor: Arc<dyn ToolExecutor>,
) -> Arc<dyn AgentTool> {
    Arc::new(
        subagent::SubagentTool::new(
            model,
            stream_fn,
            subagent_tool_sets(memory_dir, base_dir, skill_harness_cell, executor),
            crate::agent_specs::launch_resolver(),
            crate::agent_specs::spec_names(),
            registry,
        )
        .with_session_id(session_id),
    )
}

/// Build the app-layer tool-set resolver injected into the subagent tool and the DAG
/// node launcher. ONE uniform set for every spec: engine tools minus the two
/// orchestration tools (`subagent` / `dag_*`) plus local tools — assembled kernel-side,
/// [`crate::tools::assembly::subagent_tools`]. Per-spec tool differences are gone;
/// behavior is defined by the spec's system prompt and the parent's task prompt.
/// `base_dir` is the theway base dir (issue #66: `DaemonPaths::base`) for the skill
/// family's host paths.
pub fn subagent_tool_sets(
    memory_dir: PathBuf,
    base_dir: PathBuf,
    skill_harness_cell: SkillHarnessCell,
    executor: Arc<dyn ToolExecutor>,
) -> ToolSetResolver {
    subagent_tool_sets_for_cwd(
        memory_dir,
        base_dir,
        skill_harness_cell,
        executor,
        std::env::current_dir().unwrap_or_default(),
    )
}

pub fn subagent_tool_sets_for_cwd(
    memory_dir: PathBuf,
    base_dir: PathBuf,
    skill_harness_cell: SkillHarnessCell,
    executor: Arc<dyn ToolExecutor>,
    cwd: PathBuf,
) -> ToolSetResolver {
    subagent_tool_sets_for_cwd_with_tgrep(
        memory_dir,
        base_dir,
        skill_harness_cell,
        executor,
        cwd,
        None,
    )
}

/// [`subagent_tool_sets_for_cwd`] with the daemon-wide tgrep registry attached.
#[allow(clippy::too_many_arguments)]
pub fn subagent_tool_sets_for_cwd_with_tgrep(
    memory_dir: PathBuf,
    base_dir: PathBuf,
    skill_harness_cell: SkillHarnessCell,
    executor: Arc<dyn ToolExecutor>,
    cwd: PathBuf,
    tgrep: Option<crate::tgrep_server::TgrepServerRegistry>,
) -> ToolSetResolver {
    subagent_tool_sets_for_cwd_with_tgrep_and_kind(
        memory_dir,
        base_dir,
        skill_harness_cell,
        executor,
        cwd,
        tgrep,
        theway_core::executor::ExecutorKind::Local,
    )
}

/// [`subagent_tool_sets_for_cwd_with_tgrep`] with the runtime-selected execution
/// environment (issue #123). Sandbox mode swaps the app-layer tools for the
/// executor-backed set and drops the direct-FS engine tools.
#[allow(clippy::too_many_arguments)]
pub fn subagent_tool_sets_for_cwd_with_tgrep_and_kind(
    memory_dir: PathBuf,
    base_dir: PathBuf,
    skill_harness_cell: SkillHarnessCell,
    executor: Arc<dyn ToolExecutor>,
    cwd: PathBuf,
    tgrep: Option<crate::tgrep_server::TgrepServerRegistry>,
    kind: theway_core::executor::ExecutorKind,
) -> ToolSetResolver {
    assembly::subagent_tools_for_kind(
        &memory_dir,
        &base_dir,
        &skill_harness_cell,
        // The kernel-side local-tools factory closes over the daemon's executor and cwd,
        // so every subagent / DAG-node tool set dispatches through the same execution
        // environment and path-scoped direct-OS tools.
        Arc::new(move || {
            tools_for_cwd_with_tgrep(executor.clone(), cwd.clone(), tgrep.clone(), kind)
        }),
        kind,
    )
}

/// Build a DAG node launcher wired to `engine`, with the app-layer tool-set resolver.
/// `base_dir` is the theway base dir (issue #66: `DaemonPaths::base`) for the skill
/// family's host paths.
pub fn node_launcher(
    engine: Arc<DagEngine>,
    model: impl Into<Option<theway_llm_provider::Model>>,
    stream_fn: Option<theway_core::StreamFn>,
    cwd: PathBuf,
    registry: SubagentJobRegistry,
    memory_dir: PathBuf,
    base_dir: PathBuf,
    skill_harness_cell: SkillHarnessCell,
    executor: Arc<dyn ToolExecutor>,
    tgrep: Option<crate::tgrep_server::TgrepServerRegistry>,
) -> Arc<node_launcher::NodeLauncherImpl> {
    node_launcher_with_kind(
        engine,
        model,
        stream_fn,
        cwd,
        registry,
        memory_dir,
        base_dir,
        skill_harness_cell,
        executor,
        tgrep,
        theway_core::executor::ExecutorKind::Local,
    )
}

/// [`node_launcher`] with the runtime-selected execution environment (issue #123).
#[allow(clippy::too_many_arguments)]
pub fn node_launcher_with_kind(
    engine: Arc<DagEngine>,
    model: impl Into<Option<theway_llm_provider::Model>>,
    stream_fn: Option<theway_core::StreamFn>,
    cwd: PathBuf,
    registry: SubagentJobRegistry,
    memory_dir: PathBuf,
    base_dir: PathBuf,
    skill_harness_cell: SkillHarnessCell,
    executor: Arc<dyn ToolExecutor>,
    tgrep: Option<crate::tgrep_server::TgrepServerRegistry>,
    kind: theway_core::executor::ExecutorKind,
) -> Arc<node_launcher::NodeLauncherImpl> {
    node_launcher::node_launcher(
        engine,
        model.into(),
        stream_fn,
        cwd.clone(),
        registry,
        subagent_tool_sets_for_cwd_with_tgrep_and_kind(
            memory_dir,
            base_dir,
            skill_harness_cell,
            executor,
            cwd,
            tgrep,
            kind,
        ),
        crate::agent_specs::launch_resolver(),
    )
}

/// Build the per-session tool set (session-resource-model). One source of truth shared by
/// the CLI's initial harness build and the session factory ([`crate::session_ops::SessionFactory`]):
/// everything here is either session-stamped (`dag_*` / `subagent`) or must be rebuilt per
/// harness (the skill family wires a fresh harness cell per build). Local tools
/// ([`local_tools`]) + engine tools ([`crate::tools::assembly::engine_tools`]) +
/// the server-side trigger/cron family. Process-level tool groups (MCP tools) are the
/// caller's to add.
pub fn session_tool_set<'a>(
    memory_dir: &std::path::Path,
    base_dir: &std::path::Path,
    dag_engine: &Arc<DagEngine>,
    subagent_registry: &SubagentJobRegistry,
    model: impl Into<Option<&'a theway_llm_provider::Model>>,
    stream_fn: Option<&theway_core::StreamFn>,
    skill_harness_cell: &SkillHarnessCell,
    session_id: &str,
    executor: Arc<dyn ToolExecutor>,
    services: &crate::DaemonServices,
    repo: Arc<dyn SessionRepository>,
) -> Vec<Arc<dyn AgentTool>> {
    session_tool_set_for_cwd(
        memory_dir,
        base_dir,
        dag_engine,
        subagent_registry,
        model.into(),
        stream_fn,
        skill_harness_cell,
        session_id,
        executor,
        services,
        repo,
        std::env::current_dir().unwrap_or_default(),
    )
}

pub fn session_tool_set_for_cwd(
    memory_dir: &std::path::Path,
    base_dir: &std::path::Path,
    dag_engine: &Arc<DagEngine>,
    subagent_registry: &SubagentJobRegistry,
    model: Option<&theway_llm_provider::Model>,
    stream_fn: Option<&theway_core::StreamFn>,
    skill_harness_cell: &SkillHarnessCell,
    session_id: &str,
    executor: Arc<dyn ToolExecutor>,
    services: &crate::DaemonServices,
    repo: Arc<dyn SessionRepository>,
    cwd: PathBuf,
) -> Vec<Arc<dyn AgentTool>> {
    session_tool_set_for_cwd_with_kind(
        memory_dir,
        base_dir,
        dag_engine,
        subagent_registry,
        model,
        stream_fn,
        skill_harness_cell,
        session_id,
        executor,
        services,
        repo,
        cwd,
        theway_core::executor::ExecutorKind::Local,
    )
}

/// [`session_tool_set_for_cwd`] with the runtime-selected execution environment
/// (issue #123).
#[allow(clippy::too_many_arguments)]
pub fn session_tool_set_for_cwd_with_kind(
    memory_dir: &std::path::Path,
    base_dir: &std::path::Path,
    dag_engine: &Arc<DagEngine>,
    subagent_registry: &SubagentJobRegistry,
    model: Option<&theway_llm_provider::Model>,
    stream_fn: Option<&theway_core::StreamFn>,
    skill_harness_cell: &SkillHarnessCell,
    session_id: &str,
    executor: Arc<dyn ToolExecutor>,
    services: &crate::DaemonServices,
    repo: Arc<dyn SessionRepository>,
    cwd: PathBuf,
    kind: theway_core::executor::ExecutorKind,
) -> Vec<Arc<dyn AgentTool>> {
    let mut tools = tools_for_cwd_with_tgrep(
        executor.clone(),
        cwd.clone(),
        Some(services.tgrep.clone()),
        kind,
    );
    // Engine-owned tools (DAG / subagent / skills / memory), assembled kernel-side with the
    // same subagent tool-set resolver the DAG node launcher uses.
    tools.extend(assembly::engine_tools_for_kind(
        memory_dir,
        base_dir,
        dag_engine,
        subagent_registry,
        subagent_tool_sets_for_cwd_with_tgrep_and_kind(
            memory_dir.to_path_buf(),
            base_dir.to_path_buf(),
            skill_harness_cell.clone(),
            executor,
            cwd.clone(),
            Some(services.tgrep.clone()),
            kind,
        ),
        crate::agent_specs::launch_resolver(),
        crate::agent_specs::spec_names(),
        model,
        stream_fn,
        skill_harness_cell,
        session_id,
        services.reload.clone(),
        kind,
    ));
    // Session graph tools (main-agent only): list/read/status/wait/attach against
    // the Turso-backed session graph.
    let graph_path = theway_contract::config::sessions_dir_for_cwd(&cwd)
        .join(theway_storage::session_graph::SESSION_GRAPH_DB_FILE);
    tools.extend(session_graph::SessionGraphTools::create(
        repo.clone(),
        graph_path,
        cwd.clone(),
    ));
    // On-demand stored tool-result access (#50): the model can grep and page through
    // full results that were virtualized into placeholders in the LLM context.
    tools.extend(session_tool_result::SessionToolResultTools::create(
        repo.clone(),
        session_id.to_string(),
    ));
    // Trigger/cron family: harness-adjacent but implemented in this crate.
    tools.push(new_cron_job_tool(
        skill_harness_cell.clone(),
        services.cron.clone(),
    ));
    tools.push(list_cron_jobs_tool(services.cron.clone()));
    tools.push(remove_cron_job_tool(
        skill_harness_cell.clone(),
        services.cron.clone(),
    ));
    tools.push(set_cron_job_state_tool(
        skill_harness_cell.clone(),
        services.cron.clone(),
    ));
    tools.push(new_trigger_tool(services.dynamic_triggers.clone()));
    tools.push(list_triggers_tool(services.dynamic_triggers.clone()));
    tools.push(remove_trigger_tool(services.dynamic_triggers.clone()));
    tools.push(set_trigger_state_tool(services.dynamic_triggers.clone()));
    tools
}

#[cfg(test)]
// Test files live in `tests/tools/mod.rs` (mirror of src), pulled in by
// path so they keep unit-test semantics (private access). See docs/rust-test-files.md.
tests_bridge_macro::tests_bridge!("tools");