nooa-cli 0.0.10__tar.gz → 0.0.12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/PKG-INFO +4 -1
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/README.md +2 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/pyproject.toml +1 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/AGENTS.md +7 -2
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/__init__.py +53 -0
- nooa_cli-0.0.12/src/nooa_cli/coding/__init__.py +43 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/coding/activity.py +33 -5
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/coding/agent.py +50 -15
- nooa_cli-0.0.12/src/nooa_cli/commands/_connect_advertised_media.py +142 -0
- nooa_cli-0.0.12/src/nooa_cli/commands/_connect_input_policy.py +124 -0
- nooa_cli-0.0.12/src/nooa_cli/commands/_connect_prompts.py +713 -0
- nooa_cli-0.0.12/src/nooa_cli/commands/_connect_registry.py +257 -0
- nooa_cli-0.0.12/src/nooa_cli/commands/_connect_stages.py +844 -0
- nooa_cli-0.0.12/src/nooa_cli/commands/_connect_view.py +897 -0
- nooa_cli-0.0.12/src/nooa_cli/commands/_connect_wizard.py +3912 -0
- nooa_cli-0.0.12/src/nooa_cli/commands/connect.py +481 -0
- nooa_cli-0.0.12/tests/conftest.py +29 -0
- nooa_cli-0.0.12/tests/test_cli_platform_check.py +37 -0
- nooa_cli-0.0.12/tests/test_cli_utf8_output.py +57 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/tests/test_coding_activity.py +60 -8
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/tests/test_coding_agent.py +60 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/tests/test_coding_slash_commands.py +36 -0
- nooa_cli-0.0.12/tests/test_connect_advertised_media.py +602 -0
- nooa_cli-0.0.12/tests/test_connect_baseline_safety.py +240 -0
- nooa_cli-0.0.12/tests/test_connect_cache_control.py +272 -0
- nooa_cli-0.0.12/tests/test_connect_change_settings.py +321 -0
- nooa_cli-0.0.12/tests/test_connect_checkbox_pty.py +193 -0
- nooa_cli-0.0.12/tests/test_connect_coderabbit_budget.py +60 -0
- nooa_cli-0.0.12/tests/test_connect_command.py +1767 -0
- nooa_cli-0.0.12/tests/test_connect_current_verification.py +145 -0
- nooa_cli-0.0.12/tests/test_connect_decision_catalogue.py +230 -0
- nooa_cli-0.0.12/tests/test_connect_decision_listing.py +471 -0
- nooa_cli-0.0.12/tests/test_connect_decision_smoke.py +213 -0
- nooa_cli-0.0.12/tests/test_connect_diagnostic.py +118 -0
- nooa_cli-0.0.12/tests/test_connect_docs.py +59 -0
- nooa_cli-0.0.12/tests/test_connect_docs_contract.py +695 -0
- nooa_cli-0.0.12/tests/test_connect_edit.py +307 -0
- nooa_cli-0.0.12/tests/test_connect_encrypted_reasoning.py +168 -0
- nooa_cli-0.0.12/tests/test_connect_endpoint_scope.py +45 -0
- nooa_cli-0.0.12/tests/test_connect_entrypoint.py +86 -0
- nooa_cli-0.0.12/tests/test_connect_exact_context.py +215 -0
- nooa_cli-0.0.12/tests/test_connect_initial_order.py +469 -0
- nooa_cli-0.0.12/tests/test_connect_initial_order_pty.py +161 -0
- nooa_cli-0.0.12/tests/test_connect_interface_selection.py +778 -0
- nooa_cli-0.0.12/tests/test_connect_listing_provenance.py +340 -0
- nooa_cli-0.0.12/tests/test_connect_live_followups.py +134 -0
- nooa_cli-0.0.12/tests/test_connect_manual_limits.py +623 -0
- nooa_cli-0.0.12/tests/test_connect_media_inputs.py +119 -0
- nooa_cli-0.0.12/tests/test_connect_media_onboarding.py +965 -0
- nooa_cli-0.0.12/tests/test_connect_media_review.py +571 -0
- nooa_cli-0.0.12/tests/test_connect_metadata_first.py +663 -0
- nooa_cli-0.0.12/tests/test_connect_model_first.py +450 -0
- nooa_cli-0.0.12/tests/test_connect_native_media_policy.py +348 -0
- nooa_cli-0.0.12/tests/test_connect_native_profile.py +270 -0
- nooa_cli-0.0.12/tests/test_connect_practical_ui.py +525 -0
- nooa_cli-0.0.12/tests/test_connect_prompts.py +197 -0
- nooa_cli-0.0.12/tests/test_connect_proposal_layout.py +120 -0
- nooa_cli-0.0.12/tests/test_connect_real_pty.py +176 -0
- nooa_cli-0.0.12/tests/test_connect_reasoning_choice.py +397 -0
- nooa_cli-0.0.12/tests/test_connect_reply_budget.py +309 -0
- nooa_cli-0.0.12/tests/test_connect_review_followups.py +233 -0
- nooa_cli-0.0.12/tests/test_connect_review_readiness.py +115 -0
- nooa_cli-0.0.12/tests/test_connect_save_policy.py +769 -0
- nooa_cli-0.0.12/tests/test_connect_serial_ux.py +662 -0
- nooa_cli-0.0.12/tests/test_connect_server_details.py +768 -0
- nooa_cli-0.0.12/tests/test_connect_severin_review.py +83 -0
- nooa_cli-0.0.12/tests/test_connect_skill.py +227 -0
- nooa_cli-0.0.12/tests/test_connect_stages.py +521 -0
- nooa_cli-0.0.12/tests/test_connect_transcripts.py +252 -0
- nooa_cli-0.0.12/tests/test_connect_ux.py +443 -0
- nooa_cli-0.0.12/tests/test_connect_view.py +1159 -0
- nooa_cli-0.0.12/tests/test_connect_windows.py +79 -0
- nooa_cli-0.0.10/src/nooa_cli/coding/__init__.py +0 -32
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/.gitignore +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/docs/activity-introspection-design.md +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/__main__.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/_common.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/coding/instructions.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/coding/settings.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/coding/slash_commands.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/commands/__init__.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/commands/_otlp_helpers.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/commands/_template.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/commands/config.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/commands/delete_traces.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/commands/eval.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/commands/import_harbor.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/commands/import_traces.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/commands/start_dev.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/commands/traces.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/completion.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/sessions/__init__.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/sessions/events.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/sessions/store.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/tools/__init__.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/tools/_tree_sitter_backend.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/tools/pyp/__init__.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/tools/pyp/errors.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/tools/pyp/sources.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/tools/pyp/stream.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/tools/pyp/tests/__init__.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/tools/pyp/tests/test_pyp.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/src/nooa_cli/tools/repo_tools.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/tests/test_cli_smoke.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/tests/test_coding_settings.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/tests/test_command_plugins.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/tests/test_import_traces.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/tests/test_sessions.py +0 -0
- {nooa_cli-0.0.10 → nooa_cli-0.0.12}/tests/test_tree_sitter_backend.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: nooa-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.12
|
|
4
4
|
Summary: CLI for nemo-oo-agents (`nooa` command: eval runner, trace viewer, config)
|
|
5
5
|
Project-URL: Homepage, https://github.com/NVIDIA-NeMo/labs-OO-Agents
|
|
6
6
|
Project-URL: Repository, https://github.com/NVIDIA-NeMo/labs-OO-Agents
|
|
@@ -9,6 +9,7 @@ License: Apache-2.0
|
|
|
9
9
|
Requires-Python: <3.14,>=3.12
|
|
10
10
|
Requires-Dist: click>=8.1.0
|
|
11
11
|
Requires-Dist: nooa
|
|
12
|
+
Requires-Dist: prompt-toolkit>=3.0.52
|
|
12
13
|
Requires-Dist: pyyaml>=6.0
|
|
13
14
|
Provides-Extra: ast
|
|
14
15
|
Requires-Dist: tree-sitter-go>=0.23.0; extra == 'ast'
|
|
@@ -40,6 +41,8 @@ uv add "nooa-cli[datascience]"
|
|
|
40
41
|
|
|
41
42
|
`nooa-cli` automatically pulls in matching `nemo-oo-agents` (the core framework). The `[datascience]` extra adds libraries the LLM can use in REPL-generated code.
|
|
42
43
|
|
|
44
|
+
> **Windows:** use [WSL 2](https://learn.microsoft.com/windows/wsl/install). Native Windows is not supported; see the [main README](https://github.com/NVIDIA-NeMo/labs-OO-Agents#installation).
|
|
45
|
+
|
|
43
46
|
## Usage
|
|
44
47
|
|
|
45
48
|
```bash
|
|
@@ -13,6 +13,8 @@ uv add "nooa-cli[datascience]"
|
|
|
13
13
|
|
|
14
14
|
`nooa-cli` automatically pulls in matching `nemo-oo-agents` (the core framework). The `[datascience]` extra adds libraries the LLM can use in REPL-generated code.
|
|
15
15
|
|
|
16
|
+
> **Windows:** use [WSL 2](https://learn.microsoft.com/windows/wsl/install). Native Windows is not supported; see the [main README](https://github.com/NVIDIA-NeMo/labs-OO-Agents#installation).
|
|
17
|
+
|
|
16
18
|
## Usage
|
|
17
19
|
|
|
18
20
|
```bash
|
|
@@ -127,6 +127,11 @@ same contract as an in-repo command module.
|
|
|
127
127
|
- The **Performance Rule** below applies with extra force: every registered
|
|
128
128
|
entry point is loaded on *every* `nooa` invocation, including `nooa --help`.
|
|
129
129
|
Keep heavy imports inside the handler.
|
|
130
|
+
- **Importing `nooa_cli` must not import `nooa`** (`test_connect_entrypoint.py`
|
|
131
|
+
checks this). `main()` checks the native-Windows policy itself
|
|
132
|
+
(`_refuse_unsupported_platform`, kept equal to `nooa._platform` by
|
|
133
|
+
`packages/nooa-cli/tests/test_cli_platform_check.py`) and switches redirected output to UTF-8
|
|
134
|
+
on Windows. New console entry points should go through `main()`.
|
|
130
135
|
|
|
131
136
|
This mirrors the existing `nooa.skills` and `nooa.bundled_configs` groups.
|
|
132
137
|
|
|
@@ -186,6 +191,6 @@ New commands get tab-completion automatically. To enable it:
|
|
|
186
191
|
nooa completion install # auto-detects your shell
|
|
187
192
|
|
|
188
193
|
# Or manually:
|
|
189
|
-
eval "$(
|
|
190
|
-
eval "$(
|
|
194
|
+
eval "$(_NOOA_COMPLETE=zsh_source nooa)" # zsh
|
|
195
|
+
eval "$(_NOOA_COMPLETE=bash_source nooa)" # bash
|
|
191
196
|
```
|
|
@@ -19,6 +19,9 @@ Shell completion:
|
|
|
19
19
|
eval "$(_NOOA_COMPLETE=zsh_source nooa)" # zsh
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
|
|
22
25
|
import click
|
|
23
26
|
|
|
24
27
|
from .commands import discover_commands
|
|
@@ -72,6 +75,56 @@ for _name, _cmd in discover_commands():
|
|
|
72
75
|
oo.add_command(completion)
|
|
73
76
|
|
|
74
77
|
|
|
78
|
+
# Same policy and wording as nooa._platform, checked here without importing
|
|
79
|
+
# nooa so the CLI stays lazy (packages/nooa-cli/tests/test_cli_platform_check.py keeps them equal).
|
|
80
|
+
_SKIP_PLATFORM_CHECK_ENV = "NOOA_SKIP_PLATFORM_CHECK"
|
|
81
|
+
_UNSUPPORTED_PLATFORM_MESSAGE = (
|
|
82
|
+
"nooa does not support native Windows; use WSL 2 "
|
|
83
|
+
"(https://learn.microsoft.com/windows/wsl/install). "
|
|
84
|
+
f"Setting the {_SKIP_PLATFORM_CHECK_ENV} environment variable to 1 skips this check: "
|
|
85
|
+
"the pure-Python core and `nooa connect` are tested on native Windows; "
|
|
86
|
+
"other features may work but are unsupported."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _refuse_unsupported_platform() -> None:
|
|
91
|
+
"""Exit with one clean line on native Windows unless the user opted in."""
|
|
92
|
+
if sys.platform != "win32":
|
|
93
|
+
return
|
|
94
|
+
if os.environ.get(_SKIP_PLATFORM_CHECK_ENV, "").strip().lower() in {"1", "true", "yes"}:
|
|
95
|
+
return
|
|
96
|
+
raise SystemExit(f"Error: {_UNSUPPORTED_PLATFORM_MESSAGE}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _is_utf8(stream) -> bool:
|
|
100
|
+
encoding = getattr(stream, "encoding", None) or ""
|
|
101
|
+
return encoding.lower().replace("-", "").replace("_", "") == "utf8"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _use_utf8_output() -> None:
|
|
105
|
+
"""Use UTF-8 for redirected output and piped input on native Windows.
|
|
106
|
+
|
|
107
|
+
Redirected stdout/stderr and piped stdin use the ANSI code page there (for
|
|
108
|
+
example cp1252), which cannot encode the CLI's check marks and arrows and
|
|
109
|
+
would crash the command midway. Consoles already use UTF-8, so this changes
|
|
110
|
+
nothing there.
|
|
111
|
+
"""
|
|
112
|
+
if sys.platform != "win32":
|
|
113
|
+
return
|
|
114
|
+
for stream in (sys.stdout, sys.stderr):
|
|
115
|
+
if stream is not None and not _is_utf8(stream) and hasattr(stream, "reconfigure"):
|
|
116
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
117
|
+
stdin = sys.stdin
|
|
118
|
+
try:
|
|
119
|
+
piped = stdin is not None and not stdin.isatty()
|
|
120
|
+
except (OSError, ValueError):
|
|
121
|
+
piped = False
|
|
122
|
+
if piped and not _is_utf8(stdin) and hasattr(stdin, "reconfigure"):
|
|
123
|
+
stdin.reconfigure(encoding="utf-8", errors="replace")
|
|
124
|
+
|
|
125
|
+
|
|
75
126
|
def main():
|
|
76
127
|
"""Entry point for console_scripts: nooa <subcommand>."""
|
|
128
|
+
_refuse_unsupported_platform()
|
|
129
|
+
_use_utf8_output()
|
|
77
130
|
oo()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Shared coding-agent components used by terminal and protocol hosts."""
|
|
4
|
+
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
|
|
7
|
+
_EXPORT_MODULES = {
|
|
8
|
+
"ActivityShellTools": "activity",
|
|
9
|
+
"FileEdit": "activity",
|
|
10
|
+
"TerminalCommandFinished": "activity",
|
|
11
|
+
"TerminalCommandOutput": "activity",
|
|
12
|
+
"TerminalCommandStarted": "activity",
|
|
13
|
+
"CodingAgent": "agent",
|
|
14
|
+
"discover_agent_instruction_files": "instructions",
|
|
15
|
+
"render_agent_instructions": "instructions",
|
|
16
|
+
"load_coding_skills_dirs": "settings",
|
|
17
|
+
"CodingSlashCommand": "slash_commands",
|
|
18
|
+
"CodingSlashCommandRegistry": "slash_commands",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def __getattr__(name: str):
|
|
23
|
+
"""Load host components only when requested, not for leaf utility imports."""
|
|
24
|
+
if name not in _EXPORT_MODULES:
|
|
25
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
26
|
+
value = getattr(import_module(f"{__name__}.{_EXPORT_MODULES[name]}"), name)
|
|
27
|
+
globals()[name] = value
|
|
28
|
+
return value
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"ActivityShellTools",
|
|
33
|
+
"CodingAgent",
|
|
34
|
+
"CodingSlashCommand",
|
|
35
|
+
"CodingSlashCommandRegistry",
|
|
36
|
+
"FileEdit",
|
|
37
|
+
"TerminalCommandFinished",
|
|
38
|
+
"TerminalCommandOutput",
|
|
39
|
+
"TerminalCommandStarted",
|
|
40
|
+
"discover_agent_instruction_files",
|
|
41
|
+
"load_coding_skills_dirs",
|
|
42
|
+
"render_agent_instructions",
|
|
43
|
+
]
|
|
@@ -69,8 +69,15 @@ def _edit_diff(
|
|
|
69
69
|
old_text: str,
|
|
70
70
|
new_text: str,
|
|
71
71
|
start_line: int | None,
|
|
72
|
+
*,
|
|
73
|
+
whole_file: bool = False,
|
|
72
74
|
) -> tuple[str, bool]:
|
|
73
|
-
"""Return a bounded, line-oriented unified diff and its completeness.
|
|
75
|
+
"""Return a bounded, line-oriented unified diff and its completeness.
|
|
76
|
+
|
|
77
|
+
``whole_file`` marks both texts as complete file contents, where a missing
|
|
78
|
+
final newline is a real file state worth reporting. Fragment diffs stop
|
|
79
|
+
mid-file, so the EOF marker there was noise.
|
|
80
|
+
"""
|
|
74
81
|
if _diff_input_is_too_large(old_text) or _diff_input_is_too_large(new_text):
|
|
75
82
|
return _omitted_diff(path, "file content exceeds the safe diff preview limit")
|
|
76
83
|
|
|
@@ -91,7 +98,13 @@ def _edit_diff(
|
|
|
91
98
|
if not line.endswith("\n"):
|
|
92
99
|
# difflib emits the source line verbatim, so unterminated content
|
|
93
100
|
# would run the next marker onto the same line ("-a+b").
|
|
94
|
-
|
|
101
|
+
if whole_file:
|
|
102
|
+
line = f"{line}\n\\n"
|
|
103
|
+
else:
|
|
104
|
+
# A fragment routinely stops before the file's final
|
|
105
|
+
# newline; marking that as "no newline at end of file"
|
|
106
|
+
# reported a file state that does not exist.
|
|
107
|
+
line = f"{line}\n"
|
|
95
108
|
output.write(line)
|
|
96
109
|
return output.getvalue(), not output.was_truncated
|
|
97
110
|
|
|
@@ -252,7 +265,9 @@ class ActivityShellTools(Skill):
|
|
|
252
265
|
stdin: Annotated[
|
|
253
266
|
str | None, spec(description="Text piped to stdin (replaces heredocs)")
|
|
254
267
|
] = None,
|
|
255
|
-
timeout: Annotated[
|
|
268
|
+
timeout: Annotated[
|
|
269
|
+
float | None, spec(description="Max seconds; None uses the shell's default_timeout")
|
|
270
|
+
] = None,
|
|
256
271
|
) -> ShellResult:
|
|
257
272
|
command_id = str(uuid4())
|
|
258
273
|
bounded_command = pformat(command, max_string=_MAX_EVENT_TEXT_CHARS, unquote_strings=True)
|
|
@@ -313,7 +328,13 @@ class ActivityShellTools(Skill):
|
|
|
313
328
|
async def run_stream(
|
|
314
329
|
self,
|
|
315
330
|
command: Annotated[str, spec(description="Shell command to execute")],
|
|
316
|
-
|
|
331
|
+
*,
|
|
332
|
+
stdin: Annotated[
|
|
333
|
+
str | None, spec(description="Text piped to stdin (replaces heredocs)")
|
|
334
|
+
] = None,
|
|
335
|
+
timeout: Annotated[
|
|
336
|
+
float | None, spec(description="Max seconds; None uses the shell's default_timeout")
|
|
337
|
+
] = None,
|
|
317
338
|
) -> AsyncIterator[StreamEvent | StreamDone]:
|
|
318
339
|
command_id = str(uuid4())
|
|
319
340
|
bounded_command = pformat(command, max_string=_MAX_EVENT_TEXT_CHARS, unquote_strings=True)
|
|
@@ -324,12 +345,18 @@ class ActivityShellTools(Skill):
|
|
|
324
345
|
command=bounded_command,
|
|
325
346
|
working_directory=str(self.cwd),
|
|
326
347
|
command_truncated=command_truncated,
|
|
348
|
+
stdin=(
|
|
349
|
+
pformat(stdin, max_string=_MAX_EVENT_TEXT_CHARS, unquote_strings=True)
|
|
350
|
+
if stdin is not None
|
|
351
|
+
else None
|
|
352
|
+
),
|
|
353
|
+
stdin_truncated=stdin is not None and len(stdin) > _MAX_EVENT_TEXT_CHARS,
|
|
327
354
|
)
|
|
328
355
|
)
|
|
329
356
|
finished = False
|
|
330
357
|
stdout_buffer = TruncatingStringIO(limit=_MAX_COMMAND_OUTPUT_CHARS // 2)
|
|
331
358
|
stderr_buffer = TruncatingStringIO(limit=_MAX_COMMAND_OUTPUT_CHARS // 2)
|
|
332
|
-
stream = self._shell.run_stream(command, timeout=timeout)
|
|
359
|
+
stream = self._shell.run_stream(command, stdin=stdin, timeout=timeout)
|
|
333
360
|
try:
|
|
334
361
|
async for item in stream:
|
|
335
362
|
if isinstance(item, StreamDone):
|
|
@@ -504,6 +531,7 @@ class ActivityShellTools(Skill):
|
|
|
504
531
|
old_diff_text or "",
|
|
505
532
|
content,
|
|
506
533
|
None,
|
|
534
|
+
whole_file=True,
|
|
507
535
|
)
|
|
508
536
|
self._emit(
|
|
509
537
|
FileEdit(
|
|
@@ -9,12 +9,14 @@ from typing import TYPE_CHECKING, Annotated, Any
|
|
|
9
9
|
|
|
10
10
|
from nooa import Context, hidden, strategy
|
|
11
11
|
from nooa.agentdoc import doc, spec
|
|
12
|
+
from nooa.agents import TokenBudgetSummarizer
|
|
12
13
|
from nooa.config import CodeActConfig, PredictConfig
|
|
13
14
|
from nooa.interactive import (
|
|
15
|
+
Done,
|
|
14
16
|
InteractiveAgent,
|
|
15
|
-
|
|
16
|
-
RespondResult,
|
|
17
|
+
NeedInput,
|
|
17
18
|
SummarizationConfig,
|
|
19
|
+
Waiting,
|
|
18
20
|
install_summarizer,
|
|
19
21
|
)
|
|
20
22
|
from nooa.paths import get_project_dir
|
|
@@ -31,7 +33,7 @@ if TYPE_CHECKING:
|
|
|
31
33
|
from nooa.runtime.channels import Channel
|
|
32
34
|
from nooa.unifiedllm import UnifiedLLM
|
|
33
35
|
|
|
34
|
-
__all__ = ["CodingAgent"
|
|
36
|
+
__all__ = ["CodingAgent"]
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
class CodingAgent(InteractiveAgent):
|
|
@@ -41,10 +43,10 @@ class CodingAgent(InteractiveAgent):
|
|
|
41
43
|
unrelated worktree changes. Use the shell for files and commands, the repo
|
|
42
44
|
tools for definitions and references, and todos for multi-step work.
|
|
43
45
|
|
|
44
|
-
Complete and verify the requested work before returning ``
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
Complete and verify the requested work before returning ``Done``. Put the
|
|
47
|
+
reply to the user in ``Done.message`` as a complete Markdown document.
|
|
48
|
+
Return ``NeedInput`` only when human input is required, and ``Waiting``
|
|
49
|
+
only while an actual background job is active.
|
|
48
50
|
"""
|
|
49
51
|
|
|
50
52
|
# Attributes carrying this agent's own tools. SkillRegistry refuses to let
|
|
@@ -144,16 +146,41 @@ class CodingAgent(InteractiveAgent):
|
|
|
144
146
|
summary_tags = [tag for tag in tags if ".." in tag]
|
|
145
147
|
summarizers = getattr(self, "_summarizers", [])
|
|
146
148
|
summarizer = summarizers[0] if summarizers else None
|
|
149
|
+
config = getattr(summarizer, "config", None) if summarizer else None
|
|
150
|
+
pending = getattr(summarizer, "_pending_task", None) if summarizer else None
|
|
151
|
+
automatic = bool(getattr(summarizer, "_automatic_context_budget", False))
|
|
152
|
+
declared_policy = getattr(summarizer, "policy", None) if summarizer else None
|
|
153
|
+
policy = (
|
|
154
|
+
declared_policy
|
|
155
|
+
if isinstance(declared_policy, str) and declared_policy
|
|
156
|
+
else "token_budget"
|
|
157
|
+
if isinstance(summarizer, TokenBudgetSummarizer)
|
|
158
|
+
else "custom"
|
|
159
|
+
if summarizer is not None
|
|
160
|
+
else "none"
|
|
161
|
+
)
|
|
147
162
|
stats = self.context_stats
|
|
148
163
|
return {
|
|
149
164
|
"active_events": len(tags),
|
|
150
165
|
"summary_count": len(summary_tags),
|
|
151
166
|
"summary_tags": summary_tags,
|
|
152
167
|
"has_summarizer": summarizer is not None,
|
|
153
|
-
"policy":
|
|
154
|
-
"current_tokens": getattr(stats, "
|
|
155
|
-
"max_tokens": getattr(
|
|
156
|
-
"
|
|
168
|
+
"policy": policy,
|
|
169
|
+
"current_tokens": getattr(stats, "prompt_tokens", 0) if stats else 0,
|
|
170
|
+
"max_tokens": getattr(config, "max_tokens", 0) if config else 0,
|
|
171
|
+
"threshold_fraction": (
|
|
172
|
+
getattr(summarizer, "_automatic_context_budget_percent", None)
|
|
173
|
+
if automatic
|
|
174
|
+
else None
|
|
175
|
+
),
|
|
176
|
+
"preserve_recent": getattr(config, "preserve_recent", 0) if config else 0,
|
|
177
|
+
"compaction_pending": pending is not None,
|
|
178
|
+
"compaction_ready": bool(
|
|
179
|
+
pending is not None
|
|
180
|
+
and pending.done()
|
|
181
|
+
and getattr(summarizer, "_pending_summary", None)
|
|
182
|
+
and getattr(summarizer, "_pending_range", None)
|
|
183
|
+
),
|
|
157
184
|
}
|
|
158
185
|
|
|
159
186
|
@hidden
|
|
@@ -165,16 +192,23 @@ class CodingAgent(InteractiveAgent):
|
|
|
165
192
|
|
|
166
193
|
@hidden
|
|
167
194
|
@strategy(CodeActStrategy(config=CodeActConfig(cell_timeout=1800.0)))
|
|
168
|
-
async def handle(self, notification: dict[str, list[Any]]) ->
|
|
195
|
+
async def handle(self, notification: dict[str, list[Any]]) -> Done | NeedInput | Waiting:
|
|
169
196
|
"""Fulfill the newest coding request delivered in ``notification``.
|
|
170
197
|
|
|
171
198
|
Work until the request is complete or genuinely needs user input. Use
|
|
172
199
|
as many small execution cells as necessary and inspect each result
|
|
173
200
|
before proceeding. Never claim a check passed without running it.
|
|
174
201
|
|
|
175
|
-
End with exactly one ``return_result(
|
|
176
|
-
|
|
177
|
-
|
|
202
|
+
End the turn with exactly one ``return_result(...)`` of one of these:
|
|
203
|
+
|
|
204
|
+
- ``Done(message=..., explanation=...)`` — the request is complete.
|
|
205
|
+
``message`` is the reply the host shows the user; ``explanation``
|
|
206
|
+
is a short status line. Add ``evidence=[...]`` for checks you ran.
|
|
207
|
+
- ``NeedInput(question=...)`` — you cannot continue without an
|
|
208
|
+
answer. The host shows the question, so do not also send it with
|
|
209
|
+
``self.message()``. Add ``options=[...]`` for a single choice.
|
|
210
|
+
- ``Waiting(message=..., explanation=..., on=[...])`` — a background
|
|
211
|
+
job is still running; ``on`` names the job or channel.
|
|
178
212
|
"""
|
|
179
213
|
...
|
|
180
214
|
|
|
@@ -182,6 +216,7 @@ class CodingAgent(InteractiveAgent):
|
|
|
182
216
|
async def close(self) -> None:
|
|
183
217
|
shell = self.shell
|
|
184
218
|
try:
|
|
219
|
+
await self.aclose()
|
|
185
220
|
await self.skills.aclose()
|
|
186
221
|
finally:
|
|
187
222
|
try:
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Wizard step that shows which media inputs the model is said to accept.
|
|
4
|
+
|
|
5
|
+
Published model information (the model listing, server details or the
|
|
6
|
+
OpenRouter catalogue) may list input types such as images or audio. This step
|
|
7
|
+
shows those as suggestions and lets the user decide: test them, enable them
|
|
8
|
+
without testing, pick them by hand, or turn media off. Listed media is never
|
|
9
|
+
enabled automatically.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def offer_suggestions(state, previous):
|
|
14
|
+
"""Show the media inputs listed for the selected model and ask how to use them.
|
|
15
|
+
|
|
16
|
+
``state`` is the wizard state; ``previous`` is the list of media inputs the
|
|
17
|
+
entry had before (shown as "Current inputs"). Depending on the answer this
|
|
18
|
+
sets ``state.test_media_inputs`` (inputs to test) or ``state.media_inputs``
|
|
19
|
+
(inputs to enable without testing).
|
|
20
|
+
|
|
21
|
+
Returns True if the user made a choice here, and False if no question was
|
|
22
|
+
asked: the choice already came from command-line options, ``--yes`` was
|
|
23
|
+
given, or nothing was suggested.
|
|
24
|
+
"""
|
|
25
|
+
from nooa.unifiedllm import connect
|
|
26
|
+
|
|
27
|
+
from . import _connect_prompts as prompts
|
|
28
|
+
from . import _connect_view as view
|
|
29
|
+
|
|
30
|
+
if not state.metadata_displayed:
|
|
31
|
+
from ._connect_wizard import show_model_metadata
|
|
32
|
+
|
|
33
|
+
show_model_metadata(state)
|
|
34
|
+
from ._connect_input_policy import media_route
|
|
35
|
+
from ._connect_wizard import model_media_hints
|
|
36
|
+
|
|
37
|
+
hints = model_media_hints(state)
|
|
38
|
+
report = connect.advertised_media_suggestions(
|
|
39
|
+
hints,
|
|
40
|
+
source=(hints or {}).get(
|
|
41
|
+
"media_source", "published model information" if hints else "unknown"
|
|
42
|
+
),
|
|
43
|
+
api_style=state.api_style,
|
|
44
|
+
entry=media_route(state),
|
|
45
|
+
decision_image_transport=state.decision_image_transport or "json",
|
|
46
|
+
typesafe_danger_zone_images=state.typesafe_danger_zone_images,
|
|
47
|
+
)
|
|
48
|
+
from ._connect_wizard import scrub_setup
|
|
49
|
+
|
|
50
|
+
view.section("Media setup")
|
|
51
|
+
for note in report["notes"]:
|
|
52
|
+
if note not in {
|
|
53
|
+
"Media support isn't listed. You can test inputs or choose them yourself.",
|
|
54
|
+
"No media is listed.",
|
|
55
|
+
}:
|
|
56
|
+
view.note(scrub_setup(state, note))
|
|
57
|
+
from ._connect_input_policy import available_media
|
|
58
|
+
|
|
59
|
+
supported = available_media(state)
|
|
60
|
+
# Formats this interface can't send are already excluded from the choices.
|
|
61
|
+
explained = [
|
|
62
|
+
(view.media_label(kind) + " test", reason)
|
|
63
|
+
for kind, reason in report["unavailable_checks"].items()
|
|
64
|
+
if kind in supported
|
|
65
|
+
]
|
|
66
|
+
if explained:
|
|
67
|
+
view.fields(scrub_setup(state, explained))
|
|
68
|
+
|
|
69
|
+
def input_label(kind):
|
|
70
|
+
return {"audio": "Audio clips", "video": "Videos"}.get(kind, view.media_label(kind))
|
|
71
|
+
|
|
72
|
+
suggested_label = ", ".join(input_label(kind) for kind in report["suggested"])
|
|
73
|
+
checkable_label = ", ".join(input_label(kind) for kind in report["checkable"])
|
|
74
|
+
view.fields(
|
|
75
|
+
[
|
|
76
|
+
("Suggested inputs", suggested_label),
|
|
77
|
+
("Current inputs", ", ".join(view.media_label(kind) for kind in previous) or "Off"),
|
|
78
|
+
]
|
|
79
|
+
if suggested_label
|
|
80
|
+
else [("Current inputs", ", ".join(view.media_label(kind) for kind in previous) or "Off")]
|
|
81
|
+
)
|
|
82
|
+
if (
|
|
83
|
+
state.media_inputs is not None
|
|
84
|
+
or state.test_media_inputs is not None
|
|
85
|
+
or state.yes
|
|
86
|
+
or not report["suggested"]
|
|
87
|
+
):
|
|
88
|
+
return False
|
|
89
|
+
options = []
|
|
90
|
+
can_test = not state.no_probe and state.approval == "all" and bool(report["checkable"])
|
|
91
|
+
all_inputs = (
|
|
92
|
+
["image"]
|
|
93
|
+
if state.api_style in connect.DECISION_API_STYLES
|
|
94
|
+
else ["image", "audio", "video", "pdf"]
|
|
95
|
+
)
|
|
96
|
+
all_report = connect.advertised_media_suggestions(
|
|
97
|
+
{"id": state.model, "input_modalities": all_inputs},
|
|
98
|
+
api_style=state.api_style,
|
|
99
|
+
entry=media_route(state),
|
|
100
|
+
decision_image_transport=state.decision_image_transport or "json",
|
|
101
|
+
typesafe_danger_zone_images=state.typesafe_danger_zone_images,
|
|
102
|
+
)
|
|
103
|
+
can_test_all = not state.no_probe and state.approval == "all" and bool(all_report["checkable"])
|
|
104
|
+
if can_test:
|
|
105
|
+
options.append(("test", f"Test and enable {checkable_label}"))
|
|
106
|
+
if can_test_all:
|
|
107
|
+
options.append(("test-all", "Test all media inputs"))
|
|
108
|
+
options.extend(
|
|
109
|
+
(
|
|
110
|
+
("suggested", f"Enable {suggested_label} without testing"),
|
|
111
|
+
("manual", "Select media inputs manually"),
|
|
112
|
+
("none", "Turn media inputs off"),
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
action = prompts.labelled_choice(
|
|
116
|
+
"Choose how to use the listed media inputs",
|
|
117
|
+
tuple(options),
|
|
118
|
+
default="test" if can_test else "test-all" if can_test_all else "manual",
|
|
119
|
+
)
|
|
120
|
+
if action in {"test", "test-all"}:
|
|
121
|
+
state.test_media_inputs = (
|
|
122
|
+
list(report["checkable"]) if action == "test" else list(all_inputs)
|
|
123
|
+
)
|
|
124
|
+
state.media_inputs = list(previous)
|
|
125
|
+
state.inherit_media_inputs = True
|
|
126
|
+
if action == "test-all":
|
|
127
|
+
supported = available_media(state)
|
|
128
|
+
for kind, reason in all_report["unavailable_checks"].items():
|
|
129
|
+
if kind not in report["unavailable_checks"] and kind in supported:
|
|
130
|
+
view.fields(scrub_setup(state, [(view.media_label(kind) + " test", reason)]))
|
|
131
|
+
elif action == "manual":
|
|
132
|
+
from ._connect_wizard import select_manual_media
|
|
133
|
+
|
|
134
|
+
select_manual_media(
|
|
135
|
+
state,
|
|
136
|
+
previous,
|
|
137
|
+
**({"available": ("image",)} if state.api_style in connect.DECISION_API_STYLES else {}),
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
state.media_inputs = list(report["suggested"]) if action == "suggested" else []
|
|
141
|
+
state.inherit_media_inputs = False
|
|
142
|
+
return True
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Which media inputs the wizard may offer for the chosen API style.
|
|
4
|
+
|
|
5
|
+
Each API style can send only some media kinds (for example, the Anthropic
|
|
6
|
+
Messages and OpenAI Responses clients send images and PDFs, Chat Completions
|
|
7
|
+
also sends audio and video, decision clients send images). The rules live in
|
|
8
|
+
``nooa.media_policy``; this module applies them to the wizard state. It limits
|
|
9
|
+
the media picker to the kinds the API style can send, and asks the user to fix
|
|
10
|
+
an entry whose saved media inputs include kinds the API style cannot send.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def media_route(state):
|
|
17
|
+
"""Return a model entry describing the endpoint and API style chosen so far.
|
|
18
|
+
|
|
19
|
+
The entry is built from the wizard state without any network request. It
|
|
20
|
+
is only used to look up which media kinds that API style can send; it says
|
|
21
|
+
nothing about what the model itself accepts.
|
|
22
|
+
"""
|
|
23
|
+
if state.editing is not None:
|
|
24
|
+
# When editing, the existing entry's client and provider settings are
|
|
25
|
+
# kept unchanged, and they decide how requests are sent at runtime.
|
|
26
|
+
# The wizard's api_style label may not match them, so use the entry.
|
|
27
|
+
return state.editing
|
|
28
|
+
style = state.api_style or "chat"
|
|
29
|
+
prefix = "anthropic/" if style == "anthropic" else "openai/"
|
|
30
|
+
return {
|
|
31
|
+
"model_name": prefix + (state.model or ""),
|
|
32
|
+
"api_base": state.endpoint,
|
|
33
|
+
"client_type": "decision"
|
|
34
|
+
if style in {"systemone", "openai"}
|
|
35
|
+
else "responses"
|
|
36
|
+
if style == "responses"
|
|
37
|
+
else "completion",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def available_media(state, *, entry=None):
|
|
42
|
+
"""Return the media kinds the API style of ``entry`` (or the wizard state) can send."""
|
|
43
|
+
from nooa.media_policy import entry_input_interface, implemented_input_kinds
|
|
44
|
+
|
|
45
|
+
return implemented_input_kinds(
|
|
46
|
+
entry_input_interface(entry if entry is not None else media_route(state))
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def picker_options(state, *, entry=None):
|
|
51
|
+
"""Return keyword arguments that limit the media picker to the kinds that can be sent."""
|
|
52
|
+
available = available_media(state, entry=entry)
|
|
53
|
+
# When all four kinds can be sent (Chat Completions), return no arguments so
|
|
54
|
+
# the picker is called exactly as before, with its default list of kinds.
|
|
55
|
+
return {"available": available} if len(available) != 4 else {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def repair_native_media(state, current, *, entry=None, explicit=False):
|
|
59
|
+
"""Make sure the enabled media inputs ``current`` can be sent with this API style.
|
|
60
|
+
|
|
61
|
+
Only the Anthropic Messages and OpenAI Responses styles are checked here.
|
|
62
|
+
If every enabled kind can be sent, the list is returned unchanged. If not,
|
|
63
|
+
the user chooses: drop the kinds that cannot be sent, turn media off, or
|
|
64
|
+
cancel. The chosen list is stored on ``state`` and returned; cancel returns
|
|
65
|
+
None. Nothing is removed without the user's choice, and the saved entry
|
|
66
|
+
being edited is not modified here (only the new entry being built is).
|
|
67
|
+
|
|
68
|
+
With ``explicit`` (the list came from ``--media-inputs``) or ``--yes``,
|
|
69
|
+
there is no one to ask, so a ``click.UsageError`` is raised instead.
|
|
70
|
+
This only concerns enabled inputs; media tests chosen with "Test all" are
|
|
71
|
+
handled separately and may report kinds that were skipped.
|
|
72
|
+
"""
|
|
73
|
+
from nooa.media_inputs import validate_media_inputs
|
|
74
|
+
from nooa.media_policy import (
|
|
75
|
+
entry_input_interface,
|
|
76
|
+
implemented_input_kinds,
|
|
77
|
+
unsupported_input_reason,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
from . import _connect_prompts as prompts
|
|
81
|
+
from . import _connect_view as view
|
|
82
|
+
|
|
83
|
+
enabled = list(validate_media_inputs(current))
|
|
84
|
+
interface = entry_input_interface(entry if entry is not None else media_route(state))
|
|
85
|
+
if interface not in {"anthropic", "responses"}:
|
|
86
|
+
return enabled
|
|
87
|
+
available = implemented_input_kinds(interface)
|
|
88
|
+
unsupported = [kind for kind in enabled if kind not in available]
|
|
89
|
+
if not unsupported:
|
|
90
|
+
return enabled
|
|
91
|
+
reason = unsupported_input_reason(interface, unsupported)
|
|
92
|
+
if explicit or state.yes:
|
|
93
|
+
raise click.UsageError(
|
|
94
|
+
reason + " Use --media-inputs image,pdf or --media-inputs - to repair, or choose Chat."
|
|
95
|
+
)
|
|
96
|
+
view.warning(reason)
|
|
97
|
+
action = prompts.labelled_choice(
|
|
98
|
+
"Repair unsupported media permissions for this interface",
|
|
99
|
+
(
|
|
100
|
+
("supported", "Remove unsupported inputs and keep image/PDF settings"),
|
|
101
|
+
("off", "Turn all media inputs off"),
|
|
102
|
+
("cancel", "Cancel without changing or saving; choose another interface"),
|
|
103
|
+
),
|
|
104
|
+
default="cancel",
|
|
105
|
+
)
|
|
106
|
+
if action == "cancel":
|
|
107
|
+
return None
|
|
108
|
+
selected = [kind for kind in enabled if kind in available] if action == "supported" else []
|
|
109
|
+
state.media_inputs = selected
|
|
110
|
+
state.inherit_media_inputs = False
|
|
111
|
+
state.guided_media_revised = True
|
|
112
|
+
return selected
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def validate_native_declaration(state, *, entry=None):
|
|
116
|
+
"""Raise ``click.UsageError`` if ``--media-inputs`` names a kind this API style cannot send.
|
|
117
|
+
|
|
118
|
+
Runs before any request is sent, so an invalid option fails early.
|
|
119
|
+
"""
|
|
120
|
+
if state.media_inputs is None:
|
|
121
|
+
return
|
|
122
|
+
from ._connect_prompts import parse_media_inputs
|
|
123
|
+
|
|
124
|
+
repair_native_media(state, parse_media_inputs(state.media_inputs), entry=entry, explicit=True)
|