npcsh 1.1.21__py3-none-any.whl → 1.1.23__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 +282 -125
- npcsh/benchmark/npcsh_agent.py +77 -232
- npcsh/benchmark/templates/install-npcsh.sh.j2 +12 -4
- npcsh/config.py +5 -2
- npcsh/mcp_server.py +9 -1
- npcsh/npc_team/alicanto.npc +8 -6
- npcsh/npc_team/corca.npc +5 -12
- npcsh/npc_team/frederic.npc +6 -9
- npcsh/npc_team/guac.npc +4 -4
- npcsh/npc_team/jinxs/lib/core/delegate.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/edit_file.jinx +84 -62
- npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/skill.jinx +59 -0
- npcsh/npc_team/jinxs/lib/utils/help.jinx +194 -10
- npcsh/npc_team/jinxs/lib/utils/init.jinx +528 -37
- npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -1
- npcsh/npc_team/jinxs/lib/utils/serve.jinx +938 -21
- npcsh/npc_team/jinxs/modes/alicanto.jinx +102 -41
- npcsh/npc_team/jinxs/modes/build.jinx +378 -0
- npcsh-1.1.21.data/data/npcsh/npc_team/config_tui.jinx → npcsh/npc_team/jinxs/modes/config.jinx +1 -1
- npcsh/npc_team/jinxs/modes/convene.jinx +670 -0
- npcsh/npc_team/jinxs/modes/corca.jinx +777 -387
- npcsh/npc_team/jinxs/modes/crond.jinx +818 -0
- npcsh/npc_team/jinxs/modes/kg.jinx +69 -2
- npcsh/npc_team/jinxs/modes/plonk.jinx +86 -15
- npcsh/npc_team/jinxs/modes/roll.jinx +368 -55
- npcsh/npc_team/jinxs/modes/skills.jinx +621 -0
- npcsh/npc_team/jinxs/modes/yap.jinx +1092 -177
- npcsh/npc_team/jinxs/skills/code-review/SKILL.md +45 -0
- npcsh/npc_team/jinxs/skills/debugging/SKILL.md +44 -0
- npcsh/npc_team/jinxs/skills/git-workflow.jinx +44 -0
- npcsh/npc_team/kadiefa.npc +6 -6
- npcsh/npc_team/npcsh.ctx +16 -0
- npcsh/npc_team/plonk.npc +5 -9
- npcsh/npc_team/sibiji.npc +15 -7
- npcsh/npcsh.py +1 -0
- npcsh/routes.py +0 -4
- npcsh/yap.py +22 -4
- npcsh-1.1.23.data/data/npcsh/npc_team/SKILL.md +44 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.jinx +102 -41
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.npc +8 -6
- npcsh-1.1.23.data/data/npcsh/npc_team/build.jinx +378 -0
- npcsh/npc_team/jinxs/modes/config_tui.jinx → npcsh-1.1.23.data/data/npcsh/npc_team/config.jinx +1 -1
- npcsh-1.1.23.data/data/npcsh/npc_team/convene.jinx +670 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/corca.jinx +820 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.npc +5 -12
- npcsh-1.1.23.data/data/npcsh/npc_team/crond.jinx +818 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/delegate.jinx +1 -1
- npcsh-1.1.23.data/data/npcsh/npc_team/edit_file.jinx +119 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic.npc +6 -9
- npcsh-1.1.23.data/data/npcsh/npc_team/git-workflow.jinx +44 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.npc +4 -4
- npcsh-1.1.23.data/data/npcsh/npc_team/help.jinx +236 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/init.jinx +532 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/jinxs.jinx +0 -1
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.npc +6 -6
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kg.jinx +69 -2
- npcsh-1.1.23.data/data/npcsh/npc_team/npcsh.ctx +34 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.jinx +86 -15
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.npc +5 -9
- npcsh-1.1.23.data/data/npcsh/npc_team/roll.jinx +378 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/serve.jinx +943 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sh.jinx +1 -1
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.npc +15 -7
- npcsh-1.1.23.data/data/npcsh/npc_team/skill.jinx +59 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/skills.jinx +621 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/yap.jinx +1190 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/METADATA +404 -278
- npcsh-1.1.23.dist-info/RECORD +216 -0
- npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -11
- npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -8
- npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/notify.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -13
- npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -12
- npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -11
- npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -9
- npcsh/npc_team/jinxs/lib/core/convene.jinx +0 -232
- npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +0 -429
- npcsh/npc_team/jinxs/lib/core/search.jinx +0 -54
- npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -65
- npcsh-1.1.21.data/data/npcsh/npc_team/add_tab.jinx +0 -11
- npcsh-1.1.21.data/data/npcsh/npc_team/build.jinx +0 -65
- npcsh-1.1.21.data/data/npcsh/npc_team/close_pane.jinx +0 -9
- npcsh-1.1.21.data/data/npcsh/npc_team/close_tab.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/confirm.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/convene.jinx +0 -232
- npcsh-1.1.21.data/data/npcsh/npc_team/corca.jinx +0 -430
- npcsh-1.1.21.data/data/npcsh/npc_team/edit_file.jinx +0 -97
- npcsh-1.1.21.data/data/npcsh/npc_team/focus_pane.jinx +0 -9
- npcsh-1.1.21.data/data/npcsh/npc_team/help.jinx +0 -52
- npcsh-1.1.21.data/data/npcsh/npc_team/init.jinx +0 -41
- npcsh-1.1.21.data/data/npcsh/npc_team/kg_search.jinx +0 -429
- npcsh-1.1.21.data/data/npcsh/npc_team/list_panes.jinx +0 -8
- npcsh-1.1.21.data/data/npcsh/npc_team/navigate.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/notify.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/npcsh.ctx +0 -18
- npcsh-1.1.21.data/data/npcsh/npc_team/open_pane.jinx +0 -13
- npcsh-1.1.21.data/data/npcsh/npc_team/read_pane.jinx +0 -9
- npcsh-1.1.21.data/data/npcsh/npc_team/roll.jinx +0 -65
- npcsh-1.1.21.data/data/npcsh/npc_team/run_terminal.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/search.jinx +0 -54
- npcsh-1.1.21.data/data/npcsh/npc_team/send_message.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/serve.jinx +0 -26
- npcsh-1.1.21.data/data/npcsh/npc_team/split_pane.jinx +0 -12
- npcsh-1.1.21.data/data/npcsh/npc_team/switch_npc.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/switch_tab.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/write_file.jinx +0 -11
- npcsh-1.1.21.data/data/npcsh/npc_team/yap.jinx +0 -275
- npcsh-1.1.21.data/data/npcsh/npc_team/zen_mode.jinx +0 -9
- npcsh-1.1.21.dist-info/RECORD +0 -243
- /npcsh/npc_team/jinxs/lib/{core → utils}/chat.jinx +0 -0
- /npcsh/npc_team/jinxs/lib/{core → utils}/cmd.jinx +0 -0
- /npcsh/npc_team/jinxs/{incognide → lib/utils}/incognide.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/click.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/db_search.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/file_search.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/git.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/incognide.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/key_press.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/load_file.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/memories.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/models.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/nql.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/papers.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/paste.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/pti.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/reattach.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/setup.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/shh.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sleep.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sql.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switch.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switches.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sync.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/team.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/type_text.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/verbose.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wait.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wander.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/web_search.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/WHEEL +0 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/entry_points.txt +0 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/top_level.txt +0 -0
npcsh/_state.py
CHANGED
|
@@ -190,6 +190,8 @@ class ShellState:
|
|
|
190
190
|
edit_approval: str = NPCSH_EDIT_APPROVAL
|
|
191
191
|
# Pending file edits for approval
|
|
192
192
|
pending_edits: Dict[str, Dict[str, str]] = field(default_factory=dict)
|
|
193
|
+
# Command history for jinx execution logging
|
|
194
|
+
command_history: Optional[Any] = None
|
|
193
195
|
|
|
194
196
|
def get_model_for_command(self, model_type: str = "chat"):
|
|
195
197
|
if model_type == "chat":
|
|
@@ -278,6 +280,9 @@ CONFIG_KEY_MAP = {
|
|
|
278
280
|
"buildkg": "NPCSH_BUILD_KG",
|
|
279
281
|
"editapproval": "NPCSH_EDIT_APPROVAL",
|
|
280
282
|
"approval": "NPCSH_EDIT_APPROVAL",
|
|
283
|
+
"ttsengine": "NPCSH_TTS_ENGINE",
|
|
284
|
+
"ttsvoice": "NPCSH_TTS_VOICE",
|
|
285
|
+
"yapsetup": "NPCSH_YAP_SETUP_DONE",
|
|
281
286
|
}
|
|
282
287
|
|
|
283
288
|
|
|
@@ -1671,10 +1676,19 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
|
|
|
1671
1676
|
try:
|
|
1672
1677
|
import shutil
|
|
1673
1678
|
term_width = shutil.get_terminal_size().columns
|
|
1674
|
-
except
|
|
1679
|
+
except Exception:
|
|
1675
1680
|
term_width = 80
|
|
1676
1681
|
|
|
1682
|
+
# Track how many hint lines were drawn last time (for clearing on redraw)
|
|
1683
|
+
_prev_hint_lines = 1
|
|
1684
|
+
|
|
1685
|
+
# Tab completion state
|
|
1686
|
+
_tab_matches = []
|
|
1687
|
+
_tab_index = -1
|
|
1688
|
+
_tab_prefix = ""
|
|
1689
|
+
|
|
1677
1690
|
def draw():
|
|
1691
|
+
nonlocal _prev_hint_lines
|
|
1678
1692
|
# Calculate how many lines the input takes
|
|
1679
1693
|
total_len = prompt_visible_len + len(buf)
|
|
1680
1694
|
num_lines = (total_len // term_width) + 1
|
|
@@ -1688,18 +1702,21 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
|
|
|
1688
1702
|
for _ in range(num_lines - 1):
|
|
1689
1703
|
sys.stdout.write('\033[A')
|
|
1690
1704
|
|
|
1691
|
-
# Clear from cursor to end of screen (clears all wrapped lines + hint)
|
|
1705
|
+
# Clear from cursor to end of screen (clears all wrapped lines + all hint lines below)
|
|
1692
1706
|
sys.stdout.write('\033[J')
|
|
1693
1707
|
|
|
1694
1708
|
# Print prompt and buffer
|
|
1695
1709
|
sys.stdout.write(prompt + buf)
|
|
1696
1710
|
|
|
1697
|
-
# Print hint on next line
|
|
1698
|
-
|
|
1711
|
+
# Print hint on next line(s) - hint may contain newlines
|
|
1712
|
+
hint = current_hint()
|
|
1713
|
+
hint_line_count = hint.count('\n') + 1 if hint else 1
|
|
1714
|
+
_prev_hint_lines = hint_line_count
|
|
1715
|
+
sys.stdout.write('\n' + hint)
|
|
1699
1716
|
|
|
1700
1717
|
# Now position cursor back to correct spot
|
|
1701
1718
|
# Go back up to the line where cursor should be
|
|
1702
|
-
lines_after_cursor = (total_len // term_width) - (cursor_total // term_width) +
|
|
1719
|
+
lines_after_cursor = (total_len // term_width) - (cursor_total // term_width) + hint_line_count
|
|
1703
1720
|
for _ in range(lines_after_cursor):
|
|
1704
1721
|
sys.stdout.write('\033[A')
|
|
1705
1722
|
|
|
@@ -1924,6 +1941,9 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
|
|
|
1924
1941
|
if pos > 0:
|
|
1925
1942
|
buf = buf[:pos-1] + buf[pos:]
|
|
1926
1943
|
pos -= 1
|
|
1944
|
+
_tab_matches = []
|
|
1945
|
+
_tab_index = -1
|
|
1946
|
+
_tab_prefix = ""
|
|
1927
1947
|
draw()
|
|
1928
1948
|
|
|
1929
1949
|
elif c == '\x03': # Ctrl-C
|
|
@@ -1941,10 +1961,26 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
|
|
|
1941
1961
|
buf = ""
|
|
1942
1962
|
pos = 0
|
|
1943
1963
|
pasted_content = None
|
|
1944
|
-
|
|
1964
|
+
_tab_matches = []
|
|
1965
|
+
_tab_index = -1
|
|
1966
|
+
_tab_prefix = ""
|
|
1967
|
+
# Clear all previous hint lines + current input
|
|
1968
|
+
sys.stdout.write('\r')
|
|
1969
|
+
for _ in range(_prev_hint_lines):
|
|
1970
|
+
sys.stdout.write('\033[B')
|
|
1971
|
+
sys.stdout.write('\033[J') # Clear from cursor to end
|
|
1972
|
+
for _ in range(_prev_hint_lines):
|
|
1973
|
+
sys.stdout.write('\033[A')
|
|
1974
|
+
sys.stdout.write('\r\033[K')
|
|
1945
1975
|
sys.stdout.write('^C\n')
|
|
1946
|
-
# Redraw prompt
|
|
1947
|
-
|
|
1976
|
+
# Redraw prompt with fresh hint
|
|
1977
|
+
hint = current_hint()
|
|
1978
|
+
_prev_hint_lines = hint.count('\n') + 1 if hint else 1
|
|
1979
|
+
sys.stdout.write(prompt + '\n' + hint)
|
|
1980
|
+
# Go back up past all hint lines
|
|
1981
|
+
for _ in range(_prev_hint_lines):
|
|
1982
|
+
sys.stdout.write('\033[A')
|
|
1983
|
+
sys.stdout.write('\r')
|
|
1948
1984
|
if prompt_visible_len > 0:
|
|
1949
1985
|
sys.stdout.write('\033[' + str(prompt_visible_len) + 'C')
|
|
1950
1986
|
sys.stdout.flush()
|
|
@@ -1981,8 +2017,55 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
|
|
|
1981
2017
|
pos -= 1
|
|
1982
2018
|
draw()
|
|
1983
2019
|
|
|
1984
|
-
elif c == '\t': # Tab -
|
|
1985
|
-
|
|
2020
|
+
elif c == '\t': # Tab - inline completion
|
|
2021
|
+
try:
|
|
2022
|
+
if _tab_matches and _tab_prefix == buf:
|
|
2023
|
+
# Subsequent Tab: cycle through matches
|
|
2024
|
+
_tab_index = (_tab_index + 1) % len(_tab_matches)
|
|
2025
|
+
buf = _tab_matches[_tab_index]
|
|
2026
|
+
pos = len(buf)
|
|
2027
|
+
draw()
|
|
2028
|
+
else:
|
|
2029
|
+
# First Tab: compute matches
|
|
2030
|
+
matches = []
|
|
2031
|
+
if buf.startswith('/'):
|
|
2032
|
+
cmds = _get_slash_commands_set(state, router, buf)
|
|
2033
|
+
matches = ['/' + c for c in sorted(cmds)]
|
|
2034
|
+
elif buf.startswith('@'):
|
|
2035
|
+
npcs = _get_npc_names_set(state, buf)
|
|
2036
|
+
matches = ['@' + n for n in sorted(npcs)]
|
|
2037
|
+
else:
|
|
2038
|
+
# File path completion for non-prefix input
|
|
2039
|
+
import glob as _glob
|
|
2040
|
+
pattern = buf + '*'
|
|
2041
|
+
matches = sorted(_glob.glob(pattern))
|
|
2042
|
+
|
|
2043
|
+
if len(matches) == 1:
|
|
2044
|
+
# Exact single match - autocomplete with trailing space
|
|
2045
|
+
buf = matches[0] + ' '
|
|
2046
|
+
pos = len(buf)
|
|
2047
|
+
_tab_matches = []
|
|
2048
|
+
_tab_index = -1
|
|
2049
|
+
_tab_prefix = ""
|
|
2050
|
+
draw()
|
|
2051
|
+
elif len(matches) > 1:
|
|
2052
|
+
# Find longest common prefix
|
|
2053
|
+
common = matches[0]
|
|
2054
|
+
for m in matches[1:]:
|
|
2055
|
+
while not m.startswith(common):
|
|
2056
|
+
common = common[:-1]
|
|
2057
|
+
if len(common) > len(buf):
|
|
2058
|
+
# Complete common prefix
|
|
2059
|
+
buf = common
|
|
2060
|
+
pos = len(buf)
|
|
2061
|
+
# Set up cycling state
|
|
2062
|
+
_tab_matches = matches
|
|
2063
|
+
_tab_index = -1
|
|
2064
|
+
_tab_prefix = buf
|
|
2065
|
+
draw()
|
|
2066
|
+
except Exception:
|
|
2067
|
+
pass
|
|
2068
|
+
continue
|
|
1986
2069
|
|
|
1987
2070
|
elif c == '\x0f': # Ctrl-O - show last tool call args
|
|
1988
2071
|
try:
|
|
@@ -2022,6 +2105,9 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
|
|
|
2022
2105
|
elif c and ord(c) >= 32: # Printable
|
|
2023
2106
|
buf = buf[:pos] + c + buf[pos:]
|
|
2024
2107
|
pos += 1
|
|
2108
|
+
_tab_matches = []
|
|
2109
|
+
_tab_index = -1
|
|
2110
|
+
_tab_prefix = ""
|
|
2025
2111
|
draw()
|
|
2026
2112
|
|
|
2027
2113
|
finally:
|
|
@@ -2030,42 +2116,66 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
|
|
|
2030
2116
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
2031
2117
|
|
|
2032
2118
|
|
|
2033
|
-
def
|
|
2034
|
-
"""
|
|
2035
|
-
|
|
2119
|
+
def _get_slash_commands_set(state, router, prefix='/') -> set:
|
|
2120
|
+
"""Return the set of matching slash command names (without /)."""
|
|
2121
|
+
# Computer-use commands hidden from hints/tab-complete (still callable)
|
|
2122
|
+
_HIDDEN_CMDS = {
|
|
2123
|
+
'browser_action', 'browser_screenshot', 'click', 'close_browser',
|
|
2124
|
+
'key_press', 'launch_app', 'open_browser', 'screenshot',
|
|
2125
|
+
'trigger', 'type_text', 'wait',
|
|
2126
|
+
}
|
|
2127
|
+
cmds = {'help', 'set', 'agent', 'chat', 'cmd', 'sq', 'quit', 'exit', 'clear', 'npc', 'reattach'}
|
|
2036
2128
|
if state and state.team and hasattr(state.team, 'jinxs_dict'):
|
|
2037
2129
|
cmds.update(state.team.jinxs_dict.keys())
|
|
2038
2130
|
if router and hasattr(router, 'jinx_routes'):
|
|
2039
2131
|
cmds.update(router.jinx_routes.keys())
|
|
2132
|
+
cmds -= _HIDDEN_CMDS
|
|
2040
2133
|
if len(prefix) > 1:
|
|
2041
2134
|
f = prefix[1:].lower()
|
|
2042
2135
|
cmds = {c for c in cmds if c.lower().startswith(f)}
|
|
2136
|
+
return cmds
|
|
2137
|
+
|
|
2138
|
+
|
|
2139
|
+
def _layout_hints_multirow(items, term_width, style_fn) -> str:
|
|
2140
|
+
"""Lay out hint items across multiple rows that fit terminal width."""
|
|
2141
|
+
if not items:
|
|
2142
|
+
return ""
|
|
2143
|
+
rows = []
|
|
2144
|
+
current_row = []
|
|
2145
|
+
current_len = 2 # leading spaces
|
|
2146
|
+
for item in items:
|
|
2147
|
+
needed = len(item) + 2 # item + spacing
|
|
2148
|
+
if current_len + needed > term_width - 2 and current_row:
|
|
2149
|
+
rows.append(current_row)
|
|
2150
|
+
current_row = [item]
|
|
2151
|
+
current_len = 2 + len(item) + 2
|
|
2152
|
+
else:
|
|
2153
|
+
current_row.append(item)
|
|
2154
|
+
current_len += needed
|
|
2155
|
+
if current_row:
|
|
2156
|
+
rows.append(current_row)
|
|
2157
|
+
return '\n'.join(style_fn(' ' + ' '.join(row)) for row in rows)
|
|
2158
|
+
|
|
2159
|
+
|
|
2160
|
+
def _get_slash_hints(state, router, prefix='/') -> str:
|
|
2161
|
+
"""Slash command hints - shows ALL commands across multiple rows."""
|
|
2162
|
+
cmds = _get_slash_commands_set(state, router, prefix)
|
|
2043
2163
|
if cmds:
|
|
2044
|
-
# Get terminal width, default 80
|
|
2045
2164
|
try:
|
|
2046
2165
|
import shutil
|
|
2047
2166
|
term_width = shutil.get_terminal_size().columns
|
|
2048
2167
|
except Exception:
|
|
2049
2168
|
term_width = 80
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
for c in sorted_cmds:
|
|
2056
|
-
item = '/' + c
|
|
2057
|
-
if current_len + len(item) + 2 > term_width - 5: # leave margin
|
|
2058
|
-
break
|
|
2059
|
-
hint_parts.append(item)
|
|
2060
|
-
current_len += len(item) + 2
|
|
2061
|
-
|
|
2062
|
-
if hint_parts:
|
|
2063
|
-
return colored(' ' + ' '.join(hint_parts), 'white', attrs=['dark'])
|
|
2169
|
+
sorted_items = ['/' + c for c in sorted(cmds)]
|
|
2170
|
+
return _layout_hints_multirow(
|
|
2171
|
+
sorted_items, term_width,
|
|
2172
|
+
lambda s: colored(s, 'white', attrs=['dark'])
|
|
2173
|
+
)
|
|
2064
2174
|
return ""
|
|
2065
2175
|
|
|
2066
2176
|
|
|
2067
|
-
def
|
|
2068
|
-
"""NPC
|
|
2177
|
+
def _get_npc_names_set(state, prefix='@') -> set:
|
|
2178
|
+
"""Return the set of matching NPC names (without @)."""
|
|
2069
2179
|
npcs = set()
|
|
2070
2180
|
if state and state.team:
|
|
2071
2181
|
if hasattr(state.team, 'npcs') and state.team.npcs:
|
|
@@ -2077,8 +2187,23 @@ def _get_npc_hints(state, prefix='@') -> str:
|
|
|
2077
2187
|
if len(prefix) > 1:
|
|
2078
2188
|
f = prefix[1:].lower()
|
|
2079
2189
|
npcs = {n for n in npcs if n.lower().startswith(f)}
|
|
2190
|
+
return npcs
|
|
2191
|
+
|
|
2192
|
+
|
|
2193
|
+
def _get_npc_hints(state, prefix='@') -> str:
|
|
2194
|
+
"""NPC hints - shows all NPCs across multiple rows."""
|
|
2195
|
+
npcs = _get_npc_names_set(state, prefix)
|
|
2080
2196
|
if npcs:
|
|
2081
|
-
|
|
2197
|
+
try:
|
|
2198
|
+
import shutil
|
|
2199
|
+
term_width = shutil.get_terminal_size().columns
|
|
2200
|
+
except Exception:
|
|
2201
|
+
term_width = 80
|
|
2202
|
+
sorted_items = ['@' + n for n in sorted(npcs)]
|
|
2203
|
+
return _layout_hints_multirow(
|
|
2204
|
+
sorted_items, term_width,
|
|
2205
|
+
lambda s: colored(s, 'cyan')
|
|
2206
|
+
)
|
|
2082
2207
|
return ""
|
|
2083
2208
|
|
|
2084
2209
|
|
|
@@ -2374,55 +2499,28 @@ def parse_generic_command_flags(parts: List[str]) -> Tuple[Dict[str, Any], List[
|
|
|
2374
2499
|
|
|
2375
2500
|
return parsed_kwargs, positional_args
|
|
2376
2501
|
|
|
2377
|
-
def _ollama_supports_tools(model: str) -> Optional[bool]:
|
|
2378
|
-
"""
|
|
2379
|
-
Best-effort check for tool-call support on an Ollama model by inspecting its template/metadata.
|
|
2380
|
-
Mirrors the lightweight check used in the Flask serve path.
|
|
2381
|
-
"""
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
try:
|
|
2385
|
-
details = ollama.show(model)
|
|
2386
|
-
template = details.get("template") or ""
|
|
2387
|
-
metadata = details.get("metadata") or {}
|
|
2388
|
-
if any(token in template for token in ["{{- if .Tools", "{{- range .Tools", "{{- if .ToolCalls"]):
|
|
2389
|
-
return True
|
|
2390
|
-
if metadata.get("tools") or metadata.get("tool_calls"):
|
|
2391
|
-
return True
|
|
2392
|
-
return False
|
|
2393
|
-
except Exception:
|
|
2394
|
-
return None
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
2502
|
def model_supports_tool_calls(model: Optional[str], provider: Optional[str]) -> bool:
|
|
2398
|
-
"""
|
|
2399
|
-
Decide whether to attempt tool-calling for the given model/provider.
|
|
2400
|
-
Uses Ollama template inspection when possible and falls back to name heuristics.
|
|
2401
|
-
"""
|
|
2503
|
+
"""Check whether a model supports tool/function calling."""
|
|
2402
2504
|
if not model:
|
|
2403
2505
|
return False
|
|
2404
2506
|
|
|
2405
2507
|
provider = (provider or "").lower()
|
|
2406
|
-
model_lower = model.lower()
|
|
2407
2508
|
|
|
2509
|
+
# Ollama: use the capabilities field from the model metadata
|
|
2408
2510
|
if provider == "ollama":
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
"gemini",
|
|
2423
|
-
"tool",
|
|
2424
|
-
]
|
|
2425
|
-
return any(marker in model_lower for marker in toolish_markers)
|
|
2511
|
+
try:
|
|
2512
|
+
details = ollama.show(model)
|
|
2513
|
+
caps = getattr(details, "capabilities", None) or []
|
|
2514
|
+
return "tools" in caps
|
|
2515
|
+
except Exception:
|
|
2516
|
+
pass
|
|
2517
|
+
|
|
2518
|
+
# API providers: always support tools
|
|
2519
|
+
if provider in ("anthropic", "openai", "gemini", "google", "deepseek", "groq", "openrouter"):
|
|
2520
|
+
return True
|
|
2521
|
+
|
|
2522
|
+
# Unknown provider: assume yes
|
|
2523
|
+
return True
|
|
2426
2524
|
|
|
2427
2525
|
|
|
2428
2526
|
def wrap_tool_with_display(tool_name: str, tool_func: Callable, state: ShellState) -> Callable:
|
|
@@ -2537,19 +2635,16 @@ def collect_llm_tools(state: ShellState) -> Tuple[List[Dict[str, Any]], Dict[str
|
|
|
2537
2635
|
elif npc_obj and getattr(npc_obj, "tool_map", None):
|
|
2538
2636
|
tool_map.update(npc_obj.tool_map)
|
|
2539
2637
|
|
|
2540
|
-
# Jinx tools from NPC
|
|
2638
|
+
# Jinx tools from NPC only (NPC.jinxs_dict is already filtered by jinxs_spec
|
|
2639
|
+
# during initialize_jinxs - don't add the full team catalog which overwhelms small models)
|
|
2541
2640
|
aggregated_jinxs: Dict[str, Any] = {}
|
|
2542
2641
|
if npc_obj and getattr(npc_obj, "jinxs_dict", None):
|
|
2543
2642
|
aggregated_jinxs.update(npc_obj.jinxs_dict)
|
|
2544
|
-
if state.team and isinstance(state.team, Team) and getattr(state.team, "jinxs_dict", None):
|
|
2545
|
-
aggregated_jinxs.update({k: v for k, v in state.team.jinxs_dict.items() if k not in aggregated_jinxs})
|
|
2546
2643
|
|
|
2547
2644
|
if aggregated_jinxs:
|
|
2548
2645
|
jinx_catalog: Dict[str, Dict[str, Any]] = {}
|
|
2549
2646
|
if npc_obj and getattr(npc_obj, "jinx_tool_catalog", None):
|
|
2550
2647
|
jinx_catalog.update(npc_obj.jinx_tool_catalog or {})
|
|
2551
|
-
if state.team and isinstance(state.team, Team) and getattr(state.team, "jinx_tool_catalog", None):
|
|
2552
|
-
jinx_catalog.update(state.team.jinx_tool_catalog or {})
|
|
2553
2648
|
if not jinx_catalog:
|
|
2554
2649
|
jinx_catalog = build_jinx_tool_catalog(aggregated_jinxs)
|
|
2555
2650
|
|
|
@@ -2701,15 +2796,45 @@ def execute_slash_command(command: str,
|
|
|
2701
2796
|
'vmodel': state.vision_model, 'vprovider': state.vision_provider, 'rmodel': state.reasoning_model,
|
|
2702
2797
|
'rprovider': state.reasoning_provider, 'state': state
|
|
2703
2798
|
}
|
|
2799
|
+
import time as _time
|
|
2800
|
+
_start = _time.monotonic()
|
|
2801
|
+
_status = "success"
|
|
2802
|
+
_error = None
|
|
2704
2803
|
try:
|
|
2705
2804
|
result = handler(command=command, **handler_kwargs)
|
|
2706
|
-
if isinstance(result, dict):
|
|
2805
|
+
if isinstance(result, dict):
|
|
2707
2806
|
state.messages = result.get("messages", state.messages)
|
|
2708
|
-
return state, result
|
|
2709
2807
|
except Exception as e:
|
|
2710
2808
|
import traceback
|
|
2711
2809
|
traceback.print_exc()
|
|
2712
|
-
|
|
2810
|
+
_status = "error"
|
|
2811
|
+
_error = str(e)
|
|
2812
|
+
result = {"output": colored(f"Error executing slash command '{command_name}': {e}", "red"), "messages": state.messages}
|
|
2813
|
+
|
|
2814
|
+
_duration_ms = int((_time.monotonic() - _start) * 1000)
|
|
2815
|
+
|
|
2816
|
+
# Log jinx execution to jinx_execution_log
|
|
2817
|
+
if hasattr(state, 'command_history') and state.command_history is not None and hasattr(state.command_history, 'save_jinx_execution'):
|
|
2818
|
+
try:
|
|
2819
|
+
_npc_name = state.npc.name if isinstance(state.npc, NPC) else "npcsh"
|
|
2820
|
+
_team_name = state.team.name if state.team else "npcsh"
|
|
2821
|
+
_args = ' '.join(all_command_parts[1:]) if len(all_command_parts) > 1 else ''
|
|
2822
|
+
state.command_history.save_jinx_execution(
|
|
2823
|
+
triggering_message_id=None,
|
|
2824
|
+
conversation_id=state.conversation_id,
|
|
2825
|
+
npc_name=_npc_name,
|
|
2826
|
+
jinx_name=command_name,
|
|
2827
|
+
jinx_inputs={"command": command, "args": _args},
|
|
2828
|
+
jinx_output=result.get('output', '') if isinstance(result, dict) else str(result)[:2000],
|
|
2829
|
+
status=_status,
|
|
2830
|
+
team_name=_team_name,
|
|
2831
|
+
error_message=_error,
|
|
2832
|
+
duration_ms=_duration_ms,
|
|
2833
|
+
)
|
|
2834
|
+
except Exception:
|
|
2835
|
+
pass # Don't fail command execution due to logging error
|
|
2836
|
+
|
|
2837
|
+
return state, result
|
|
2713
2838
|
|
|
2714
2839
|
# Fallback for switching NPC by name
|
|
2715
2840
|
if state.team and command_name in state.team.npcs:
|
|
@@ -2758,46 +2883,13 @@ def process_pipeline_command(
|
|
|
2758
2883
|
exec_provider = provider_override or npc_provider or state.chat_provider
|
|
2759
2884
|
|
|
2760
2885
|
if cmd_to_process.startswith("/"):
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
is_interactive_mode = True
|
|
2769
|
-
|
|
2770
|
-
# Also check modes/ directory (legacy)
|
|
2771
|
-
if not is_interactive_mode:
|
|
2772
|
-
global_modes_jinx = os.path.expanduser(f'~/.npcsh/npc_team/jinxs/modes/{command_name}.jinx')
|
|
2773
|
-
if os.path.exists(global_modes_jinx):
|
|
2774
|
-
is_interactive_mode = True
|
|
2775
|
-
|
|
2776
|
-
if not is_interactive_mode and state.team and state.team.team_path:
|
|
2777
|
-
team_modes_jinx = os.path.join(state.team.team_path, 'jinxs', 'modes', f'{command_name}.jinx')
|
|
2778
|
-
if os.path.exists(team_modes_jinx):
|
|
2779
|
-
is_interactive_mode = True
|
|
2780
|
-
|
|
2781
|
-
if is_interactive_mode:
|
|
2782
|
-
result = execute_slash_command(
|
|
2783
|
-
cmd_to_process,
|
|
2784
|
-
stdin_input,
|
|
2785
|
-
state,
|
|
2786
|
-
stream_final,
|
|
2787
|
-
router
|
|
2788
|
-
)
|
|
2789
|
-
else:
|
|
2790
|
-
with SpinnerContext(
|
|
2791
|
-
f"Routing to {cmd_to_process.split()[0]}",
|
|
2792
|
-
style="arrow"
|
|
2793
|
-
):
|
|
2794
|
-
result = execute_slash_command(
|
|
2795
|
-
cmd_to_process,
|
|
2796
|
-
stdin_input,
|
|
2797
|
-
state,
|
|
2798
|
-
stream_final,
|
|
2799
|
-
router
|
|
2800
|
-
)
|
|
2886
|
+
result = execute_slash_command(
|
|
2887
|
+
cmd_to_process,
|
|
2888
|
+
stdin_input,
|
|
2889
|
+
state,
|
|
2890
|
+
stream_final,
|
|
2891
|
+
router
|
|
2892
|
+
)
|
|
2801
2893
|
return result
|
|
2802
2894
|
cmd_parts = parse_command_safely(cmd_to_process)
|
|
2803
2895
|
if not cmd_parts:
|
|
@@ -2898,6 +2990,14 @@ def process_pipeline_command(
|
|
|
2898
2990
|
tools_for_llm, tool_exec_map = collect_llm_tools(state)
|
|
2899
2991
|
if not tools_for_llm:
|
|
2900
2992
|
tool_capable = False
|
|
2993
|
+
else:
|
|
2994
|
+
# Add tool guidance so model knows to use function calls
|
|
2995
|
+
tool_names = [t['function']['name'] for t in tools_for_llm if 'function' in t]
|
|
2996
|
+
info += (
|
|
2997
|
+
f"\nYou have access to these tools: {', '.join(tool_names)}. "
|
|
2998
|
+
f"You MUST use the function calling interface to invoke them. "
|
|
2999
|
+
f"Do NOT write tool names as text - call them as functions."
|
|
3000
|
+
)
|
|
2901
3001
|
|
|
2902
3002
|
npc_name = (
|
|
2903
3003
|
state.npc.name
|
|
@@ -2949,10 +3049,22 @@ def process_pipeline_command(
|
|
|
2949
3049
|
iteration = 0
|
|
2950
3050
|
max_iterations = 50 # Safety limit to prevent infinite loops
|
|
2951
3051
|
total_usage = {"input_tokens": 0, "output_tokens": 0}
|
|
3052
|
+
state._agent_nudges = 0 # Track continuation nudges
|
|
3053
|
+
|
|
3054
|
+
tool_calls_count = 0 # Track how many tool calls have been made
|
|
2952
3055
|
|
|
2953
3056
|
while iteration < max_iterations:
|
|
2954
3057
|
iteration += 1
|
|
2955
3058
|
|
|
3059
|
+
# After a tool has been called, force the model to generate a
|
|
3060
|
+
# text response on the NEXT iteration by setting tool_choice='none'.
|
|
3061
|
+
# This prevents models from endlessly repeating tool calls when
|
|
3062
|
+
# they already have the answer. For multi-step tasks, delegate
|
|
3063
|
+
# and convene jinxs have their own internal loops.
|
|
3064
|
+
iter_kwargs = dict(llm_kwargs)
|
|
3065
|
+
if tool_calls_count > 0:
|
|
3066
|
+
iter_kwargs["tool_choice"] = 'none'
|
|
3067
|
+
|
|
2956
3068
|
llm_result = get_llm_response(
|
|
2957
3069
|
full_llm_cmd if iteration == 1 else None, # Only pass prompt on first call
|
|
2958
3070
|
model=exec_model,
|
|
@@ -2963,7 +3075,7 @@ def process_pipeline_command(
|
|
|
2963
3075
|
stream=False, # Don't stream intermediate calls
|
|
2964
3076
|
attachments=state.attachments if iteration == 1 else None,
|
|
2965
3077
|
context=info if iteration == 1 else None,
|
|
2966
|
-
**
|
|
3078
|
+
**iter_kwargs,
|
|
2967
3079
|
)
|
|
2968
3080
|
|
|
2969
3081
|
# Accumulate usage
|
|
@@ -2994,12 +3106,34 @@ def process_pipeline_command(
|
|
|
2994
3106
|
else:
|
|
2995
3107
|
render_markdown(tool_content)
|
|
2996
3108
|
|
|
2997
|
-
# Check if LLM made tool calls - if not,
|
|
3109
|
+
# Check if LLM made tool calls - if not, consider re-prompting
|
|
2998
3110
|
tool_calls_made = isinstance(llm_result, dict) and llm_result.get("tool_calls")
|
|
2999
3111
|
if not tool_calls_made:
|
|
3112
|
+
# In agent mode, nudge to use tools — but ONLY if no tools
|
|
3113
|
+
# have been called yet (model hasn't started working).
|
|
3114
|
+
# Once tools have been called and returned results,
|
|
3115
|
+
# the model should synthesize an answer, not call more tools.
|
|
3116
|
+
if (state.current_mode == 'agent'
|
|
3117
|
+
and tool_calls_count == 0
|
|
3118
|
+
and iteration < max_iterations
|
|
3119
|
+
and state._agent_nudges < 3):
|
|
3120
|
+
state._agent_nudges += 1
|
|
3121
|
+
state.messages.append({
|
|
3122
|
+
"role": "user",
|
|
3123
|
+
"content": (
|
|
3124
|
+
"You have not yet completed the task. "
|
|
3125
|
+
"Continue working by calling tools. "
|
|
3126
|
+
"Use the function calling interface to invoke tools - "
|
|
3127
|
+
"do NOT write tool calls as text."
|
|
3128
|
+
)
|
|
3129
|
+
})
|
|
3130
|
+
continue
|
|
3000
3131
|
# LLM is done - no more tool calls
|
|
3001
3132
|
break
|
|
3002
3133
|
|
|
3134
|
+
# Track tool calls and reset nudge counter
|
|
3135
|
+
tool_calls_count += 1
|
|
3136
|
+
state._agent_nudges = 0
|
|
3003
3137
|
# Clear the prompt for continuation calls - context is in messages
|
|
3004
3138
|
full_llm_cmd = None
|
|
3005
3139
|
|
|
@@ -3488,8 +3622,31 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
|
|
|
3488
3622
|
|
|
3489
3623
|
forenpc_path = os.path.join(team_dir, f"{forenpc_name}.npc")
|
|
3490
3624
|
|
|
3491
|
-
|
|
3492
|
-
|
|
3625
|
+
try:
|
|
3626
|
+
team = Team(team_path=team_dir, db_conn=command_history.engine)
|
|
3627
|
+
except FileNotFoundError as e:
|
|
3628
|
+
print(f"Warning: Team compilation failed - {e}")
|
|
3629
|
+
print("Auto-refreshing npc_team files from package...")
|
|
3630
|
+
|
|
3631
|
+
global_team_path = os.path.expanduser(DEFAULT_NPC_TEAM_PATH)
|
|
3632
|
+
if team_dir == global_team_path:
|
|
3633
|
+
user_jinxs_dir = os.path.join(global_team_path, "jinxs")
|
|
3634
|
+
if os.path.exists(user_jinxs_dir):
|
|
3635
|
+
print(f"Removing stale jinxs directory: {user_jinxs_dir}")
|
|
3636
|
+
shutil.rmtree(user_jinxs_dir)
|
|
3637
|
+
initialize_base_npcs_if_needed(db_path)
|
|
3638
|
+
print("npc_team files refreshed. Retrying team initialization...")
|
|
3639
|
+
try:
|
|
3640
|
+
team = Team(team_path=team_dir, db_conn=command_history.engine)
|
|
3641
|
+
except FileNotFoundError as retry_e:
|
|
3642
|
+
print(f"Error: Team initialization still failed after refresh: {retry_e}")
|
|
3643
|
+
print("This may indicate corrupted NPC files. Try: rm -rf ~/.npcsh/npc_team && npcsh")
|
|
3644
|
+
raise
|
|
3645
|
+
else:
|
|
3646
|
+
print(f"Error: Project team at '{team_dir}' has compilation errors.")
|
|
3647
|
+
print("Please check your .npc files and jinx references.")
|
|
3648
|
+
raise
|
|
3649
|
+
|
|
3493
3650
|
forenpc_obj = team.forenpc if hasattr(team, 'forenpc') and team.forenpc else None
|
|
3494
3651
|
|
|
3495
3652
|
for npc_name, npc_obj in team.npcs.items():
|