@sema-agent/core
Version:
Stateless, task-oriented AI agent core
88 lines (87 loc) • 6.61 kB
TypeScript
/**
* design/135 G2 (审计 🔴) — the `Monitor` tool: run a shell command in the background and watch its
* stdout as a stream of NOTIFICATION EVENTS (the自主循环 core piece: "watch this and tell me when something
* happens" without burning poll turns).
*
* CC-parity semantics (live-harness schema, the archived ground truth):
* ① every stdout LINE becomes one event; ② lines within the 200ms batch window coalesce into ONE
* notification; ③ process exit ends the watch (exit code reported); ④ non-persistent watches are killed at
* `timeout_ms` (default 5min, max 60min); ⑤ stderr never notifies but is spooled (TaskOutput-readable);
* ⑥ an event storm is SUPPRESSED first and auto-stops the monitor only if it persists (RB-334, CC `PCo`
* 220:403278-403321 two-level control — the suppressed count and the overload duration are disclosed
* on both the suppression frame and the stop notification).
*
* Wiring: the process runs through the SAME ExecutionEnv background seam as Bash(run_in_background)
* (`spawnBackground` — remote envs included); the watcher/batching/timeout machinery lives in
* {@link TaskRegistry.registerMonitor} (background_bash G2b watcher's near kin); events ride the
* TaskNotificationPayload lane at "next" priority (design/373 #445 ALIGNED re-seat: the running turn's
* next boundary — consecutive frames drain as one boundary batch, the rate limiter + engine-note cap
* with terminal preference keep a chatty watcher from monopolizing the run's boundaries).
* (1.257): events born BETWEEN turns (run torn down / harness idle — the long-watch main case)
* are no longer lost: the Runner parks them per session (bounded, drop-disclosing) and the session's next
* run redelivers them through the same notification lane at its first turn boundary.
* (CC parity): a monitor's lifetime = min(timeout_ms, process exit, TaskStop); a `persistent`
* one is SESSION-anchored (reaped at session release). NEITHER is run-anchored: the run teardown leaves an
* in-window non-persistent watch alive (process on the dispose except-list, watcher ticking, row kept), so
* cross-turn watching — CC's main Monitor use case — works with the default persistent:false too; its
* between-turns events AND its timeout/exit terminal ride the same pending lane.
* The handle is a first-class m* registry task: TaskOutput serves the re-readable spool, TaskStop stops the
* watch (design/134 stoppedBy attribution applies automatically).
*
* Security posture: `command` is an arbitrary shell script ⇒ effect:"write", same as Bash; the catalog puts Monitor on the
* SHELL FACE, so the `Bash(<command>)` rules, the shell seat, the read station and the removal guard judge its `command`
* as they judge Bash's — Monitor is never a policy side-door around a gated Bash.
*
* 深对比残差 M17 CLOSED (2026-07-09 live-CC probe, docs/cc-probe-198/live-schema-monitor-worktree-2026-07-09.json):
* the deep-dive bundle showed a `command? + ws?:{url,protocols}` dual-mode schema; the LIVE current-CC schema is
* shell-single-mode ({command, description, persistent, timeout_ms} — no ws branch anywhere in desc or schema).
* sema's shell-single-mode Monitor is therefore aligned with current CC — do NOT port a WebSocket event-source
* branch off the stale bundle. (Full-desc density diff vs the live guidance card remains P1, a separate item.)
*/
import type { AgentTool, ExecutionEnv } from "../internal/harness-types.js";
import { type MonitorTimers, type TaskRegistry } from "../core/task-registry.js";
import type { TaskNotificationPayload } from "../core/task-notification.js";
import type { ToolResultStore } from "../core/tool-result-store.js";
import type { ShellReadAdmission } from "./fs/shell-read-check.js";
export interface MonitorToolOptions {
registry: TaskRegistry;
/** Fallbacks when the runtime ctx carries no identity (direct mounts) — same posture as createBashTool. */
owner?: string;
scope?: string;
/** design/129: the session id — a `persistent` monitor registers session-scoped under it (outlives the
* turn; reaped by the deployment's session release via `reapSessionBackground`). */
sessionId?: string;
/** The notification sink (the run-local task-notification lane). Without it the monitor still runs and
* spools — readable via TaskOutput — but no events reach the model (the tool reply says so honestly). */
onTaskNotification?: (n: TaskNotificationPayload, opts?: {
priority?: "now" | "next" | "later";
}) => void;
/** The tracked logical cwd (shared with the hands band) — the monitor spawns there, like Bash. */
cwdRef?: {
current: string;
};
/** design/87: injectable timers/clock for the watcher (tests drive ticks manually; default real timers). */
timers?: MonitorTimers;
/** Test/deployment knobs (defaults: 200ms window, 50 batches/min). */
batchWindowMs?: number;
maxBatchesPerMinute?: number;
/** design/158 S2 (RB-364 半): the offload store a rolled-off spool middle spills to (see
* task-registry-monitor.ts's spill helper). Absent ⇒ legal degrade, byte-identical rolling (no spill,
* same loss shape as before this design). */
toolResultStore?: ToolResultStore;
/** The gate's shell read station's pre-spawn ADMISSION — the SAME option `createBashTool` takes: this face
* spawns a shell command too, so what it runs is what the station judged, re-established before the watch
* spawns. Absent ⇒ no station judged this mount; nothing is re-established. */
shellReadAdmission?: ShellReadAdmission;
/** RB-465-b (HRD-PTL-3 残件): spec-level retain (`TaskSpec.retainBackgroundProcesses`) — the task lane of
* THE retain predicate (`retainsBackgroundProcesses`), OR-composed with the env capability. Read twice,
* by the same predicate: the persistent-watch receipt's wording, and the row's `retained` mark at
* registration (so the session reap and the exit-boundary hard face judge the row the way the receipt
* described it). */
retainBackgroundProcesses?: boolean;
/** The task root in the SAME canonical spelling `createBashTool` is built with, so a dead-cwd self-heal
* lands both tools on one directory. Absent ⇒ the diagnosis says it has no reset target, rather than
* naming the cwd "the task root" or promising a recovery it cannot make. */
rootCanonical?: string;
}
export declare function createMonitorTool(env: ExecutionEnv, opts: MonitorToolOptions): AgentTool;