Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
98f8ca2
fix(deepagents): accept absolute paths inside the shell workspace
AjayThorve Oct 27, 2026
8c69085
fix(mini-swe-agent): install the NVIDIA provider runtime dependency
AjayThorve Oct 27, 2026
c00da82
fix(nooa): install the NVIDIA provider runtime dependency
AjayThorve Oct 27, 2026
a8194f3
fix(pi): constrain the compatible SDK package set
AjayThorve Oct 27, 2026
ab457c6
fix(strands): declare output token reasoning inclusion
AjayThorve Oct 27, 2026
116712a
fix(harbor): separate provider selection from served model IDs
AjayThorve Oct 27, 2026
d3da47e
fix(adapters): constrain incompatible LiteLLM tool dispatch
AjayThorve Oct 27, 2026
de182fb
test(harbor): include the provider override in the options schema
AjayThorve Oct 27, 2026
868983a
fix(mini-swe-agent): select the NVIDIA LiteLLM dispatch provider
AjayThorve Oct 27, 2026
8bdf2ee
fix(qwen): retain container-aware Node memory limits
AjayThorve Oct 27, 2026
330e437
test: quote Deep Agents workspace paths on Windows
AjayThorve Oct 27, 2026
4374239
fix(deepagents): preserve file-tool path round trips
AjayThorve Oct 28, 2026
796c81c
fix(deepagents): retain tool-compatible paths on Windows
AjayThorve Oct 28, 2026
6305ffb
docs(kilo): clarify background process lifetime in Harbor
AjayThorve Oct 28, 2026
83c5628
fix: preserve Deep Agents inclusive invocation usage
AjayThorve Oct 28, 2026
d240782
fix: allow explicitly unpriced Mini-SWE model execution
AjayThorve Oct 28, 2026
e29e907
fix: forward adapter model settings through Harbor
AjayThorve Oct 28, 2026
f983b96
fix: route OpenHands NVIDIA models through LiteLLM NIM provider
AjayThorve Oct 28, 2026
6b04d4e
fix: report invocation-local Pi token usage
AjayThorve Oct 28, 2026
92e9232
fix: preserve Qwen inclusive invocation usage
AjayThorve Oct 28, 2026
37adab6
fix: request and qualify OpenClaw gateway usage observations
AjayThorve Oct 28, 2026
ee5d504
fix: preserve NOOA observed inclusive token totals
AjayThorve Oct 28, 2026
ff0bc13
test: align harness E2E accounting expectations
AjayThorve Oct 28, 2026
1be9c57
fix: preserve unknown OpenHands usage and pricing evidence
AjayThorve Oct 28, 2026
44330e2
fix: declare OpenHands inclusive prompt token accounting
AjayThorve Oct 28, 2026
ab26988
test: scope pinned NOOA SDK regression to supported platforms
AjayThorve Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .agents/skills/maintain-packaging/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ in parity. The task uses `fabric-agent-ctl` from the Python SDK.

Run `just adapter-catalog`, `just check-adapter-catalog`, and `uv build --wheel --sdist --out-dir dist sdk/python/nemo-fabric-adapter-catalog` when maintaining the bundle. `just set-version` refreshes it after stamping Python and TypeScript versions. Python tests and wheel builds reject stale bundles; wheels and source distributions must build independently of the adapter source tree. Keep these maintainer details out of the PyPI-facing package README.

### Pi SDK Compatibility

Keep Pi’s optional peer constraints, development pins, consumer install command,
and harness-install diagnostic aligned across all four SDK packages, including
`pi-codemode`. Verify imports in a fresh consumer install from packed artifacts;
the workspace lockfile can hide incompatible transitive SDK resolutions.

### Select Dependencies

Treat every direct dependency as a long-lived API, supply-chain, and licensing
Expand Down
32 changes: 25 additions & 7 deletions adapters/python/deepagents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,26 @@ provides only its fallback path; configure required variables explicitly and
use absolute executable paths when needed. The `execute` tool remains subject
to `tools.enabled` and `tools.blocked`, like all other Deep Agents built-ins.

File tools and shell commands use different absolute-path namespaces when
`virtual_mode=True`. For file tools, `/report.txt` resolves to
`<workspace>/report.txt`; in `execute`, the same path refers to `/report.txt` on
the adapter host. Shell commands start in `environment.workspace`, so use
workspace-relative paths, such as `cat report.txt`, for files surfaced by file
tools. Do not pass a file tool's virtual absolute path directly to `execute`.
On POSIX, with `local_shell`, file tools accept absolute paths inside
`environment.workspace` as aliases for workspace files. For example, with
workspace `/app`, both file-tool paths `/app/report.txt` and `/report.txt` refer
to `/app/report.txt` on disk. Shell commands use host paths and start in the
workspace, so `cat report.txt` and `cat /app/report.txt` read that file.

On POSIX, listings and searches return canonical physical paths inside the
workspace. These paths identify the same file in subsequent reads and edits,
even when a nested directory repeats the workspace prefix. For example, `/app/app/report.txt`
remains distinct from `/app/report.txt`; the relative file-tool path
`app/report.txt` also identifies the nested file.

On Windows, listings and searches retain workspace-rooted virtual paths, such
as `/nested/report.txt`. Use these paths with file tools; Deep Agents rejects
drive-prefixed paths such as `C:/workspace/nested/report.txt` before backend
resolution. Shell commands continue to use native host paths.

File-tool traversal and symlink-escape checks remain enabled. Paths outside the
workspace retain the existing virtual-root interpretation; they do not grant
access to host files.

Python middleware, `FilesystemPermission` objects, Python tool objects, and
precompiled `runnable` subagents are not exposed through `harness.settings`.
Expand Down Expand Up @@ -252,7 +266,11 @@ service.
The normalized result includes the final response, buffered messages and
per-step events, LangGraph thread id, token usage (and cost when the provider
reports it), and errors. Usage aggregates the current turn across the main agent
and any delegated subagents (streamed with `subgraphs=True`). Configuration and
and any delegated subagents (streamed with `subgraphs=True`). Input totals use
LangChain's inclusive convention, which already includes cached tokens; an
unavailable cache breakdown stays unknown. If an assistant message omits a token
counter, that invocation's corresponding total stays unknown rather than
reporting a partial sum. Configuration and
preflight failures (a missing credential, an absent `deepagents` package, or an
invalid MCP server) fail runtime start before an invocation is accepted.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,39 @@ def build_chat_model(
return ChatOpenAI(**_supported_kwargs(ChatOpenAI, kwargs)), model_name, base_url


def _local_shell_backend(**kwargs: Any) -> Any:
from deepagents.backends import LocalShellBackend

workspace = Path(kwargs["root_dir"]).absolute()

class WorkspaceLocalShellBackend(LocalShellBackend):
"""Accept the shell's workspace paths without weakening virtual file checks."""

def _to_virtual_path(self, path: Path) -> str:
# Listings and searches must return paths the resolver reads back
# unambiguously, even if a nested path repeats the workspace prefix.
virtual = super()._to_virtual_path(path)
# Deep Agents tools reject drive-prefixed paths. Windows virtual
# paths cannot collide with the drive-qualified physical aliases.
if self.cwd.drive:
return virtual
return str(self.cwd / virtual.lstrip("/"))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

def _resolve_path(self, key: str) -> Path:
path = Path(key)
if path.is_absolute():
for root in (workspace, self.cwd):
try:
relative = path.relative_to(root)
except ValueError:
continue
# Native resolution still rejects traversal and symlink escapes.
return super()._resolve_path("/" + relative.as_posix())
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
return super()._resolve_path(key)

return WorkspaceLocalShellBackend(**kwargs)


def resolve_backend(
runtime_context: RuntimeContext,
base_dir: str,
Expand All @@ -290,9 +323,7 @@ def resolve_backend(
root = Path(base_dir) / root

if backend_config is not None:
from deepagents.backends import LocalShellBackend

return LocalShellBackend(
return _local_shell_backend(
root_dir=str(root),
virtual_mode=True,
env=dict(runtime_context.environment.env),
Expand Down Expand Up @@ -838,7 +869,16 @@ async def _invoke(
else None
),
usage=(
AgentUsage(**tokens, cost_usd=cost)
# LangChain UsageMetadata input_tokens includes all input types,
# including cache. Preserve that convention for consumers such
# as Harbor without inventing a missing cache breakdown.
AgentUsage(
**tokens,
input_tokens_include_cache=(
True if "input_tokens" in tokens else None
),
cost_usd=cost,
)
if tokens or cost is not None
else None
),
Expand Down Expand Up @@ -1436,6 +1476,7 @@ def _final_response(messages: list[dict[str, Any]]) -> Any:

def _aggregate_usage(messages: list[dict[str, Any]]) -> dict[str, Any] | None:
totals: dict[str, Any] = {}
unknown: set[str] = set()
cost = 0.0
has_cost = False
for message in messages:
Expand All @@ -1452,6 +1493,10 @@ def _aggregate_usage(messages: list[dict[str, Any]]) -> dict[str, Any] | None:
and 0 <= value <= (1 << 64) - 1
):
totals[target] = int(totals.get(target, 0)) + value
elif message.get("role") in ("ai", "assistant"):
# One missing model response makes this dimension incomplete;
# do not promote the other responses' partial sum to a total.
unknown.add(target)
# Cost is not part of LangChain's UsageMetadata; surface it only when a
# model/provider reports it on the usage or response metadata.
candidate = (
Expand All @@ -1467,6 +1512,11 @@ def _aggregate_usage(messages: list[dict[str, Any]]) -> dict[str, Any] | None:
has_cost = True
if has_cost:
totals["cost"] = cost
for target in unknown:
totals.pop(target, None)
for target in ("prompt_tokens", "completion_tokens", "total_tokens"):
if totals.get(target, 0) > (1 << 64) - 1:
totals.pop(target)
return totals or None


Expand Down
28 changes: 27 additions & 1 deletion adapters/python/mini-swe-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ environment:
| `pip install nemo-fabric-adapters-mini-swe-agent` | No | Yes | No | No |

The `harness` and `full` extras install the latest compatible mini-SWE-agent
2.x release.
2.x release and constrain LiteLLM below 1.106. LiteLLM 1.106.4 and 1.106.5 import MCP types unavailable in MCP 1.x during ordinary tool dispatch. The tested 1.105.6 dispatch works without an MCP runtime. This constraint does not enable MCP tools in the adapter. For provider `nvidia`, the adapter adds LiteLLM’s `nvidia_nim/` dispatch prefix while preserving the configured served model ID, including its vendor prefix.

## Configuration

Expand All @@ -36,6 +36,32 @@ sampling fields through mini-SWE-agent's LiteLLM model arguments and rejects
`harness.settings.timeout` to set the maximum duration of one command; the
default is `30` seconds.

For a local or unpriced model, explicitly set
`models.<role>.settings={"cost_tracking": "disabled"}` and a positive
`runtime.max_turns`. The default is strict SDK pricing and its monetary budget;
ambient `MSWEA_COST_TRACKING` does not override it. Disabled tracking removes
the SDK monetary budget, so use it only when you accept execution without a
monetary cap. Turn and invocation time limits still apply. Cost remains
unavailable in Fabric output and Relay telemetry; the SDK's fallback zero is
not reported as measured cost. Provider token usage in Relay is preserved.

For example, these fields explicitly opt into an unpriced model:

```json
{
"models": {
"default": {
"provider": "nvidia",
"model": "nvidia/nemotron-3-ultra-550b-a55b",
"base_url": "http://localhost:8000/v1",
"settings": {"cost_tracking": "disabled"}
}
},
"runtime": {"max_turns": 50, "timeout_seconds": 1800}
}
```


Set `models.<role>.api_key_env` to the environment variable containing the
model-provider credential. This adapter does not support MCP, skills, tool
policy, or native OpenAI streaming.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@
"base_url": {"type": "string", "minLength": 1},
"settings": {
"type": "object",
"properties": {},
"properties": {
"cost_tracking": {
"type": "string",
"enum": ["default", "disabled"],
"default": "default",
"description": "Use strict SDK pricing, or explicitly disable monetary budgeting for unpriced models. Disabled requires a positive runtime.max_turns; cost remains unavailable."
}
},
"additionalProperties": false
}
},
Expand Down
4 changes: 4 additions & 0 deletions adapters/python/mini-swe-agent/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,17 @@ dependencies = [
harness = [
"fsspec>=2026.6.0",
"mini-swe-agent>=2.0,<3",
# LiteLLM 1.106 tool dispatch imports MCP types unavailable in MCP 1.x.
"litellm<1.106",
]
relay = [
"nemo-relay>=0.11,<0.12",
]
full = [
"fsspec>=2026.6.0",
"mini-swe-agent>=2.0,<3",
# LiteLLM 1.106 tool dispatch imports MCP types unavailable in MCP 1.x.
"litellm<1.106",
"nemo-relay>=0.11,<0.12",
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def __init__(self) -> None:
self._relay_plugin = self._relay_scope = self._relay_scope_type = None
self._relay_plugin_config: dict[str, Any] | None = None
self._telemetry_quarantine: str | None = None
self._cost_tracking = "default"

async def start(self, payload: dict[str, Any]) -> None:
config: contract.AgentConfig = payload["config"]
Expand Down Expand Up @@ -89,6 +90,27 @@ async def start(self, payload: dict[str, Any]) -> None:
agent_type = RelayRetainingDefaultAgent

model = _selected_model(config)
if model.settings.keys() - {"cost_tracking"}:
raise lifecycle.LifecycleError(
"mini_swe_agent_unsupported_model_settings",
"mini-SWE-agent supports only the cost_tracking model setting",
)
self._cost_tracking = model.settings.get("cost_tracking", "default")
if self._cost_tracking not in ("default", "disabled"):
raise lifecycle.LifecycleError(
"mini_swe_agent_invalid_cost_tracking",
"models settings.cost_tracking must be 'default' or 'disabled'",
)
if self._cost_tracking == "disabled" and (
config.runtime is None
or config.runtime.max_turns is None
or config.runtime.max_turns <= 0
):
raise lifecycle.LifecycleError(
"mini_swe_agent_unbounded_unpriced_model",
"Disabling cost tracking requires a positive runtime.max_turns; "
"mini-SWE-agent cannot enforce a monetary budget without pricing",
)
model_kwargs: dict[str, Any] = {}
if model.api_key_env and (api_key := os.environ.get(model.api_key_env)):
model_kwargs["api_key"] = api_key
Expand All @@ -100,17 +122,27 @@ async def start(self, payload: dict[str, Any]) -> None:
model_kwargs["top_p"] = model.top_p
if model.max_tokens is not None:
model_kwargs["max_tokens"] = model.max_tokens
model_name = model.model
if model.provider == "nvidia" and not model_name.startswith("nvidia_nim/"):
# LiteLLM's dispatch prefix is separate from the NIM served model ID.
model_name = f"nvidia_nim/{model_name}"
elif "/" not in model_name:
model_name = f"{model.provider}/{model_name}"
self._model = LitellmModel(
model_name=(
model.model if "/" in model.model else f"{model.provider}/{model.model}"
),
model_name=model_name,
model_kwargs=model_kwargs,
# Pin the default instead of inheriting MSWEA_COST_TRACKING.
cost_tracking=(
"ignore_errors" if self._cost_tracking == "disabled" else "default"
),
)
self._environment = LocalEnvironment(
**(config.harness.settings if config.harness else {})
)
self._system_instruction = instruction.content if instruction else ""
agent_kwargs: dict[str, Any] = {}
if self._cost_tracking == "disabled":
agent_kwargs["cost_limit"] = 0
if self._relay_enabled:
agent_kwargs["relay_model_name"] = self._model.config.model_name
self._agent = agent_type(
Expand Down Expand Up @@ -141,6 +173,8 @@ async def invoke(
"output": result.get("submission", ""),
"usage": {"api_calls": self._agent.n_calls},
}
if self._cost_tracking == "disabled":
output["usage"].update(cost_tracking="disabled", cost_status="unavailable")
if self._relay_enabled:
output["telemetry"] = {
"enabled": True,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,9 @@ def query(self) -> dict[str, Any]:
metadata: dict[str, Any] = {"status": "succeeded"}
try:
message = super().query()
response = _relay_response(message)
response = _relay_response(
message, include_cost=self.model.config.cost_tracking != "ignore_errors"
)
return message
except Exception as error:
response = _relay_exception_response(error)
Expand Down Expand Up @@ -266,15 +268,15 @@ def _end_tool_call(
self._record_relay_error(error)


def _relay_response(message: dict[str, Any]) -> Any:
def _relay_response(message: dict[str, Any], *, include_cost: bool = True) -> Any:
extra = message.get("extra", {})
response = extra.get("response")
if not isinstance(response, dict):
return {key: value for key, value in message.items() if key != "extra"}

response = deepcopy(response)
cost = extra.get("cost")
if isinstance(cost, (int, float)) and not isinstance(cost, bool):
if include_cost and isinstance(cost, (int, float)) and not isinstance(cost, bool):
usage = response.setdefault("usage", {})
if isinstance(usage, dict):
usage.setdefault("cost", float(cost))
Expand Down
4 changes: 4 additions & 0 deletions adapters/python/mini-swe-agent/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion adapters/python/nooa/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ The shared `nvidia.fabric.nooa` adapter owns the common NOOA queue dispatcher.
It also maps normalized skill paths and whole MCP servers into compatible
registered targets.
The dedicated `nvidia.fabric.nooa.bench-agent` adapter maps the benchmark-native
`BenchAgent` task contract directly into a NeMo Fabric invocation.
`BenchAgent` task contract directly into a NeMo Fabric invocation. Its
[observed token usage](docs/bench-agent.md#observed-token-usage) preserves the
SDK's inclusive input convention and documents missing-request limitations.

## Install

Expand All @@ -28,6 +30,8 @@ tested NOOA packages together:
pip install "nemo-fabric[nooa]"
```

The `harness` and `full` extras constrain LiteLLM below 1.106 for NOOA and standalone BenchAgent runs. LiteLLM 1.106.4 and 1.106.5 import MCP types unavailable in MCP 1.x during ordinary tool dispatch. The tested 1.105.6 dispatch works without an MCP runtime.

For a split adapter environment, install
`nemo-fabric-adapters-nooa[harness]`. Use the adapter package without an extra
when the environment already manages compatible NOOA packages. The installed
Expand Down
Loading
Loading