Skip to content

Commit 88fb998

Browse files
feat(agents): bundle native monitoring tools (#234)
## Summary Bundle long-lived monitoring and cross-session orchestration directly with Berd: - Add a native `berd-monitor` sidecar that detaches producers, durably buffers line-oriented output, wakes the owning session through `berdctl`, and stops complete process trees on macOS, Linux, and Windows. - Bundle `berd-monitor` and `berd-orchestrator` skills so agents can discover these workflows without private configuration. - Add optional `--from` attribution to `berdctl session create` and `session send`, while retaining trusted Berd provenance. Monitor deliveries use `--from berd-monitor`. - Add optional `--delivery-id` to `session send`. Repeated IDs for one session are accepted without creating duplicate user turns, so a monitor can safely retry after a crash between delivery and local acknowledgement. Monitor state is private, session-scoped, and exclusively owned. Failed or timed-out launches terminate and reap their detached child. Producer trees are cleaned up on every exit path. Delivery subprocesses have a monitor-owned deadline. Unterminated and oversized output is bounded and safely chunked, and final delivery retries until success or explicit stop. ### Related issue none found ### Testing - `cargo test -p berdctl`: 52 passed, 1 ignored - `cargo test -p berd-monitor`: 22 passed - `cargo check -p berd-monitor --target x86_64-pc-windows-msvc` - Live smoke tests verified custom create/send attribution, detached monitor wake-up, duplicate-owner rejection, synchronous launch failure, and process-tree termination. --------- Signed-off-by: John Tennant <jtennant@block.xyz>
1 parent 1366fb9 commit 88fb998

34 files changed

Lines changed: 3184 additions & 188 deletions
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
name: berd-monitor
3+
description: >-
4+
Run a long-lived command outside the current Berd turn and wake the owning
5+
session with actionable stdout. Use for builds, tests, reviews, deployments,
6+
polling, watchers, and other external waits.
7+
metadata:
8+
berdBundled: true
9+
---
10+
11+
# Berd Monitor
12+
13+
Use `berd-monitor` when a command may outlive the current turn. It detaches the
14+
producer from Berd and the agent harness, buffers output across delivery
15+
failures, and sends complete stdout lines to the exact originating session.
16+
17+
Run short, bounded commands normally. Do not hold a turn open with `sleep`,
18+
polling, log tailing, or a foreground timeout.
19+
20+
## Start
21+
22+
```bash
23+
berd-monitor run \
24+
--state-key <stable-key> \
25+
--label '<concise source name>' \
26+
--instructions '<short event-handling guidance>' \
27+
-- <producer-command> [args...]
28+
```
29+
30+
`AGENT_SESSION_ID` selects the current session. Use `--session-id` only for
31+
another positively identified session; never infer a session from the working
32+
directory. The command prints the detached monitor PID. After checking the
33+
monitor's `watcher.log` under the printed state directory when diagnosis is
34+
needed, continue other work or end the turn—never wait on the detached PID.
35+
36+
The producer's stdout is the event API:
37+
38+
- emit concise, newline-terminated milestones and flush promptly;
39+
- put verbose output and diagnostics in durable logs or stderr;
40+
- avoid NUL bytes and emit a final summary when practical.
41+
42+
Default `--if-running steer` adds an event to an active run or starts a new
43+
turn. Use `--if-running queue` only when the active run must not be steered.
44+
Pending events retry without being dropped, and a trailing partial line is
45+
delivered when the producer exits.
46+
47+
Delivered messages are visibly labeled as coming from `berd-monitor`. Stop a
48+
monitor with:
49+
50+
```bash
51+
berd-monitor stop --state-key <stable-key>
52+
```
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
name: berd-orchestrator
3+
description: >-
4+
Coordinate work across Berd sessions while keeping one conversation
5+
available to the user. Use for a long-lived orchestration session.
6+
metadata:
7+
berdBundled: true
8+
---
9+
10+
# Berd Orchestrator
11+
12+
Keep this session available for conversation. Use `berdctl session list`,
13+
`get`, `create`, and `send` to delegate substantial work to other sessions
14+
instead of doing it here.
15+
16+
Prefer an existing session that owns the relevant context. Create one when
17+
work has no owner, and give it a complete task. Do not approve, merge, or
18+
archive work without the user's direction.
19+
20+
When creating or sending to another session, pass
21+
`--from '<concise owner or workstream>'` so the receiving transcript explains
22+
where the message came from.
23+
24+
Use the `berd-monitor` skill for external waits associated with delegated
25+
work. End the turn after starting a monitor; do not poll in the foreground.
26+
27+
Stay quiet while work is progressing. When attention is needed, either
28+
continue an obvious next step or bring the user one result or decision with a
29+
`[session link](berd://session/<id>)`.

justfile

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -498,16 +498,18 @@ dev:
498498
export VITE_APP_VERSION="$BERD_APP_VERSION_RICH"
499499
echo "Using app version: ${BERD_APP_VERSION} (${BERD_APP_VERSION_RICH})"
500500
501-
# tauri dev only builds the root package; the berdctl CLI workspace
502-
# member needs an explicit build, resolved at runtime via BERDCTL_BIN
503-
# because tauri.dev.conf.json blanks externalBin.
501+
# tauri dev only builds the root package; the agent-facing CLI workspace
502+
# members need explicit builds because tauri.dev.conf.json blanks externalBin.
504503
BERDCTL_FEATURES=()
505504
[[ "${VITE_FEEDBACK:-0}" == "1" ]] && BERDCTL_FEATURES+=(--features block-feedback)
506505
# ${arr[@]+...} guards the empty-array expansion, which bash 3.2 (stock
507506
# macOS) treats as an unbound variable under `set -u`.
508507
(cd src-tauri && cargo build -p berdctl ${BERDCTL_FEATURES[@]+"${BERDCTL_FEATURES[@]}"})
508+
(cd src-tauri && cargo build -p berd-monitor)
509509
export BERDCTL_BIN="${CARGO_TARGET_DIR}/debug/berdctl"
510+
export BERD_MONITOR_BIN="${CARGO_TARGET_DIR}/debug/berd-monitor"
510511
echo "Using berdctl CLI: ${BERDCTL_BIN}"
512+
echo "Using berd-monitor CLI: ${BERD_MONITOR_BIN}"
511513
512514
if [[ "${VITE_AGENT_TOOLS:-0}" == "1" ]]; then
513515
./scripts/prepare-bb-cli-resource.sh

scripts/prepare-berdctl-sidecar.sh

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env bash
2-
# Build and stage the berdctl CLI for Tauri's externalBin bundling.
2+
# Build and stage the berdctl and berd-monitor CLIs for Tauri externalBin bundling.
33
#
44
# Tauri expects external binaries to be present at build time with the target
55
# triple appended to the configured stem. For config
@@ -13,8 +13,9 @@ usage() {
1313
cat <<'USAGE'
1414
Usage: scripts/prepare-berdctl-sidecar.sh [target-triple]
1515
16-
Builds the berdctl workspace crate in release mode and copies the binary
17-
into src-tauri/binaries with the target triple suffix required by Tauri.
16+
Builds the berdctl and berd-monitor workspace crates in release mode and
17+
copies both binaries into src-tauri/binaries with the target triple suffix
18+
required by Tauri.
1819
1920
The triple defaults to the rustc host. Pass it explicitly (or set
2021
BERDCTL_TRIPLE) when the Tauri build itself uses an explicit --target, so
@@ -29,9 +30,9 @@ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
2930
fi
3031

3132
EXPLICIT_TRIPLE="${1:-${BERDCTL_TRIPLE:-}}"
32-
CARGO_ARGS=(build -p berdctl --release)
33+
CARGO_ARGS=(build -p berdctl -p berd-monitor --release)
3334
if [[ "${VITE_FEEDBACK:-0}" == "1" ]]; then
34-
CARGO_ARGS+=(--features block-feedback)
35+
CARGO_ARGS+=(--features berdctl/block-feedback)
3536
fi
3637
if [[ -n "$EXPLICIT_TRIPLE" ]]; then
3738
TRIPLE="$EXPLICIT_TRIPLE"
@@ -75,3 +76,19 @@ mkdir -p "$OUT_DIR"
7576
cp "$BUILT" "$OUT"
7677
chmod +x "$OUT"
7778
echo "Staged berdctl sidecar: $OUT"
79+
80+
if [[ -n "$EXPLICIT_TRIPLE" ]]; then
81+
MONITOR_BUILT="$TARGET_DIR/$TRIPLE/release/berd-monitor"
82+
else
83+
MONITOR_BUILT="$TARGET_DIR/release/berd-monitor"
84+
fi
85+
86+
if [[ ! -x "$MONITOR_BUILT" ]]; then
87+
echo "Built berd-monitor binary not found at: $MONITOR_BUILT" >&2
88+
exit 1
89+
fi
90+
91+
MONITOR_OUT="$OUT_DIR/berd-monitor-$TRIPLE"
92+
cp "$MONITOR_BUILT" "$MONITOR_OUT"
93+
chmod +x "$MONITOR_OUT"
94+
echo "Staged berd-monitor sidecar: $MONITOR_OUT"

scripts/windows/Dev-Windows.ps1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ if (-not (Test-Path $env:BERDCTL_BIN -PathType Leaf)) {
124124
}
125125
Write-WindowsDevInfo "Using berdctl CLI: $env:BERDCTL_BIN"
126126

127+
Invoke-CheckedCommand -FilePath "cargo" -ArgumentList @("build", "-p", "berd-monitor") -WorkingDirectory (Join-Path (Get-BerdRepoRoot) "src-tauri") -Label "cargo build berd-monitor"
128+
$env:BERD_MONITOR_BIN = Join-Path (Join-Path $env:CARGO_TARGET_DIR "debug") "berd-monitor.exe"
129+
if (-not (Test-Path $env:BERD_MONITOR_BIN -PathType Leaf)) {
130+
throw "Expected berd-monitor.exe at $env:BERD_MONITOR_BIN after cargo build."
131+
}
132+
Write-WindowsDevInfo "Using berd-monitor CLI: $env:BERD_MONITOR_BIN"
133+
127134
if ([string]::IsNullOrWhiteSpace($env:GOOSE_BIN)) {
128135
$env:GOOSE_BUILD_PROFILE = "debug"
129136
$result = Invoke-EnsureLocalGoose -Action Check

scripts/windows/Stage-Sidecar-Windows.ps1

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ Write-WindowsDevInfo "Staged Goose sidecar: $staged"
7171
$tauriTargetDir = Get-TauriCargoTargetDir
7272
$env:CARGO_TARGET_DIR = $tauriTargetDir
7373
$hostTriple = Get-RustHostTriple
74-
$cargoArgs = @("build", "-p", "berdctl", "--release")
74+
$cargoArgs = @("build", "-p", "berdctl", "-p", "berd-monitor", "--release")
7575
if ($env:VITE_FEEDBACK -eq "1") {
76-
$cargoArgs += @("--features", "block-feedback")
76+
$cargoArgs += @("--features", "berdctl/block-feedback")
7777
}
7878
if (-not [string]::IsNullOrWhiteSpace($hostTriple) -and $Triple -ne $hostTriple) {
7979
$cargoArgs += @("--target", $Triple)
@@ -82,10 +82,15 @@ if (-not [string]::IsNullOrWhiteSpace($hostTriple) -and $Triple -ne $hostTriple)
8282
$berdctlReleaseDir = Join-Path $tauriTargetDir "release"
8383
}
8484
Invoke-CheckedCommand -FilePath "cargo" -ArgumentList $cargoArgs `
85-
-WorkingDirectory (Join-Path (Get-BerdRepoRoot) "src-tauri") -Label "cargo build -p berdctl --release"
85+
-WorkingDirectory (Join-Path (Get-BerdRepoRoot) "src-tauri") -Label "cargo build -p berdctl -p berd-monitor --release"
8686
$berdctlSource = Join-Path $berdctlReleaseDir (Get-WindowsExeName "berdctl")
8787
$staged = Stage-WindowsSidecar -SourcePath $berdctlSource -Triple $Triple -Stem "berdctl" -BinDir $binDir
8888
Write-WindowsDevInfo "Staged berdctl sidecar: $staged"
8989

90+
# ── berd-monitor ─────────────────────────────────────────────
91+
$monitorSource = Join-Path $berdctlReleaseDir (Get-WindowsExeName "berd-monitor")
92+
$staged = Stage-WindowsSidecar -SourcePath $monitorSource -Triple $Triple -Stem "berd-monitor" -BinDir $binDir
93+
Write-WindowsDevInfo "Staged berd-monitor sidecar: $staged"
94+
9095
# Catch is deliberately not staged on Windows (see header).
9196
Write-WindowsDevInfo "Skipping Catch sidecar: unsupported on Windows (excluded from externalBin)."

scripts/windows/Test-WindowsDev.ps1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ try {
439439
$windowsExternalBin = @(Get-ObjectValue (Get-ObjectValue $windowsConf "bundle") "externalBin")
440440
Assert-Equal "Windows externalBin stages goosed" ($windowsExternalBin -contains "binaries/goosed") $true
441441
Assert-Equal "Windows externalBin stages berdctl" ($windowsExternalBin -contains "binaries/berdctl") $true
442+
Assert-Equal "Windows externalBin stages berd-monitor" ($windowsExternalBin -contains "binaries/berd-monitor") $true
442443
Assert-Equal "Windows externalBin excludes catch" ($windowsExternalBin -contains "binaries/catch") $false
443444

444445
# Tauri merges platform overlays into the base config with json_patch (RFC
@@ -454,6 +455,7 @@ try {
454455
$mergedExternalBin = if ($null -ne $windowsExternalBin) { $windowsExternalBin } else { $baseExternalBin }
455456
Assert-Equal "merged Windows externalBin stages goosed" ($mergedExternalBin -contains "binaries/goosed") $true
456457
Assert-Equal "merged Windows externalBin stages berdctl" ($mergedExternalBin -contains "binaries/berdctl") $true
458+
Assert-Equal "merged Windows externalBin stages berd-monitor" ($mergedExternalBin -contains "binaries/berd-monitor") $true
457459
Assert-Equal "merged Windows externalBin drops catch" ($mergedExternalBin -contains "binaries/catch") $false
458460

459461
# ── Windows bundle recipes route through native staging ──────

src-tauri/Cargo.lock

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ crate-type = ["staticlib", "cdylib", "rlib"]
1414
# <plugin> --features ...` only works for workspace members. app-test-driver
1515
# stays excluded (a plain path dependency, as before this workspace existed).
1616
[workspace]
17-
members = ["crates/berd-voice", "crates/berdctl", "plugins/berdctl"]
17+
members = [
18+
"crates/berd-monitor",
19+
"crates/berd-voice",
20+
"crates/berdctl",
21+
"plugins/berdctl",
22+
]
1823
exclude = ["plugins/app-test-driver"]
1924

2025
[build-dependencies]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "berd-monitor"
3+
version = "0.6.2"
4+
description = "Detached command monitor for Berd sessions"
5+
authors = ["Block, Inc."]
6+
edition = "2021"
7+
8+
[dependencies]
9+
clap = { version = "4", features = ["derive", "env"] }
10+
fs2 = "0.4"
11+
12+
[target.'cfg(unix)'.dependencies]
13+
libc = "0.2"
14+
15+
[target.'cfg(windows)'.dependencies]
16+
windows-sys = { version = "0.59", features = [
17+
"Win32_Foundation",
18+
"Win32_Security",
19+
"Win32_Storage_FileSystem",
20+
"Win32_System_Diagnostics_ToolHelp",
21+
"Win32_System_JobObjects",
22+
"Win32_System_Threading",
23+
] }

0 commit comments

Comments
 (0)