Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/workflows/agent-terminal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Agent terminal

on:
workflow_dispatch:
pull_request:
paths:
- 'tools/agent/**'
- 'common/console.*'
- '.github/workflows/agent-terminal.yml'
push:
branches: [master]
paths:
- 'tools/agent/**'
- 'common/console.*'
- '.github/workflows/agent-terminal.yml'

jobs:
terminal:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/setup-node@v4
if: runner.os == 'Linux'
with:
node-version: '22'
- name: Install terminal test dependencies
if: runner.os != 'Windows'
run: python -m pip install -r tools/agent/tests/requirements.txt
- name: Configure
shell: bash
run: |
terminal_tests=OFF
if [ "$RUNNER_OS" != Windows ]; then terminal_tests=ON; fi
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DGGML_NATIVE=OFF -DLLAMA_BUILD_UI=OFF -DLLAMA_HTTPLIB=ON -DLLAMA_BUILD_TESTS=ON -DLLAMA_AGENT_TERMINAL_TESTS=$terminal_tests -DPython3_EXECUTABLE="$(which python)"
- name: Build
run: cmake --build build --config Debug --parallel 3 --target llama-agent test-agent-tui test-agent-inference-backend test-agent-http-inference-backend test-agent-tool-parser test-agent-permission test-agent-resources test-agent-session-file test-agent-compaction
- name: Test
run: ctest --test-dir build -C Debug -R '^agent-' --output-on-failure
- name: Browser replay
if: runner.os == 'Linux'
run: |
npm install --prefix "$RUNNER_TEMP/tui-browser" @xterm/xterm@5.5.0 playwright@1.55.0
"$RUNNER_TEMP/tui-browser/node_modules/.bin/playwright" install --with-deps chromium
node tools/agent/tests/replay_terminal.mjs build/tools/agent/terminal-artifacts "$RUNNER_TEMP/tui-browser"
- uses: actions/upload-artifact@v4
if: always() && runner.os != 'Windows'
with:
name: terminal-${{ matrix.os }}
path: build/tools/agent/terminal-artifacts
51 changes: 50 additions & 1 deletion common/console.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <cwctype>
#include <cstdint>
#include <condition_variable>
#include <atomic>
#include <mutex>
#include <thread>
#include <stdarg.h>
Expand Down Expand Up @@ -72,6 +73,7 @@ namespace console {
static bool advanced_display = false;
static bool simple_io = true;
static display_type current_display = DISPLAY_TYPE_RESET;
static std::atomic<bool> tui_active = false;

static FILE* out = stdout;

Expand Down Expand Up @@ -99,11 +101,30 @@ namespace console {
}

std::vector<std::pair<std::vector<uint8_t>, std::string>> take_pending_images() {
std::lock_guard<std::mutex> lock(g_console_mutex);
std::vector<std::pair<std::vector<uint8_t>, std::string>> result;
result.swap(pending_images_);
return result;
}

bool try_paste_image_marker(std::string & marker) {
std::lock_guard<std::mutex> lock(g_console_mutex);
if (!paste_image_cb) {
return false;
}

std::vector<uint8_t> bytes;
std::string mime;
if (!paste_image_cb(bytes, mime)) {
return false;
}

pending_images_.push_back({std::move(bytes), std::move(mime)});
marker = pending_images_.size() == 1
? "[image]" : "[image " + std::to_string(pending_images_.size()) + "]";
return true;
}

//
// Init and cleanup
//
Expand Down Expand Up @@ -198,6 +219,18 @@ namespace console {
#endif
}

FILE * output_file() {
return out;
}

void set_tui_active(bool active) {
tui_active.store(active);
}

bool is_tui_active() {
return tui_active.load();
}

//
// Display and IO
//
Expand Down Expand Up @@ -1300,6 +1333,18 @@ namespace console {
completion_cb = cb;
}

std::vector<std::pair<std::string, size_t>> complete(std::string_view line, size_t cursor_byte_pos) {
completion_callback cb;
{
std::lock_guard<std::mutex> lock(g_console_mutex);
cb = completion_cb;
}
if (!cb) {
return {};
}
return cb(line, cursor_byte_pos);
}

namespace spinner {
static const char LOADING_CHARS[] = {'|', '/', '-', '\\'};
static std::condition_variable cv_stop;
Expand All @@ -1311,15 +1356,18 @@ namespace console {
static void draw_next_frame() {
// don't need lock because only one thread modifies running
frame = (frame + 1) % sizeof(LOADING_CHARS);
std::lock_guard<std::mutex> console_lock(g_console_mutex);
replace_last(LOADING_CHARS[frame]);
fflush(out);
}
void start() {
std::unique_lock<std::mutex> lock(mtx);
if (simple_io || running) {
assert(!tui_active.load());
if (simple_io || tui_active.load() || running) {
return;
}
common_log_flush(common_log_main());
std::lock_guard<std::mutex> console_lock(g_console_mutex);
fprintf(out, "%c", LOADING_CHARS[0]);
fflush(out);
frame = 1;
Expand All @@ -1346,6 +1394,7 @@ namespace console {
if (th.joinable()) {
th.join();
}
std::lock_guard<std::mutex> console_lock(g_console_mutex);
replace_last(' ');
pop_cursor();
fflush(out);
Expand Down
9 changes: 9 additions & 0 deletions common/console.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
#include "common.h"

#include <functional>
#include <cstdio>
#include <string>
#include <string_view>
#include <vector>

enum display_type {
Expand All @@ -23,15 +25,22 @@ namespace console {
void cleanup();
void set_display(display_type display);
bool readline(std::string & line, bool multiline_input);
FILE * output_file();

// TUI mode owns terminal output outside the legacy console logging path.
void set_tui_active(bool active);
bool is_tui_active();

using completion_callback = std::function<std::vector<std::pair<std::string, size_t>>(std::string_view, size_t)>;
void set_completion_callback(completion_callback cb);
std::vector<std::pair<std::string, size_t>> complete(std::string_view line, size_t cursor_byte_pos);

// Clipboard image paste support (Ctrl+V).
// The callback should attempt to read an image from the OS clipboard.
// On success, write bytes and mime type to the output params and return true.
using paste_image_callback = std::function<bool(std::vector<uint8_t> & bytes, std::string & mime)>;
void set_paste_image_callback(paste_image_callback cb);
bool try_paste_image_marker(std::string & marker);

// Returns and clears images accumulated during the last readline call.
// Each pair is (bytes, mime_type).
Expand Down
36 changes: 36 additions & 0 deletions tools/agent/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ add_executable(${TARGET}
agent-loop-tools-cli.cpp
permission-prompt.cpp
stb-image-impl.cpp
tui-editor.cpp
tui-renderer.cpp
tui-select-list.cpp
)
agent_force_link_core(${TARGET})
target_compile_features(${TARGET} PRIVATE cxx_std_17)
Expand Down Expand Up @@ -191,6 +194,39 @@ if(LLAMA_BUILD_TESTS)
endif()
add_test(NAME agent-inference-backend COMMAND test-agent-inference-backend)

add_executable(test-agent-tui
test-tui.cpp
tui-editor.cpp
tui-renderer.cpp
tui-select-list.cpp
)
target_link_libraries(test-agent-tui PRIVATE ${AGENT_CORE_TARGET})
target_compile_features(test-agent-tui PRIVATE cxx_std_17)
target_include_directories(test-agent-tui PRIVATE ../server)
add_test(NAME agent-tui COMMAND test-agent-tui)

option(LLAMA_AGENT_TERMINAL_TESTS "Run agent PTY rendering tests (requires Python and pyte)" OFF)
if(LLAMA_AGENT_TERMINAL_TESTS)
if(WIN32)
message(FATAL_ERROR "Agent PTY tests currently require a POSIX host")
endif()
find_package(Python3 REQUIRED COMPONENTS Interpreter)
execute_process(COMMAND ${Python3_EXECUTABLE} -c "import pyte" RESULT_VARIABLE PYTE_STATUS)
if(NOT PYTE_STATUS EQUAL 0)
message(FATAL_ERROR "Install tools/agent/tests/requirements.txt into the selected Python environment")
endif()
add_test(NAME agent-terminal COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_terminal.py
--executable $<TARGET_FILE:test-agent-tui>
--artifacts ${CMAKE_CURRENT_BINARY_DIR}/terminal-artifacts/plain)
add_test(NAME agent-terminal-color COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_terminal.py --color
--executable $<TARGET_FILE:test-agent-tui>
--artifacts ${CMAKE_CURRENT_BINARY_DIR}/terminal-artifacts/color)
set_tests_properties(agent-terminal PROPERTIES TIMEOUT 600)
set_tests_properties(agent-terminal-color PROPERTIES TIMEOUT 600)
endif()

if(LLAMA_HTTPLIB)
add_executable(test-agent-http-inference-backend
test-http-inference-backend.cpp
Expand Down
4 changes: 4 additions & 0 deletions tools/agent/agent-loop-compaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ bool agent_loop::do_compact(int32_t effective_keep) {
last_prompt_tokens_, to_summarize.size(), messages_.size() - cut_idx);

std::string summary = generate_summary(to_summarize, previous_summary_);
if (is_interrupted_.load()) {
LOG_WRN("Compaction cancelled: partial summary discarded\n");
return false;
}
if (summary.empty()) {
LOG_WRN("Compaction failed: empty summary\n");
return false;
Expand Down
Loading
Loading