Skip to content

Commit bffe605

Browse files
committed
fix(agentdoc): hide skill lifecycle hooks
Signed-off-by: Paul Furgale <pfurgale@nvidia.com>
1 parent 6ffbf60 commit bffe605

3 files changed

Lines changed: 65 additions & 0 deletions

File tree

src/nooa/agentdoc/_structured.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,12 +1516,17 @@ def _extract_methods(obj: type) -> list[CallableInfo]:
15161516

15171517
methods = []
15181518
seen_names = set()
1519+
always_hidden_names = {
1520+
name for klass in obj.__mro__ for name in getattr(klass, "__agentdoc_hidden_methods__", ())
1521+
}
15191522

15201523
# Walk the MRO dictionaries directly instead of inspect.getmembers().
15211524
# inspect.getmembers() calls getattr() for every name and can execute arbitrary
15221525
# descriptors while formatting an object.
15231526
for klass in obj.__mro__:
15241527
for name, raw in vars(klass).items():
1528+
if name in always_hidden_names:
1529+
continue
15251530
value = raw
15261531
if isinstance(raw, (classmethod, staticmethod)):
15271532
value = raw.__func__
@@ -1556,6 +1561,8 @@ def _extract_methods(obj: type) -> list[CallableInfo]:
15561561
except TypeError:
15571562
fields_meta = {}
15581563
for name, meta in fields_meta.items():
1564+
if name in always_hidden_names:
1565+
continue
15591566
if meta.get("hidden") is not False:
15601567
continue
15611568
if name in seen_names:

src/nooa/skill.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,10 @@ class MyAgent(Agent, llm=llm):
271271
# Skills show a brief one-liner wherever they appear as a field type.
272272
__agentdoc_skip__ = True
273273

274+
# Lifecycle hooks are framework plumbing, never model-callable capabilities.
275+
# The marker is inherited so overrides remain hidden for every Skill subclass.
276+
__agentdoc_hidden_methods__ = frozenset({"attach", "detach"})
277+
274278
# Skills are reconstructed on agent init (e.g., LibraryManager rescans libs/).
275279
# They shouldn't be serialized in snapshots — mark them as nosnapshot.
276280
__nosnapshot__ = True
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Skill lifecycle hooks are framework plumbing, not model-facing tools."""
4+
5+
from typing import Any
6+
7+
from nooa.agentdoc import doc, spec
8+
from nooa.skill import Skill
9+
10+
11+
class ExampleSkill(Skill):
12+
"""A useful example skill."""
13+
14+
def useful(self, value: str) -> str:
15+
"""Return a useful value."""
16+
return value
17+
18+
19+
class OverridingSkill(ExampleSkill):
20+
"""A skill that customizes its lifecycle hooks."""
21+
22+
def attach(self, agent: Any) -> None:
23+
self.agent = agent
24+
25+
def detach(self) -> None:
26+
self.agent = None
27+
28+
29+
def test_skill_lifecycle_hooks_are_hidden_from_docs():
30+
rendered = doc(ExampleSkill)
31+
32+
assert "def useful(" in rendered
33+
assert "def attach(" not in rendered
34+
assert "def detach(" not in rendered
35+
36+
37+
def test_overridden_skill_lifecycle_hooks_remain_hidden_from_docs():
38+
rendered = doc(OverridingSkill)
39+
40+
assert "def useful(" in rendered
41+
assert "def attach(" not in rendered
42+
assert "def detach(" not in rendered
43+
44+
45+
def test_skill_lifecycle_hooks_cannot_be_explicitly_unhidden():
46+
class ExplicitlyUnhiddenSkill(Skill):
47+
def attach(self, agent: Any) -> None:
48+
self.agent = agent
49+
50+
spec(ExplicitlyUnhiddenSkill, "attach", hidden=False)
51+
52+
rendered = doc(ExplicitlyUnhiddenSkill())
53+
assert "def attach(" not in rendered
54+
assert "def detach(" not in rendered

0 commit comments

Comments
 (0)