Part 6 · Agents & Protocol IP · Intermediate
Agent Wrapper Role: Contract First, Internals Second
Define what the wrapper owns and exposes, enforce encapsulation, and keep public APIs stable while internal classes evolve.
Wrapper as integration contract
The wrapper should expose only the surfaces consumers need: start traffic through sequencer, observe traffic through analysis ports, and set behavior through cfg.
[AGT] public contract
publish:
- sequencer handle (active usage)
- agent-level analysis port(s)
- cfg summary/introspection APIs
hide:
- monitor internal sampling logic
- driver protocol micro-steps
- internal helper queues/state[UVM][AGT] boundary payoff
stable API:
env/test code remains unchanged across refactors
hidden internals:
monitor/driver implementation can evolve independentlyExpose capabilities, not implementation details.
Treat wrapper API as versioned contract for VIP users.
Hide child internals unless explicit extension point is required.
Canonical wrapper implementation
class can_agent extends uvm_agent;
`uvm_component_utils(can_agent)
// public surface
can_sequencer sqr;
uvm_analysis_port #(can_item) ap;
can_cfg cfg;
// private internals
local can_driver drv;
local can_monitor mon;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(can_cfg)::get(this, "", "cfg", cfg))
`uvm_fatal("NOCFG", "missing can_cfg")
ap = new("ap", this);
mon = can_monitor::type_id::create("mon", this);
uvm_config_db#(can_cfg)::set(this, "mon", "cfg", cfg);
if (cfg.is_active == UVM_ACTIVE) begin
sqr = can_sequencer::type_id::create("sqr", this);
drv = can_driver::type_id::create("drv", this);
uvm_config_db#(can_cfg)::set(this, "drv", "cfg", cfg);
end
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
mon.ap.connect(ap);
if (cfg.is_active == UVM_ACTIVE)
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclass[AGT] integration usage
tests:
seq.start(env.can_agt.sqr)
checkers:
env.can_agt.ap.connect(sb.actual_in)
no direct env/test dependence on env.can_agt.mon internalsUse factory create for all child components.
Re-export monitor stream via wrapper-level analysis ports.
Keep mode handling centralized in wrapper build logic.
Extension without leakage
Encapsulation still allows extension through factory overrides and documented wrapper hooks. The key is to avoid exposing raw child handles as external dependencies.
class trace_can_monitor extends can_monitor;
`uvm_component_utils(trace_can_monitor)
virtual function void emit_trace(can_item tr);
`uvm_info("CAN_TRACE", tr.convert2string(), UVM_MEDIUM)
endfunction
endclass
initial begin
uvm_factory::get().set_type_override_by_type(
can_monitor::get_type(),
trace_can_monitor::get_type()
);
end[UVM][AGT] extension strategy
override children via factory
▼
wrapper public API unchanged
▼
existing tests/env wiring remain stableKey takeaways
Wrapper role is to define and protect the integration contract.
Encapsulation enables safer refactors and controlled extension.
Public API should stay minimal: cfg, sequencer path, analysis path.
Factory overrides work best with stable wrapper boundaries.
Common pitfalls
Publishing driver/monitor internals as external dependencies.
Embedding test-specific behavior directly into wrapper core code.
Bypassing wrapper analysis export and wiring consumers to hidden ports.
Using global mutable state instead of cfg-forwarded behavior.