npcsh 1.1.16__py3-none-any.whl → 1.1.17__py3-none-any.whl
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.
- npcsh/_state.py +24 -9
- npcsh/benchmark/__init__.py +22 -0
- npcsh/benchmark/npcsh_agent.py +262 -0
- npcsh/benchmark/runner.py +569 -0
- npcsh/npc_team/jinxs/bin/benchmark.jinx +146 -0
- npcsh/npc_team/jinxs/bin/nql.jinx +7 -7
- npcsh/npc_team/jinxs/bin/roll.jinx +20 -23
- npcsh/npc_team/jinxs/bin/sample.jinx +6 -7
- npcsh/npc_team/jinxs/bin/spool.jinx +4 -4
- npcsh/npc_team/jinxs/bin/sync.jinx +6 -6
- npcsh/npc_team/jinxs/bin/vixynt.jinx +8 -8
- npcsh/npc_team/jinxs/bin/wander.jinx +109 -19
- npcsh/npc_team/jinxs/bin/yap.jinx +5 -5
- npcsh/npc_team/jinxs/incognide/add_tab.jinx +11 -0
- npcsh/npc_team/jinxs/incognide/close_pane.jinx +9 -0
- npcsh/npc_team/jinxs/incognide/close_tab.jinx +10 -0
- npcsh/npc_team/jinxs/incognide/confirm.jinx +10 -0
- npcsh/npc_team/jinxs/incognide/focus_pane.jinx +9 -0
- npcsh/npc_team/jinxs/{npc_studio/npc-studio.jinx → incognide/incognide.jinx} +2 -2
- npcsh/npc_team/jinxs/incognide/list_panes.jinx +8 -0
- npcsh/npc_team/jinxs/incognide/navigate.jinx +10 -0
- npcsh/npc_team/jinxs/incognide/notify.jinx +10 -0
- npcsh/npc_team/jinxs/incognide/open_pane.jinx +13 -0
- npcsh/npc_team/jinxs/incognide/read_pane.jinx +9 -0
- npcsh/npc_team/jinxs/incognide/run_terminal.jinx +10 -0
- npcsh/npc_team/jinxs/incognide/send_message.jinx +10 -0
- npcsh/npc_team/jinxs/incognide/split_pane.jinx +12 -0
- npcsh/npc_team/jinxs/incognide/switch_npc.jinx +10 -0
- npcsh/npc_team/jinxs/incognide/switch_tab.jinx +10 -0
- npcsh/npc_team/jinxs/incognide/write_file.jinx +11 -0
- npcsh/npc_team/jinxs/incognide/zen_mode.jinx +9 -0
- npcsh/npc_team/jinxs/lib/browser/browser_action.jinx +4 -4
- npcsh/npc_team/jinxs/lib/browser/browser_screenshot.jinx +1 -1
- npcsh/npc_team/jinxs/lib/browser/open_browser.jinx +2 -2
- npcsh/npc_team/jinxs/lib/computer_use/click.jinx +2 -2
- npcsh/npc_team/jinxs/lib/computer_use/key_press.jinx +1 -1
- npcsh/npc_team/jinxs/lib/computer_use/launch_app.jinx +1 -1
- npcsh/npc_team/jinxs/lib/computer_use/screenshot.jinx +1 -1
- npcsh/npc_team/jinxs/lib/computer_use/trigger.jinx +2 -2
- npcsh/npc_team/jinxs/lib/computer_use/type_text.jinx +1 -1
- npcsh/npc_team/jinxs/lib/computer_use/wait.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/chat.jinx +4 -4
- npcsh/npc_team/jinxs/lib/core/cmd.jinx +4 -4
- npcsh/npc_team/jinxs/lib/core/compress.jinx +8 -8
- npcsh/npc_team/jinxs/lib/core/edit_file.jinx +3 -0
- npcsh/npc_team/jinxs/lib/core/ots.jinx +7 -7
- npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +44 -0
- npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +94 -0
- npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +96 -0
- npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +80 -0
- npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +51 -0
- npcsh/npc_team/jinxs/lib/core/search.jinx +52 -129
- npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/sleep.jinx +7 -7
- npcsh/npc_team/jinxs/lib/core/sql.jinx +7 -7
- npcsh/npc_team/jinxs/lib/orchestration/convene.jinx +7 -7
- npcsh/npc_team/jinxs/lib/orchestration/delegate.jinx +8 -9
- npcsh/npc_team/jinxs/lib/research/arxiv.jinx +2 -2
- npcsh/npc_team/jinxs/lib/research/paper_search.jinx +3 -3
- npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +2 -2
- npcsh/npc_team/jinxs/lib/utils/build.jinx +5 -5
- npcsh/npc_team/jinxs/lib/utils/compile.jinx +2 -2
- npcsh/npc_team/jinxs/lib/utils/help.jinx +1 -1
- npcsh/npc_team/jinxs/lib/utils/init.jinx +5 -5
- npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +1 -1
- npcsh/npc_team/jinxs/lib/utils/serve.jinx +2 -2
- npcsh/npc_team/jinxs/lib/utils/set.jinx +2 -2
- npcsh/npc_team/jinxs/lib/utils/switch.jinx +3 -3
- npcsh/npc_team/jinxs/lib/utils/switches.jinx +1 -1
- npcsh/npc_team/jinxs/lib/utils/teamviz.jinx +2 -2
- npcsh/npc_team/sibiji.npc +1 -1
- npcsh/npcsh.py +81 -43
- npcsh-1.1.17.data/data/npcsh/npc_team/add_tab.jinx +11 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/arxiv.jinx +2 -2
- npcsh-1.1.17.data/data/npcsh/npc_team/benchmark.jinx +146 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/browser_action.jinx +4 -4
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/browser_screenshot.jinx +1 -1
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/build.jinx +5 -5
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/chat.jinx +4 -4
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/click.jinx +2 -2
- npcsh-1.1.17.data/data/npcsh/npc_team/close_pane.jinx +9 -0
- npcsh-1.1.17.data/data/npcsh/npc_team/close_tab.jinx +10 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/cmd.jinx +4 -4
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/compile.jinx +2 -2
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/compress.jinx +8 -8
- npcsh-1.1.17.data/data/npcsh/npc_team/confirm.jinx +10 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/convene.jinx +7 -7
- npcsh-1.1.17.data/data/npcsh/npc_team/db_search.jinx +44 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/delegate.jinx +8 -9
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/edit_file.jinx +3 -0
- npcsh-1.1.17.data/data/npcsh/npc_team/file_search.jinx +94 -0
- npcsh-1.1.17.data/data/npcsh/npc_team/focus_pane.jinx +9 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/help.jinx +1 -1
- npcsh-1.1.16.data/data/npcsh/npc_team/npc-studio.jinx → npcsh-1.1.17.data/data/npcsh/npc_team/incognide.jinx +2 -2
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/init.jinx +5 -5
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/jinxs.jinx +1 -1
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/key_press.jinx +1 -1
- npcsh-1.1.17.data/data/npcsh/npc_team/kg_search.jinx +96 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/launch_app.jinx +1 -1
- npcsh-1.1.17.data/data/npcsh/npc_team/list_panes.jinx +8 -0
- npcsh-1.1.17.data/data/npcsh/npc_team/mem_search.jinx +80 -0
- npcsh-1.1.17.data/data/npcsh/npc_team/navigate.jinx +10 -0
- npcsh-1.1.17.data/data/npcsh/npc_team/notify.jinx +10 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/nql.jinx +7 -7
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/open_browser.jinx +2 -2
- npcsh-1.1.17.data/data/npcsh/npc_team/open_pane.jinx +13 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/ots.jinx +7 -7
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/paper_search.jinx +3 -3
- npcsh-1.1.17.data/data/npcsh/npc_team/read_pane.jinx +9 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/roll.jinx +20 -23
- npcsh-1.1.17.data/data/npcsh/npc_team/run_terminal.jinx +10 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/sample.jinx +6 -7
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/screenshot.jinx +1 -1
- npcsh-1.1.17.data/data/npcsh/npc_team/search.jinx +54 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/semantic_scholar.jinx +2 -2
- npcsh-1.1.17.data/data/npcsh/npc_team/send_message.jinx +10 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/serve.jinx +2 -2
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/set.jinx +2 -2
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/sh.jinx +1 -1
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/sibiji.npc +1 -1
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/sleep.jinx +7 -7
- npcsh-1.1.17.data/data/npcsh/npc_team/split_pane.jinx +12 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/spool.jinx +4 -4
- npcsh-1.1.17.data/data/npcsh/npc_team/sql.jinx +16 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/switch.jinx +3 -3
- npcsh-1.1.17.data/data/npcsh/npc_team/switch_npc.jinx +10 -0
- npcsh-1.1.17.data/data/npcsh/npc_team/switch_tab.jinx +10 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/switches.jinx +1 -1
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/sync.jinx +6 -6
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/teamviz.jinx +2 -2
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/trigger.jinx +2 -2
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/type_text.jinx +1 -1
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/vixynt.jinx +8 -8
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/wait.jinx +1 -1
- npcsh-1.1.17.data/data/npcsh/npc_team/wander.jinx +242 -0
- npcsh-1.1.17.data/data/npcsh/npc_team/web_search.jinx +51 -0
- npcsh-1.1.17.data/data/npcsh/npc_team/write_file.jinx +11 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/yap.jinx +5 -5
- npcsh-1.1.17.data/data/npcsh/npc_team/zen_mode.jinx +9 -0
- {npcsh-1.1.16.dist-info → npcsh-1.1.17.dist-info}/METADATA +10 -7
- npcsh-1.1.17.dist-info/RECORD +219 -0
- {npcsh-1.1.16.dist-info → npcsh-1.1.17.dist-info}/entry_points.txt +2 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/search.jinx +0 -131
- npcsh-1.1.16.data/data/npcsh/npc_team/sql.jinx +0 -16
- npcsh-1.1.16.data/data/npcsh/npc_team/wander.jinx +0 -152
- npcsh-1.1.16.dist-info/RECORD +0 -170
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/alicanto.npc +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/corca.npc +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/frederic.npc +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/guac.npc +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/load_file.jinx +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/paste.jinx +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/plonk.npc +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/shh.jinx +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/verbose.jinx +0 -0
- {npcsh-1.1.16.data → npcsh-1.1.17.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.16.dist-info → npcsh-1.1.17.dist-info}/WHEEL +0 -0
- {npcsh-1.1.16.dist-info → npcsh-1.1.17.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.1.16.dist-info → npcsh-1.1.17.dist-info}/top_level.txt +0 -0
npcsh/_state.py
CHANGED
|
@@ -351,8 +351,7 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
|
|
|
351
351
|
None
|
|
352
352
|
"""
|
|
353
353
|
|
|
354
|
-
|
|
355
|
-
return
|
|
354
|
+
already_initialized = is_npcsh_initialized()
|
|
356
355
|
|
|
357
356
|
conn = sqlite3.connect(db_path)
|
|
358
357
|
cursor = conn.cursor()
|
|
@@ -508,8 +507,10 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
|
|
|
508
507
|
print(f"Copied template {file} to {destination_template_path}")
|
|
509
508
|
conn.commit()
|
|
510
509
|
conn.close()
|
|
511
|
-
|
|
512
|
-
|
|
510
|
+
|
|
511
|
+
if not already_initialized:
|
|
512
|
+
set_npcsh_initialized()
|
|
513
|
+
add_npcshrc_to_shell_config()
|
|
513
514
|
|
|
514
515
|
|
|
515
516
|
def get_shell_config_file() -> str:
|
|
@@ -2540,8 +2541,19 @@ def collect_llm_tools(state: ShellState) -> Tuple[List[Dict[str, Any]], Dict[str
|
|
|
2540
2541
|
if not jinja_env_for_jinx and state.team and isinstance(state.team, Team):
|
|
2541
2542
|
jinja_env_for_jinx = getattr(state.team, "jinja_env", None)
|
|
2542
2543
|
|
|
2544
|
+
jinx_globals = {
|
|
2545
|
+
"state": state,
|
|
2546
|
+
"CommandHistory": CommandHistory,
|
|
2547
|
+
"load_kg_from_db": load_kg_from_db,
|
|
2548
|
+
"execute_rag_command": execute_rag_command,
|
|
2549
|
+
"execute_brainblast_command": execute_brainblast_command,
|
|
2550
|
+
"load_file_contents": load_file_contents,
|
|
2551
|
+
"search_web": search_web,
|
|
2552
|
+
"get_relevant_memories": get_relevant_memories,
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2543
2555
|
for name, jinx_obj in aggregated_jinxs.items():
|
|
2544
|
-
def _make_runner(jinx=jinx_obj, jinja_env=jinja_env_for_jinx, tool_name=name):
|
|
2556
|
+
def _make_runner(jinx=jinx_obj, jinja_env=jinja_env_for_jinx, tool_name=name, extras=jinx_globals):
|
|
2545
2557
|
def runner(**kwargs):
|
|
2546
2558
|
input_values = kwargs if isinstance(kwargs, dict) else {}
|
|
2547
2559
|
try:
|
|
@@ -2549,7 +2561,7 @@ def collect_llm_tools(state: ShellState) -> Tuple[List[Dict[str, Any]], Dict[str
|
|
|
2549
2561
|
input_values=input_values,
|
|
2550
2562
|
npc=npc_obj,
|
|
2551
2563
|
messages=state.messages,
|
|
2552
|
-
extra_globals=
|
|
2564
|
+
extra_globals=extras,
|
|
2553
2565
|
jinja_env=jinja_env
|
|
2554
2566
|
)
|
|
2555
2567
|
return ctx.get("output", ctx)
|
|
@@ -2945,14 +2957,17 @@ def process_pipeline_command(
|
|
|
2945
2957
|
tool_name = msg.get("name", "tool")
|
|
2946
2958
|
tool_content = msg.get("content", "")
|
|
2947
2959
|
if tool_content and tool_content.strip():
|
|
2960
|
+
# Decode escaped newlines if present
|
|
2961
|
+
if isinstance(tool_content, str):
|
|
2962
|
+
tool_content = tool_content.replace('\\n', '\n').replace('\\t', '\t')
|
|
2948
2963
|
print(colored(f"\n⚡ {tool_name}:", "cyan"))
|
|
2949
2964
|
lines = tool_content.split('\n')
|
|
2950
2965
|
if len(lines) > 50:
|
|
2951
|
-
|
|
2966
|
+
render_markdown('\n'.join(lines[:25]))
|
|
2952
2967
|
print(colored(f"\n... ({len(lines) - 50} lines hidden) ...\n", "white", attrs=["dark"]))
|
|
2953
|
-
|
|
2968
|
+
render_markdown('\n'.join(lines[-25:]))
|
|
2954
2969
|
else:
|
|
2955
|
-
|
|
2970
|
+
render_markdown(tool_content)
|
|
2956
2971
|
|
|
2957
2972
|
# Check if LLM made tool calls - if not, it's done
|
|
2958
2973
|
tool_calls_made = isinstance(llm_result, dict) and llm_result.get("tool_calls")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
npcsh benchmark integration for Terminal-Bench.
|
|
3
|
+
|
|
4
|
+
This module provides integration with Terminal-Bench (tbench.ai) for benchmarking
|
|
5
|
+
npcsh against standardized terminal/CLI agent evaluation tasks.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
# Install terminal-bench
|
|
9
|
+
pip install terminal-bench harbor
|
|
10
|
+
|
|
11
|
+
# Run benchmarks with npcsh
|
|
12
|
+
harbor run -d terminal-bench@2.0 --agent-import-path npcsh.benchmark:NpcshAgent -m anthropic/claude-sonnet-4-20250514
|
|
13
|
+
|
|
14
|
+
# Or use the convenience function
|
|
15
|
+
from npcsh.benchmark import run_benchmark
|
|
16
|
+
run_benchmark(model="claude-sonnet-4-20250514", provider="anthropic")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .npcsh_agent import NpcshAgent
|
|
20
|
+
from .runner import run_benchmark, BenchmarkRunner
|
|
21
|
+
|
|
22
|
+
__all__ = ["NpcshAgent", "run_benchmark", "BenchmarkRunner"]
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
npcsh Harbor Agent Adapter for Terminal-Bench.
|
|
3
|
+
|
|
4
|
+
This module implements the BaseInstalledAgent interface for running npcsh
|
|
5
|
+
as an agent in Terminal-Bench evaluations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import shlex
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from harbor.agents.installed.base import BaseInstalledAgent, ExecInput
|
|
15
|
+
from harbor.models.agent.context import AgentContext
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NpcshAgent(BaseInstalledAgent):
|
|
19
|
+
"""
|
|
20
|
+
Harbor agent adapter for npcsh.
|
|
21
|
+
|
|
22
|
+
This allows npcsh to be evaluated on Terminal-Bench tasks by:
|
|
23
|
+
1. Installing npcsh in the benchmark container
|
|
24
|
+
2. Running npcsh with the task instruction
|
|
25
|
+
3. Parsing output for token usage and results
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
harbor run -d terminal-bench@2.0 \\
|
|
29
|
+
--agent-import-path npcsh.benchmark:NpcshAgent \\
|
|
30
|
+
-m anthropic/claude-sonnet-4-20250514 -n 4
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
SUPPORTS_ATIF = True # Agent Trajectory Interchange Format
|
|
34
|
+
|
|
35
|
+
def __init__(self, logs_dir: Path = None, model_name: str = None, logger=None, **kwargs):
|
|
36
|
+
super().__init__(logs_dir=logs_dir, model_name=model_name, logger=logger, **kwargs)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def name() -> str:
|
|
40
|
+
return "npcsh"
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def _install_agent_template_path(self) -> Path:
|
|
44
|
+
"""Path to the jinja template script for installing npcsh in the container."""
|
|
45
|
+
return Path(__file__).parent / "templates" / "install-npcsh.sh.j2"
|
|
46
|
+
|
|
47
|
+
def create_run_agent_commands(self, instruction: str) -> list:
|
|
48
|
+
"""
|
|
49
|
+
Create the commands to run npcsh in the container.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
instruction: The task instruction from Terminal-Bench
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
List of ExecInput commands to execute
|
|
56
|
+
"""
|
|
57
|
+
escaped_instruction = shlex.quote(instruction)
|
|
58
|
+
model_name = self.model_name
|
|
59
|
+
|
|
60
|
+
if model_name and "/" in model_name:
|
|
61
|
+
provider, model = model_name.split("/", 1)
|
|
62
|
+
elif model_name:
|
|
63
|
+
provider = os.environ.get("NPCSH_CHAT_PROVIDER", "")
|
|
64
|
+
model = model_name
|
|
65
|
+
else:
|
|
66
|
+
provider = os.environ.get("NPCSH_CHAT_PROVIDER", "")
|
|
67
|
+
model = os.environ.get("NPCSH_CHAT_MODEL", "")
|
|
68
|
+
|
|
69
|
+
# Map provider names to npcsh provider format
|
|
70
|
+
provider_map = {
|
|
71
|
+
"anthropic": "anthropic",
|
|
72
|
+
"openai": "openai",
|
|
73
|
+
"google": "gemini",
|
|
74
|
+
"gemini": "gemini",
|
|
75
|
+
"deepseek": "deepseek",
|
|
76
|
+
"ollama": "ollama",
|
|
77
|
+
"groq": "groq",
|
|
78
|
+
"openrouter": "openrouter",
|
|
79
|
+
}
|
|
80
|
+
npcsh_provider = provider_map.get(provider.lower(), provider)
|
|
81
|
+
|
|
82
|
+
# Build environment variables for API keys
|
|
83
|
+
env_vars = []
|
|
84
|
+
api_key_map = {
|
|
85
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
86
|
+
"openai": "OPENAI_API_KEY",
|
|
87
|
+
"gemini": "GOOGLE_API_KEY",
|
|
88
|
+
"google": "GOOGLE_API_KEY",
|
|
89
|
+
"deepseek": "DEEPSEEK_API_KEY",
|
|
90
|
+
"groq": "GROQ_API_KEY",
|
|
91
|
+
"openrouter": "OPENROUTER_API_KEY",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for prov, env_key in api_key_map.items():
|
|
95
|
+
if env_key in os.environ:
|
|
96
|
+
env_vars.append(f'{env_key}="{os.environ[env_key]}"')
|
|
97
|
+
|
|
98
|
+
env_prefix = " ".join(env_vars) + " " if env_vars else ""
|
|
99
|
+
|
|
100
|
+
# Output directory for logs
|
|
101
|
+
output_dir = str(self.logs_dir / "npcsh_output")
|
|
102
|
+
output_file = str(self.logs_dir / "npcsh_output" / "output.jsonl")
|
|
103
|
+
|
|
104
|
+
commands = []
|
|
105
|
+
|
|
106
|
+
# Create output directory
|
|
107
|
+
commands.append(ExecInput(
|
|
108
|
+
cmd=f"mkdir -p {shlex.quote(output_dir)}",
|
|
109
|
+
timeout=30
|
|
110
|
+
))
|
|
111
|
+
|
|
112
|
+
# Run npcsh with the instruction
|
|
113
|
+
# Using the npc CLI which supports single-command execution
|
|
114
|
+
npcsh_cmd = (
|
|
115
|
+
f'{env_prefix}'
|
|
116
|
+
f'NPCSH_CHAT_MODEL="{model}" '
|
|
117
|
+
f'NPCSH_CHAT_PROVIDER="{npcsh_provider}" '
|
|
118
|
+
f'NPCSH_STREAM_OUTPUT=0 '
|
|
119
|
+
f'npc {escaped_instruction} '
|
|
120
|
+
f'2>&1 | tee {shlex.quote(output_file)}'
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
commands.append(ExecInput(
|
|
124
|
+
cmd=npcsh_cmd,
|
|
125
|
+
timeout=600, # 10 minute timeout for complex tasks
|
|
126
|
+
))
|
|
127
|
+
|
|
128
|
+
return commands
|
|
129
|
+
|
|
130
|
+
def populate_context_post_run(self, context: AgentContext) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Populate the context with results of the agent execution.
|
|
133
|
+
|
|
134
|
+
Parses the output file to extract token usage metrics.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
context: The AgentContext to populate with metrics
|
|
138
|
+
"""
|
|
139
|
+
output_file = self.logs_dir / "npcsh_output" / "output.jsonl"
|
|
140
|
+
|
|
141
|
+
total_input_tokens = 0
|
|
142
|
+
total_output_tokens = 0
|
|
143
|
+
total_cost_usd = 0.0
|
|
144
|
+
|
|
145
|
+
if output_file.exists():
|
|
146
|
+
try:
|
|
147
|
+
with open(output_file, 'r') as f:
|
|
148
|
+
content = f.read()
|
|
149
|
+
|
|
150
|
+
# Try to parse as JSONL first
|
|
151
|
+
for line in content.strip().split('\n'):
|
|
152
|
+
if not line.strip():
|
|
153
|
+
continue
|
|
154
|
+
try:
|
|
155
|
+
event = json.loads(line)
|
|
156
|
+
# Extract token usage from events if present
|
|
157
|
+
if isinstance(event, dict):
|
|
158
|
+
usage = event.get('usage', {})
|
|
159
|
+
total_input_tokens += usage.get('input_tokens', 0)
|
|
160
|
+
total_output_tokens += usage.get('output_tokens', 0)
|
|
161
|
+
total_cost_usd += usage.get('cost_usd', 0.0)
|
|
162
|
+
except json.JSONDecodeError:
|
|
163
|
+
# Not JSON, just regular output
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
self.logger.warning(f"Failed to parse npcsh output: {e}")
|
|
168
|
+
|
|
169
|
+
# Set context metrics
|
|
170
|
+
if hasattr(context, 'input_tokens'):
|
|
171
|
+
context.input_tokens = total_input_tokens
|
|
172
|
+
if hasattr(context, 'output_tokens'):
|
|
173
|
+
context.output_tokens = total_output_tokens
|
|
174
|
+
if hasattr(context, 'cost_usd'):
|
|
175
|
+
context.cost_usd = total_cost_usd
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class NpcshAgentWithNpc(NpcshAgent):
|
|
179
|
+
"""
|
|
180
|
+
Variant that uses a specific NPC for task execution.
|
|
181
|
+
|
|
182
|
+
This allows benchmarking specific NPCs like sibiji (orchestrator),
|
|
183
|
+
corca (coding), or custom NPCs.
|
|
184
|
+
|
|
185
|
+
Usage:
|
|
186
|
+
harbor run -d terminal-bench@2.0 \\
|
|
187
|
+
--agent-import-path "npcsh.benchmark:NpcshAgentWithNpc" \\
|
|
188
|
+
-m anthropic/claude-sonnet-4-20250514 -n 4
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def __init__(self, *args, npc_name: str = "sibiji", **kwargs):
|
|
192
|
+
super().__init__(*args, **kwargs)
|
|
193
|
+
self.npc_name = npc_name
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def name() -> str:
|
|
197
|
+
return "npcsh-npc"
|
|
198
|
+
|
|
199
|
+
def create_run_agent_commands(self, instruction: str) -> list:
|
|
200
|
+
"""Create commands using a specific NPC."""
|
|
201
|
+
escaped_instruction = shlex.quote(instruction)
|
|
202
|
+
model_name = self.model_name
|
|
203
|
+
|
|
204
|
+
if model_name and "/" in model_name:
|
|
205
|
+
provider, model = model_name.split("/", 1)
|
|
206
|
+
elif model_name:
|
|
207
|
+
provider = os.environ.get("NPCSH_CHAT_PROVIDER", "")
|
|
208
|
+
model = model_name
|
|
209
|
+
else:
|
|
210
|
+
provider = os.environ.get("NPCSH_CHAT_PROVIDER", "")
|
|
211
|
+
model = os.environ.get("NPCSH_CHAT_MODEL", "")
|
|
212
|
+
|
|
213
|
+
provider_map = {
|
|
214
|
+
"anthropic": "anthropic",
|
|
215
|
+
"openai": "openai",
|
|
216
|
+
"google": "gemini",
|
|
217
|
+
"gemini": "gemini",
|
|
218
|
+
"deepseek": "deepseek",
|
|
219
|
+
"ollama": "ollama",
|
|
220
|
+
}
|
|
221
|
+
npcsh_provider = provider_map.get(provider.lower(), provider)
|
|
222
|
+
|
|
223
|
+
env_vars = []
|
|
224
|
+
api_key_map = {
|
|
225
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
226
|
+
"openai": "OPENAI_API_KEY",
|
|
227
|
+
"gemini": "GOOGLE_API_KEY",
|
|
228
|
+
"deepseek": "DEEPSEEK_API_KEY",
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for prov, env_key in api_key_map.items():
|
|
232
|
+
if env_key in os.environ:
|
|
233
|
+
env_vars.append(f'{env_key}="{os.environ[env_key]}"')
|
|
234
|
+
|
|
235
|
+
env_prefix = " ".join(env_vars) + " " if env_vars else ""
|
|
236
|
+
|
|
237
|
+
output_dir = str(self.logs_dir / "npcsh_output")
|
|
238
|
+
output_file = str(self.logs_dir / "npcsh_output" / "output.jsonl")
|
|
239
|
+
|
|
240
|
+
commands = []
|
|
241
|
+
|
|
242
|
+
commands.append(ExecInput(
|
|
243
|
+
cmd=f"mkdir -p {shlex.quote(output_dir)}",
|
|
244
|
+
timeout=30
|
|
245
|
+
))
|
|
246
|
+
|
|
247
|
+
# Use specific NPC with --npc flag
|
|
248
|
+
npcsh_cmd = (
|
|
249
|
+
f'{env_prefix}'
|
|
250
|
+
f'NPCSH_CHAT_MODEL="{model}" '
|
|
251
|
+
f'NPCSH_CHAT_PROVIDER="{npcsh_provider}" '
|
|
252
|
+
f'NPCSH_STREAM_OUTPUT=0 '
|
|
253
|
+
f'npc --npc {self.npc_name} {escaped_instruction} '
|
|
254
|
+
f'2>&1 | tee {shlex.quote(output_file)}'
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
commands.append(ExecInput(
|
|
258
|
+
cmd=npcsh_cmd,
|
|
259
|
+
timeout=600,
|
|
260
|
+
))
|
|
261
|
+
|
|
262
|
+
return commands
|