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
25 changes: 25 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
# corteza 0.6.6.1

## Interrupt key

* Pressing the interrupt key during an in-flight agent turn now aborts
the turn cleanly and returns control to the prompt instead of
escaping the REPL entirely. Both `corteza::chat()` and the
`~/bin/corteza` CLI catch the R-level interrupt.
* In the CLI, if the interrupt arrives while a tool call is running
inside the `callr` worker subprocess, the worker is sent SIGINT so
the in-flight tool (e.g. a long `bash` or `run_r` call) actually
stops. The worker is recycled only if it doesn't return to idle.
* The aborted exchange is recorded in history with an
`[Interrupted by user before completing.]` marker so the next turn's
model sees that the prior turn ended early.
* In `corteza::chat()` running under RStudio, Esc fires the interrupt
as expected. In the terminal CLI, only Ctrl+C is an interrupt —
terminals send raw `^[` for Esc, which is not a signal.

## Other

* `load_saber_briefing()` now wraps `saber::briefing()` in
`suppressMessages()` so the briefing text no longer leaks to the
user's terminal every time a subagent calls `session_setup()`.

# corteza 0.6.6

## Async subagent queries
Expand Down
15 changes: 15 additions & 0 deletions R/chat.R
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,21 @@ chat <- function(provider = NULL, model = NULL, tools = NULL, session = NULL,
pre_turn_len <- length(turn_session$history %||% list())
result <- tryCatch(
turn(prompt, turn_session),
interrupt = function(c) {
cat(sprintf("\n%sInterrupted.%s\n", color$yellow, color$reset))
# turn() didn't return, so its history update never landed.
# Stitch the user prompt and an interruption marker into
# turn_session$history so the next turn's LLM call sees
# this exchange was aborted instead of silently dropping it.
marker <- "[Interrupted by user before completing.]"
turn_session$history <- c(
turn_session$history %||% list(),
list(list(role = "user", content = prompt),
list(role = "assistant", content = marker))
)
transcript_append(disk_session$session, "assistant", marker)
NULL
},
error = function(e) {
message(sprintf("%sError:%s %s",
color$bright_magenta, color$reset, e$message))
Expand Down
9 changes: 7 additions & 2 deletions R/context.R
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,14 @@ load_saber_briefing <- function(cwd) {
project <- basename(cwd)
scan_dir <- dirname(cwd)
tryCatch({
# Suppress saber's cat() to stdout - we want the return value only
# saber::briefing() emits its full text via message(); without
# suppressMessages it leaks to the user's terminal every time a
# subagent calls session_setup, which masquerades as a session
# restart. capture.output() handles any stdout cat() too.
utils::capture.output(
text <- saber::briefing(project = project, scan_dir = scan_dir)
text <- suppressMessages(
saber::briefing(project = project, scan_dir = scan_dir)
)
)
if (is.null(text) || nchar(trimws(text)) == 0L) {
NULL
Expand Down
38 changes: 38 additions & 0 deletions inst/bin/corteza
Original file line number Diff line number Diff line change
Expand Up @@ -1760,6 +1760,44 @@ Tool output:
history = turn_session$history,
usage = r$usage
)
}, interrupt = function(c) {
cat(sprintf("\n%sInterrupted.%s\n", color$yellow, color$reset))
# Only touch the worker if a tool call was actually in flight
# (state "busy"). When the interrupt arrived during the LLM
# round-trip the worker is "idle" and should be left alone —
# otherwise we'd needlessly drop worker-local state like the
# background-process registry.
if (!is.null(worker) && !is.null(worker$session)) {
state <- tryCatch(worker$session$get_state(),
error = function(e) "unknown")
if (identical(state, "busy")) {
tryCatch(worker$session$interrupt(),
error = function(e) NULL)
# Drain any queued result so the state machine returns to idle.
tryCatch(worker$session$read(), error = function(e) NULL)
state <- tryCatch(worker$session$get_state(),
error = function(e) "unknown")
if (!identical(state, "idle")) {
# Worker didn't recover from SIGINT; recycle it.
suppressWarnings(tryCatch(cli_worker_close(worker),
error = function(e) NULL))
worker <<- tryCatch(
cli_worker_connect(opts$port, cwd = cwd,
tools_filter = opts$tools),
error = function(e2) NULL
)
}
}
}
# Record the interruption against the persistent session so the
# next turn's api_history shows an assistant placeholder where
# the aborted reply would have been. The user message was
# already saved before turn() ran.
marker <- "[Interrupted by user before completing.]"
session <<- corteza:::session_add_message(session, "assistant", marker)
corteza:::transcript_append(session, "assistant", marker)
corteza:::session_save(session)
NULL
}, error = function(e) {
cat(sprintf("%sError: %s%s\n", color$bright_magenta, e$message, color$reset))
NULL
Expand Down