Skip to content
Closed
6 changes: 5 additions & 1 deletion src/agentsight/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ fn main() {

// Generate tcpsniff skeleton (no header — reuses sslsniff.h event format)
generate_skeleton(&mut out, "tcpsniff");


// Generate schedmon skeleton and bindings
generate_skeleton(&mut out, "schedmon");
generate_header(&mut out, "schedmon");

// generate_header(&mut out, "frametypes");
// generate_header(&mut out, "errors");
// generate_header(&mut out, "stackdeltatypes");
Expand Down
5 changes: 5 additions & 0 deletions src/agentsight/src/bin/cli/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ pub struct TraceCommand {
#[structopt(long)]
pub enable_filewatch: bool,

/// Enable scheduler (idle-burst-idle cgroup CPU weight management)
#[structopt(long)]
pub enable_scheduler: bool,

/// Path to JSON configuration file
#[structopt(short, long, default_value = "/etc/agentsight/config.json")]
pub config: String,
Expand Down Expand Up @@ -72,6 +76,7 @@ impl TraceCommand {
let config = AgentsightConfig::new()
.set_verbose(self.verbose)
.set_enable_filewatch(self.enable_filewatch)
.set_enable_scheduler(self.enable_scheduler)
.set_config_path(std::path::PathBuf::from(&self.config));

// Create AgentSight (auto-attaches probes and starts polling)
Expand Down
1 change: 1 addition & 0 deletions src/agentsight/src/bpf/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ typedef enum {
EVENT_SOURCE_FILEWATCH = 4, // File watch events (filewatch)
EVENT_SOURCE_FILEWRITE = 5, // File write events (filewrite)
EVENT_SOURCE_UDPDNS = 6, // UDP DNS query events (udpdns)
EVENT_SOURCE_SCHED = 7, // Scheduler state events (schedmon)
} event_source_t;

// Common event header - every ringbuffer event MUST start with this
Expand Down
4 changes: 2 additions & 2 deletions src/agentsight/src/bpf/procmon.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ int trace_execve_exit(struct trace_event_raw_sys_exit *ctx)
// Fill event
event->source = EVENT_SOURCE_PROCMON;
event->timestamp_ns = ts;
event->pid = get_task_ns_pid(task);
event->pid = pid;
event->tid = tid;
event->ppid = ppid;
event->uid = uid;
Expand Down Expand Up @@ -78,7 +78,7 @@ int trace_process_exit(void *ctx)
// Fill event
event->source = EVENT_SOURCE_PROCMON;
event->timestamp_ns = ts;
event->pid = current_ns_pid();
event->pid = pid;
event->tid = tid;
event->ppid = 0;
event->uid = uid;
Expand Down
115 changes: 115 additions & 0 deletions src/agentsight/src/bpf/schedmon.bpf.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
// Copyright (c) 2025 AgentSight Project
//
// Scheduler monitor BPF program
// Detects idle/active state transitions for traced Agent processes via the
// BTF-typed sched_switch / sched_wakeup tracepoints.
//
// Using tp_btf (rather than the format-struct tracepoints) gives direct typed
// access to the task_struct, which lets us:
// 1. read the *raw* prev->__state (TASK_RUNNING == 0) and the `preempt` flag,
// so a task that is merely preempted while still runnable is NOT treated as
// going to sleep — only a genuinely blocking task emits SLEEP;
// 2. filter by tgid (thread-group id) so we only watch traced Agent families,
// but emit per-tid (thread id), so userspace can tell a family is ACTIVE
// while ANY of its threads is runnable (a multithreaded process commonly has
// some threads sleeping while others run).
//
// Events are rate-limited per-tid to avoid flooding the shared ring buffer.
#include "vmlinux.h"
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "schedmon.h"
#include "common.h"

struct pid_sched_state {
u8 last_state; // sched_event_type
};

// Per-tid last emitted state, used to deduplicate consecutive same-state events
// (a running thread does not re-emit WAKEUP). We deliberately do NOT apply a
// time-based cooldown: a thread can legitimately wake and immediately block again
// (WAKEUP then SLEEP within microseconds), and dropping that SLEEP would strand
// the thread in the userspace runnable set, pinning its family ACTIVE forever.
// LRU so it self-evicts (schedmon has no thread-exit hook) and update never fails.
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, MAX_TRACED_PROCESSES);
__type(key, u32);
__type(value, struct pid_sched_state);
} pid_sched_state SEC(".maps");

static __always_inline int
emit_sched_event(u32 tgid, u32 tid, u64 now, u8 event_type)
{
struct pid_sched_state *st = bpf_map_lookup_elem(&pid_sched_state, &tid);
if (st) {
if (st->last_state == event_type)
return 0;
st->last_state = event_type;
} else {
struct pid_sched_state new_st = {
.last_state = event_type,
};
bpf_map_update_elem(&pid_sched_state, &tid, &new_st, BPF_ANY);
}

struct sched_event *ev = bpf_ringbuf_reserve(&rb, sizeof(*ev), 0);
if (!ev)
return 0;

ev->source = EVENT_SOURCE_SCHED;
ev->tgid = tgid;
ev->tid = tid;
ev->timestamp_ns = now;
ev->event_type = event_type;
ev->pad[0] = 0;
ev->pad[1] = 0;
ev->pad[2] = 0;

bpf_ringbuf_submit(ev, 0);
return 0;
}

// sched_switch: prev (one thread) is going off-CPU.
// Emit SLEEP only when prev is actually blocking (not merely preempted while
// still runnable). Filtered by tgid, emitted per-tid.
SEC("tp_btf/sched_switch")
int BPF_PROG(handle_sched_switch, bool preempt, struct task_struct *prev,
struct task_struct *next)
{
// Preempted tasks stay runnable — they are not going to sleep.
if (preempt)
return 0;

// prev->__state has already been set to the target state by the scheduler.
// TASK_RUNNING (0) means it is still runnable (e.g. yielded); skip it.
unsigned int state = BPF_CORE_READ(prev, __state);
if (state == 0)
return 0;

u32 tgid = BPF_CORE_READ(prev, tgid);
u32 *traced = bpf_map_lookup_elem(&traced_processes, &tgid);
if (!traced)
return 0;

u32 tid = BPF_CORE_READ(prev, pid);
return emit_sched_event(tgid, tid, bpf_ktime_get_ns(), SCHED_EVENT_SLEEP);
}

// sched_wakeup: task p (one thread) is being woken up. Any thread waking makes
// its family ACTIVE. Filtered by tgid, emitted per-tid.
SEC("tp_btf/sched_wakeup")
int BPF_PROG(handle_sched_wakeup, struct task_struct *p)
{
u32 tgid = BPF_CORE_READ(p, tgid);
u32 *traced = bpf_map_lookup_elem(&traced_processes, &tgid);
if (!traced)
return 0;

u32 tid = BPF_CORE_READ(p, pid);
return emit_sched_event(tgid, tid, bpf_ktime_get_ns(), SCHED_EVENT_WAKEUP);
}

char LICENSE[] SEC("license") = "GPL";
35 changes: 35 additions & 0 deletions src/agentsight/src/bpf/schedmon.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
// Copyright (c) 2025 AgentSight Project
//
// Scheduler monitor BPF program header
// Detects idle/active state transitions for traced Agent processes
#ifndef __SCHEDMON_H
#define __SCHEDMON_H

#define TASK_COMM_LEN 16

typedef signed char s8;
typedef unsigned char u8;
typedef signed short s16;
typedef unsigned short u16;
typedef signed int s32;
typedef unsigned int u32;
typedef signed long long s64;
typedef unsigned long long u64;

enum sched_event_type {
SCHED_EVENT_SLEEP = 1,
SCHED_EVENT_WAKEUP = 2,
};

struct sched_event {
u32 source; // EVENT_SOURCE_SCHED
u32 tgid; // thread-group id — identifies the Agent family
u32 tid; // thread id — tracked individually so a family is ACTIVE
// while ANY of its threads is runnable
u64 timestamp_ns;
u8 event_type; // enum sched_event_type
u8 pad[3];
};

#endif /* __SCHEDMON_H */
41 changes: 41 additions & 0 deletions src/agentsight/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ struct JsonFullConfig {
encryption: Option<JsonEncryption>,
#[serde(default)]
runtime: Option<JsonRuntime>,
#[serde(default)]
scheduler: Option<JsonScheduler>,
}

/// Runtime 动态配置区段(支持热加载,无需重启)
Expand All @@ -245,6 +247,15 @@ pub struct JsonRuntime {
pub sls_logtail_path: Option<String>,
}

/// Activity monitor config (idle/active detection for observability)
#[derive(serde::Deserialize)]
struct JsonScheduler {
#[serde(default)]
enabled: Option<bool>,
#[serde(default)]
idle_threshold_ms: Option<u64>,
}

/// 加密配置:可选公钥(PEM 字符串)或公钥文件路径
#[derive(serde::Deserialize)]
struct JsonEncryption {
Expand Down Expand Up @@ -418,6 +429,10 @@ pub struct AgentsightConfig {
pub poll_timeout_ms: u64,
/// Enable file watch probe (monitors .jsonl file opens from traced processes)
pub enable_filewatch: bool,
/// Enable activity monitor (agent idle/active state detection via schedmon BPF)
pub enable_scheduler: bool,
/// Activity monitor configuration
pub activity_config: crate::scheduler::ActivityConfig,
/// TCP capture targets for plain HTTP capture (empty = disabled).
/// Each entry specifies destination IP, port, or both.
pub tcp_targets: Vec<TcpTarget>,
Expand Down Expand Up @@ -490,6 +505,8 @@ impl Default for AgentsightConfig {
target_uid: None,
poll_timeout_ms: DEFAULT_POLL_TIMEOUT_MS,
enable_filewatch: false,
enable_scheduler: false,
activity_config: crate::scheduler::ActivityConfig::default(),
tcp_targets: Vec::new(),

// HTTP/Aggregation defaults
Expand Down Expand Up @@ -580,6 +597,13 @@ impl AgentsightConfig {
self
}

/// Set enable_scheduler (activity monitor)
pub fn set_enable_scheduler(mut self, enable: bool) -> Self {
self.enable_scheduler = enable;
self.activity_config.enabled = enable;
self
}

/// Set connection capacity
pub fn set_connection_capacity(mut self, capacity: usize) -> Self {
self.connection_capacity = capacity;
Expand Down Expand Up @@ -643,6 +667,23 @@ impl AgentsightConfig {
}
}

// Load scheduler config
if let Some(sched) = parsed.scheduler.take() {
if let Some(enabled) = sched.enabled {
if enabled != self.enable_scheduler {
log::warn!(
"config scheduler.enabled={} overrides --enable-scheduler={}",
enabled, self.enable_scheduler
);
}
self.enable_scheduler = enabled;
self.activity_config.enabled = enabled;
}
if let Some(t) = sched.idle_threshold_ms {
self.activity_config.idle_threshold_ms = t;
}
}

let (cmdline_rules, https_rules, http_targets) = extract_rules(&parsed);
self.cmdline_rules.extend(cmdline_rules);
self.https_rules.extend(https_rules);
Expand Down
23 changes: 23 additions & 0 deletions src/agentsight/src/discovery/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,29 @@ impl AgentScanner {
}
}

/// Read the parent PID of a process from /proc/[pid]/stat
pub fn read_ppid(pid: u32) -> Option<u32> {
let data = fs::read_to_string(format!("/proc/{}/stat", pid)).ok()?;
// Format: pid (comm) state ppid ... — ppid is the 4th field.
// comm may contain spaces/parens, so find the closing ')' first.
let after_comm = data.rsplit_once(')')?.1;
let ppid_str = after_comm.split_whitespace().nth(1)?;
ppid_str.parse().ok()
}

/// Check if a process has AGENT_MODE=1 in its environment
///
/// Reads /proc/[pid]/environ and looks for the AGENT_MODE=1 variable.
pub fn has_agent_mode(pid: u32) -> bool {
let path = format!("/proc/{}/environ", pid);
match fs::read(&path) {
Ok(data) => data
.split(|&b| b == 0)
.any(|entry| entry == b"AGENT_MODE=1"),
Err(_) => false,
}
}

/// Read and parse cmdline file
///
/// The cmdline file contains arguments separated by null bytes.
Expand Down
16 changes: 16 additions & 0 deletions src/agentsight/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::probes::procmon::Event as ProcMonEvent;
use crate::probes::filewatch::FileWatchEvent;
use crate::probes::filewrite::FileWriteEvent;
use crate::probes::udpdns::UdpDnsEvent;
use crate::probes::schedmon::SchedEvent;

/// Unified event type that can represent any probe event
///
Expand All @@ -16,6 +17,7 @@ pub enum Event {
FileWatch(FileWatchEvent),
FileWrite(FileWriteEvent),
UdpDns(UdpDnsEvent),
Sched(SchedEvent),
}

impl Event {
Expand All @@ -28,6 +30,7 @@ impl Event {
Event::FileWatch(_) => "FileWatch",
Event::FileWrite(_) => "FileWrite",
Event::UdpDns(_) => "UdpDns",
Event::Sched(_) => "Sched",
}
}
}
Expand Down Expand Up @@ -110,6 +113,19 @@ impl Event {
_ => None,
}
}

/// Check if this is a scheduler state event
pub fn is_sched(&self) -> bool {
matches!(self, Event::Sched(_))
}

/// Get scheduler event if this is one
pub fn as_sched(&self) -> Option<&SchedEvent> {
match self {
Event::Sched(e) => Some(e),
_ => None,
}
}
}

#[cfg(test)]
Expand Down
2 changes: 2 additions & 0 deletions src/agentsight/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,8 @@ fn ffi_background_thread(
if sight.try_process().is_none() {
// No event available — flush any timed-out pending GenAI events
sight.flush_expired_pending_genai();
// Finalize debounced scheduler idle transitions (same as run()).
sight.on_idle_tick();
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/agentsight/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ pub mod storage;
pub mod chrome_trace;
pub mod discovery;
pub mod health;
pub mod lineage;
pub mod tokenizer;
pub mod genai;
pub mod atif;
pub mod response_map;
pub mod interruption;
pub mod scheduler;
pub mod skill_metrics;
pub mod utils;
#[cfg(feature = "server")]
Expand Down
Loading
Loading