Nothing
library(aisdk)
test_that("capture_r_execution captures output, messages, and warnings", {
captured <- aisdk:::capture_r_execution({
cat("hello\n")
message("heads up")
warning("careful")
})
expect_true(captured$ok)
expect_equal(captured$output, "hello")
expect_equal(captured$messages, "heads up")
expect_equal(captured$warnings, "careful")
formatted <- aisdk:::format_captured_execution(captured)
expect_match(formatted, "hello")
expect_match(formatted, "Message: heads up")
expect_match(formatted, "Warning: careful")
})
test_that("Computer.execute_r_code captures warnings in returned output", {
comp <- aisdk::Computer$new(working_dir = tempdir(), sandbox_mode = "permissive")
result <- comp$execute_r_code("warning('careful'); 2 + 2")
expect_false(result$error)
expect_equal(result$warnings, "careful")
rendered <- paste(result$output, collapse = "\n")
expect_match(rendered, "4")
expect_match(rendered, "Warning: careful")
})
test_that("compact tool labels hide raw json details", {
label <- aisdk:::compact_tool_start_label(
"execute_r_code",
list(code = "packageVersion('ggtree')\nprint(installed.packages()[1, ])")
)
expect_match(label, "^Running R code:")
expect_false(grepl("\\{|\\}|\\$installed", label))
})
test_that("handle_command toggles debug mode", {
session <- aisdk::create_chat_session()
on_result <- aisdk:::handle_command("/debug on", session, stream = TRUE, verbose = FALSE, show_thinking = FALSE)
expect_true(on_result$verbose)
expect_true(on_result$show_thinking)
off_result <- aisdk:::handle_command("/debug off", session, stream = TRUE, verbose = TRUE, show_thinking = TRUE)
expect_false(off_result$verbose)
expect_false(off_result$show_thinking)
})
test_that("console input state initializes with user chat messages", {
session <- aisdk::create_chat_session()
session$append_message("user", "first question")
session$append_message("assistant", "first answer")
session$append_message("user", "second\nquestion")
state <- aisdk:::console_create_input_state(session)
expect_equal(state$history, c("first question", "second\nquestion"))
expect_equal(state$history_index, 3L)
})
test_that("console input state uses a separate persisted chat history", {
history_path <- tempfile("aisdk-console-history-")
writeLines(c("previous chat", "another chat"), history_path)
state <- aisdk:::console_create_input_state(history_path = history_path)
expect_equal(state$history, c("previous chat", "another chat"))
aisdk:::console_input_history_add(state, "new chat")
expect_equal(readLines(history_path, warn = FALSE), c("previous chat", "another chat", "new chat"))
expect_false(identical(basename(aisdk:::console_chat_history_path()), ".Rhistory"))
})
test_that("console input history recall navigates only chat input state", {
state <- aisdk:::console_create_input_state()
aisdk:::console_input_history_add(state, "first")
aisdk:::console_input_history_add(state, "second")
expect_equal(aisdk:::console_input_history_recall(state, "draft", "previous"), "second")
expect_equal(aisdk:::console_input_history_recall(state, "", "previous"), "first")
expect_equal(aisdk:::console_input_history_recall(state, "", "next"), "second")
expect_equal(aisdk:::console_input_history_recall(state, "", "next"), "draft")
})
test_that("raw console backspace redraws wide-character input", {
output <- capture.output(
remaining <- aisdk:::console_delete_last_raw_input_char(" ", c("你", "好")),
type = "output"
)
expect_equal(remaining, "你")
expect_match(paste(output, collapse = "\n"), "\033\\[2K 你")
expect_false(any(grepl("\b \b", output, fixed = TRUE)))
})
test_that("console readline returns a simple line immediately", {
state <- aisdk:::console_create_input_state()
lines <- c("alpha", "ignored")
index <- 0L
fake_readline <- function(prompt) {
index <<- index + 1L
lines[[index]]
}
input <- aisdk:::readline_multiline(state, readline_fn = fake_readline, quiet = TRUE)
expect_equal(input, "alpha")
expect_equal(index, 1L)
expect_equal(state$history, "alpha")
})
test_that("console readline keeps slash commands single-line", {
state <- aisdk:::console_create_input_state()
lines <- c("/help", "ignored")
index <- 0L
fake_readline <- function(prompt) {
index <<- index + 1L
lines[[index]]
}
input <- aisdk:::readline_multiline(state, readline_fn = fake_readline, quiet = TRUE)
expect_equal(input, "/help")
expect_equal(index, 1L)
})
test_that("console readline auto-saves script-like paste without sending immediately", {
state <- aisdk:::console_create_input_state()
output_dir <- tempfile("console-paste-")
clipboard_text <- paste(c(
"### Create: Jianming Zeng",
"library(Matrix)",
"",
"sce.all <- merge(x, y)"
), collapse = "\n")
lines <- c(
"### Create: Jianming Zeng",
"library(Matrix)",
"",
"",
"ignored"
)
index <- 0L
fake_readline <- function(prompt) {
index <<- index + 1L
lines[[index]]
}
input <- aisdk:::readline_multiline(
state,
readline_fn = fake_readline,
quiet = TRUE,
paste_output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
expect_equal(input, "")
expect_s3_class(state$pending_paste, "aisdk_console_paste_ref")
expect_true(file.exists(state$pending_paste$path))
expect_equal(paste(readLines(state$pending_paste$path, warn = FALSE), collapse = "\n"), clipboard_text)
expect_equal(state$pending_paste_drain, c("library(Matrix)", "", "sce.all <- merge(x, y)"))
expect_s3_class(state$pending_paste_notice, "aisdk_console_paste_ref")
expect_equal(index, 1L)
})
test_that("console readline handles RStudio-style multiline paste chunks", {
state <- aisdk:::console_create_input_state()
output_dir <- tempfile("console-paste-")
clipboard_text <- paste(c(
"###",
"### Create: Jianming Zeng",
"library(Matrix)",
"folder <- file.path('matrix', pro)",
"folder",
"counts <- Read10X(folder)"
), collapse = "\n")
lines <- c(
paste(c(
"###",
"### Create: Jianming Zeng",
"library(Matrix)",
"folder <- file.path('matrix', pro)"
), collapse = "\n"),
"folder",
"counts <- Read10X(folder)",
""
)
index <- 0L
fake_readline <- function(prompt) {
index <<- index + 1L
lines[[index]]
}
first <- aisdk:::readline_multiline(
state,
readline_fn = fake_readline,
quiet = TRUE,
paste_output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
skipped_one <- aisdk:::readline_multiline(
state,
readline_fn = fake_readline,
quiet = TRUE,
paste_output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
skipped_two <- aisdk:::readline_multiline(
state,
readline_fn = fake_readline,
quiet = TRUE,
paste_output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
sent <- aisdk:::readline_multiline(
state,
readline_fn = fake_readline,
quiet = TRUE,
paste_output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
expect_equal(first, "")
expect_equal(skipped_one, "")
expect_equal(skipped_two, "")
expect_match(sent, "^\\[Pasted Content ")
expect_null(state$pending_paste)
})
test_that("console readline skips queued paste lines before explicit send", {
state <- aisdk:::console_create_input_state()
output_dir <- tempfile("console-paste-")
clipboard_text <- paste(c(
"### Create: Jianming Zeng",
"library(Matrix)",
"",
"sce.all <- merge(x, y)"
), collapse = "\n")
lines <- c(
"### Create: Jianming Zeng",
"library(Matrix)",
"",
"sce.all <- merge(x, y)",
"帮我解释这段代码"
)
index <- 0L
prompts <- character(0)
fake_readline <- function(prompt) {
prompts <<- c(prompts, prompt)
index <<- index + 1L
lines[[index]]
}
first <- aisdk:::readline_multiline(
state,
readline_fn = fake_readline,
quiet = TRUE,
paste_output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
skipped_one <- aisdk:::readline_multiline(
state,
readline_fn = fake_readline,
quiet = TRUE,
paste_output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
skipped_two <- aisdk:::readline_multiline(
state,
readline_fn = fake_readline,
quiet = TRUE,
paste_output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
skipped_three <- aisdk:::readline_multiline(
state,
readline_fn = fake_readline,
quiet = TRUE,
paste_output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
sent <- aisdk:::readline_multiline(
state,
readline_fn = fake_readline,
quiet = TRUE,
paste_output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
expect_equal(first, "")
expect_equal(skipped_one, "")
expect_equal(skipped_two, "")
expect_equal(skipped_three, "")
expect_match(sent, "^帮我解释这段代码\n\n\\[Pasted Content ")
expect_null(state$pending_paste)
expect_null(state$pending_paste_notice)
expect_equal(prompts, c(" ", "", "", "", " [paste pending] "))
})
test_that("console readline sends pending paste on explicit empty enter", {
state <- aisdk:::console_create_input_state()
output_dir <- tempfile("console-paste-")
clipboard_text <- "### Create: Jianming Zeng\nlibrary(Matrix)"
lines <- c("### Create: Jianming Zeng", "")
index <- 0L
fake_readline <- function(prompt) {
index <<- index + 1L
lines[[index]]
}
first <- aisdk:::readline_multiline(
state,
readline_fn = fake_readline,
quiet = TRUE,
paste_output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
second <- aisdk:::readline_multiline(
state,
readline_fn = fake_readline,
quiet = TRUE,
paste_output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
expect_equal(first, "")
expect_match(second, "^\\[Pasted Content ")
expect_null(state$pending_paste)
expect_equal(state$history, second)
})
test_that("console readline combines pending paste with typed instructions", {
state <- aisdk:::console_create_input_state()
output_dir <- tempfile("console-paste-")
clipboard_text <- "### Create: Jianming Zeng"
lines <- c("### Create: Jianming Zeng", "帮我解释这段代码")
index <- 0L
fake_readline <- function(prompt) {
index <<- index + 1L
lines[[index]]
}
first <- aisdk:::readline_multiline(
state,
readline_fn = fake_readline,
quiet = TRUE,
paste_output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
second <- aisdk:::readline_multiline(
state,
readline_fn = fake_readline,
quiet = TRUE,
paste_output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
expect_equal(first, "")
expect_match(second, "^帮我解释这段代码\n\n\\[Pasted Content ")
expect_null(state$pending_paste)
})
test_that("console readline leaves pending paste intact for slash commands", {
state <- aisdk:::console_create_input_state()
output_dir <- tempfile("console-paste-")
clipboard_text <- "### Create: Jianming Zeng"
lines <- c("### Create: Jianming Zeng", "/help")
index <- 0L
fake_readline <- function(prompt) {
index <<- index + 1L
lines[[index]]
}
first <- aisdk:::readline_multiline(
state,
readline_fn = fake_readline,
quiet = TRUE,
paste_output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
second <- aisdk:::readline_multiline(
state,
readline_fn = fake_readline,
quiet = TRUE,
paste_output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
expect_equal(first, "")
expect_equal(second, "/help")
expect_s3_class(state$pending_paste, "aisdk_console_paste_ref")
})
test_that("console readline marks pending paste prompt without consuming slash commands", {
state <- aisdk:::console_create_input_state()
state$pending_paste <- aisdk:::console_create_paste_ref("/tmp/aisdk-paste.txt", 12L)
prompts <- character(0)
input <- aisdk:::readline_multiline(
state,
readline_fn = function(prompt) {
prompts <<- c(prompts, prompt)
"/history"
},
quiet = TRUE
)
expect_equal(input, "/history")
expect_equal(prompts, " [paste pending] ")
expect_s3_class(state$pending_paste, "aisdk_console_paste_ref")
})
test_that("console paste file writer falls back to end marker when clipboard is unavailable", {
state <- aisdk:::console_create_input_state()
output_dir <- tempfile("console-paste-")
lines <- c(
"### Create: Jianming Zeng",
"library(Matrix)",
"",
"/not-a-command-inside-paste",
"/endpaste",
"ignored"
)
index <- 0L
fake_readline <- function(prompt) {
index <<- index + 1L
lines[[index]]
}
paste_ref <- aisdk:::console_read_paste_to_file(
state,
readline_fn = fake_readline,
quiet = TRUE,
output_dir = output_dir,
clipboard_fn = function() NULL
)
expect_s3_class(paste_ref, "aisdk_console_paste_ref")
expect_match(paste_ref$message, "^\\[Pasted Content ")
expect_true(file.exists(paste_ref$path))
expect_equal(paste(readLines(paste_ref$path, warn = FALSE), collapse = "\n"), paste(lines[1:4], collapse = "\n"))
expect_equal(index, 5L)
})
test_that("console paste file writer uses clipboard when it matches the first line", {
output_dir <- tempfile("console-paste-")
clipboard_text <- "### Create: Jianming Zeng\nlibrary(Matrix)"
paste_ref <- aisdk:::console_read_paste_to_file(
readline_fn = function(prompt) stop("should not read more lines"),
quiet = TRUE,
initial_lines = "### Create: Jianming Zeng",
output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
expect_equal(paste(readLines(paste_ref$path, warn = FALSE), collapse = "\n"), clipboard_text)
})
test_that("console bracketed paste event is saved without exposing content as input", {
output_dir <- tempfile("console-paste-")
paste_text <- "### Create: Jianming Zeng\r\nlibrary(Matrix)\r\n帮我检查"
paste_ref <- aisdk:::console_save_paste_event(paste_text, output_dir = output_dir)
expect_s3_class(paste_ref, "aisdk_console_paste_ref")
expect_match(paste_ref$message, "^\\[Pasted Content ")
expect_true(file.exists(paste_ref$path))
expect_equal(paste(readLines(paste_ref$path, warn = FALSE), collapse = "\n"), "### Create: Jianming Zeng\nlibrary(Matrix)\n帮我检查")
})
test_that("console bracketed paste ignores short IME text", {
output_dir <- tempfile("console-paste-")
paste_ref <- aisdk:::console_save_paste_event("可以", output_dir = output_dir)
expect_s3_class(paste_ref, "aisdk_console_paste_ref")
expect_equal(paste_ref$path, "")
expect_equal(paste_ref$message, "")
expect_false(aisdk:::console_should_file_paste("吗?"))
expect_false(aisdk:::console_should_file_paste("hello"))
})
test_that("console paste file writer stores queued complete clipboard lines", {
state <- aisdk:::console_create_input_state()
output_dir <- tempfile("console-paste-")
clipboard_text <- "### Create: Jianming Zeng\nlibrary(Matrix)\nqsave(x)"
paste_ref <- aisdk:::console_read_paste_to_file(
state,
readline_fn = function(prompt) stop("should not read queued lines immediately"),
quiet = TRUE,
initial_lines = "### Create: Jianming Zeng",
output_dir = output_dir,
clipboard_fn = function() clipboard_text
)
expect_equal(paste(readLines(paste_ref$path, warn = FALSE), collapse = "\n"), clipboard_text)
expect_equal(state$pending_paste_drain, c("library(Matrix)", "qsave(x)"))
})
test_that("console auto-paste detection stays conservative", {
expect_false(aisdk:::console_should_auto_paste("hello"))
expect_false(aisdk:::console_should_auto_paste("可以给我个"))
expect_false(aisdk:::console_should_auto_paste("吗?"))
expect_false(aisdk:::console_should_auto_paste("/help"))
expect_true(aisdk:::console_should_auto_paste("---"))
expect_true(aisdk:::console_should_auto_paste("title: \"Anthropic研究员:用AI写代码\""))
expect_true(aisdk:::console_should_auto_paste("### Create: Jianming Zeng"))
expect_true(aisdk:::console_should_auto_paste("library(Seurat)"))
expect_true(aisdk:::console_should_auto_paste("scRNAlist <- lapply(samples, function(pro) {"))
expect_true(aisdk:::console_should_auto_paste("for (sample in samples) {"))
expect_true(aisdk:::console_should_auto_paste("sce <- CreateSeuratObject(counts)"))
expect_true(aisdk:::console_should_auto_paste("df |> dplyr::filter(group == 'A')"))
expect_true(aisdk:::console_should_file_paste(paste(rep("plain text", 80), collapse = " ")))
})
test_that("console image command sends local image as multimodal message", {
image_path <- tempfile(fileext = ".png")
writeBin(as.raw(0:15), image_path)
on.exit(unlink(image_path), add = TRUE)
model <- MockModel$new(list(list(text = "ok", finish_reason = "stop")))
model$capabilities <- list(vision_input = TRUE)
session <- aisdk::create_chat_session(model = model)
result <- aisdk:::handle_command(
paste("/paste-image", image_path, "describe it"),
session,
stream = FALSE,
verbose = FALSE,
show_thinking = FALSE
)
expect_false(result$exit)
user_message <- model$last_params$messages[[length(model$last_params$messages)]]
expect_equal(user_message$content[[1]]$type, "input_text")
expect_equal(user_message$content[[1]]$text, "describe it")
expect_equal(user_message$content[[2]]$type, "input_image")
expect_equal(user_message$content[[2]]$value, normalizePath(image_path, winslash = "/", mustWork = TRUE))
history <- session$get_history()
expect_equal(history[[1]]$role, "user")
expect_equal(history[[1]]$content[[2]]$type, "input_image")
})
test_that("console image command stores clipboard image in cache with path context", {
startup_dir <- tempfile("console-startup-")
dir.create(startup_dir)
on.exit(unlink(startup_dir, recursive = TRUE), add = TRUE)
model <- MockModel$new(list(list(text = "ok", finish_reason = "stop")))
model$capabilities <- list(vision_input = TRUE)
session <- aisdk::create_chat_session(model = model)
session$set_metadata("console_startup_dir", startup_dir)
clipboard_image_fn <- function(output_dir) {
expected_cache_dir <- file.path(
normalizePath(startup_dir, winslash = "/", mustWork = TRUE),
".aisdk",
"cache",
"images"
)
expect_equal(
output_dir,
expected_cache_dir
)
dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)
path <- file.path(output_dir, "clipboard-image-test.png")
writeBin(as.raw(0:15), path)
normalizePath(path, winslash = "/", mustWork = TRUE)
}
result <- aisdk:::handle_command(
"/paste-image",
session,
stream = FALSE,
verbose = FALSE,
show_thinking = FALSE,
clipboard_image_fn = clipboard_image_fn
)
cached_path <- normalizePath(
file.path(startup_dir, ".aisdk", "cache", "images", "clipboard-image-test.png"),
winslash = "/",
mustWork = TRUE
)
user_message <- model$last_params$messages[[length(model$last_params$messages)]]
expect_false(result$exit)
expect_equal(user_message$content[[1]]$type, "input_text")
expect_match(user_message$content[[1]]$text, "Cached image file: clipboard-image-test.png", fixed = TRUE)
expect_match(user_message$content[[1]]$text, paste0("Cached image path: ", cached_path), fixed = TRUE)
expect_equal(user_message$content[[2]]$type, "input_image")
expect_equal(user_message$content[[2]]$value, cached_path)
})
test_that("handle_command toggles inspect mode through app state", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
on_result <- aisdk:::handle_command(
"/inspect on",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE,
app_state = app_state
)
expect_false(on_result$verbose)
expect_false(on_result$show_thinking)
expect_true(on_result$refresh_status)
expect_equal(app_state$view_mode, "inspect")
off_result <- aisdk:::handle_command(
"/inspect off",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE,
app_state = app_state
)
expect_equal(app_state$view_mode, "clean")
expect_true(off_result$refresh_status)
})
test_that("model command opens chooser when called without args", {
session <- aisdk::create_chat_session(model = "openai:gpt-4o")
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
result <- aisdk:::handle_command(
"/model",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE,
app_state = app_state,
model_prompt_fn = function(...) NULL
)
expect_false(result$refresh_status)
expect_equal(session$get_model_id(), "openai:gpt-4o")
})
test_that("model command can switch via chooser result", {
session <- aisdk::create_chat_session(model = "openai:gpt-4o")
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
result <- aisdk:::handle_command(
"/model",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE,
app_state = app_state,
model_prompt_fn = function(...) "anthropic:claude-sonnet-4-20250514"
)
expect_true(result$refresh_status)
expect_equal(session$get_model_id(), "anthropic:claude-sonnet-4-20250514")
expect_equal(app_state$model_id, "anthropic:claude-sonnet-4-20250514")
})
test_that("model current reports the active model without switching", {
session <- aisdk::create_chat_session(model = "openai:gpt-4o")
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
result <- aisdk:::handle_command(
"/model current",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE,
app_state = app_state
)
expect_false(result$refresh_status)
expect_equal(session$get_model_id(), "openai:gpt-4o")
})
test_that("skills command reloads the session skill registry", {
skill_root <- tempfile("console-live-skills-")
dir.create(file.path(skill_root, "live_skill"), recursive = TRUE)
on.exit(unlink(skill_root, recursive = TRUE), add = TRUE)
writeLines(c(
"---",
"name: live_skill",
"description: Live skill",
"---",
"Original"
), file.path(skill_root, "live_skill", "SKILL.md"))
agent <- create_agent(
name = "SkillConsole",
description = "Console with live skills",
skills = skill_root
)
session <- create_chat_session(model = "mock:test", agent = agent)
registry <- session$get_envir()$.skill_registry
expect_equal(registry$get_skill("live_skill")$description, "Live skill")
writeLines(c(
"---",
"name: live_skill",
"description: Updated live skill",
"---",
"Updated"
), file.path(skill_root, "live_skill", "SKILL.md"))
result <- aisdk:::handle_command(
"/skills reload",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE
)
expect_true(result$refresh_status)
expect_equal(registry$get_skill("live_skill")$description, "Updated live skill")
})
test_that("skills command defaults to list with no arguments", {
agent <- create_console_agent(profile = "minimal")
session <- create_chat_session(model = "mock:test", agent = agent)
expect_error(
result <- aisdk:::handle_command(
"/skills",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE
),
NA
)
expect_false(result$refresh_status)
})
test_that("console_subcommand treats missing and blank args as default", {
expect_equal(aisdk:::console_subcommand(character(0), default = "list"), "list")
expect_equal(aisdk:::console_subcommand(NA_character_, default = "list"), "list")
expect_equal(aisdk:::console_subcommand("", default = "list"), "list")
expect_equal(aisdk:::console_subcommand("ROOTS", default = "list"), "roots")
})
test_that("model command updates context and thinking settings", {
session <- aisdk::create_chat_session(model = "deepseek:deepseek-v4-flash")
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
context_result <- aisdk:::handle_command(
"/model context 512k",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE,
app_state = app_state
)
expect_true(context_result$refresh_status)
expect_equal(session$get_model_options()$context_window, 512000)
aisdk:::handle_command("/model output 64k", session, stream = TRUE, verbose = FALSE, show_thinking = FALSE, app_state = app_state)
aisdk:::handle_command("/model max-tokens 700", session, stream = TRUE, verbose = FALSE, show_thinking = FALSE, app_state = app_state)
aisdk:::handle_command("/model thinking on", session, stream = TRUE, verbose = FALSE, show_thinking = FALSE, app_state = app_state)
aisdk:::handle_command("/model effort high", session, stream = TRUE, verbose = FALSE, show_thinking = FALSE, app_state = app_state)
aisdk:::handle_command("/model budget 2k", session, stream = TRUE, verbose = FALSE, show_thinking = FALSE, app_state = app_state)
options <- session$get_model_options()
expect_equal(options$max_output_tokens, 64000)
expect_equal(options$call_options$max_tokens, 700)
expect_true(options$call_options$thinking)
expect_equal(options$call_options$reasoning_effort, "high")
expect_equal(options$call_options$thinking_budget, 2000)
line <- aisdk:::build_console_status_line(app_state)
expect_match(line, "Ctx\\(est\\): 512.0k")
expect_match(line, "Out: 64.0k")
expect_match(line, "Think: on")
expect_match(line, "Effort: high")
expect_match(line, "Budget: 2.0k")
expect_match(line, "Max: 700")
clear_result <- aisdk:::handle_command(
"/model thinking auto",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE,
app_state = app_state
)
expect_true(clear_result$refresh_status)
expect_null(aisdk:::list_get_exact(session$get_model_options()$call_options, "thinking"))
})
test_that("persona commands update session persona state", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
set_result <- aisdk:::handle_command(
"/persona set You are a brutal but precise reviewer.",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE,
app_state = app_state
)
expect_true(set_result$refresh_status)
expect_equal(aisdk:::console_current_persona(session)$source, "manual")
expect_equal(aisdk:::console_current_persona(session)$label, "custom")
evolve_result <- aisdk:::handle_command(
"/persona evolve Stay extra concise.",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE,
app_state = app_state
)
expect_true(evolve_result$refresh_status)
expect_true(any(grepl("Stay extra concise.", aisdk:::console_current_persona(session)$notes, fixed = TRUE)))
reset_result <- aisdk:::handle_command(
"/persona default",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE,
app_state = app_state
)
expect_true(reset_result$refresh_status)
expect_equal(aisdk:::console_current_persona(session)$source, "default")
})
test_that("console status line includes persona label", {
session <- aisdk::create_chat_session(model = "openai:gpt-5-mini")
aisdk:::console_set_manual_persona(session, "You are a skeptical reviewer.", label = "skeptic", locked = TRUE)
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
line <- aisdk:::build_console_status_line(app_state)
expect_match(line, "Persona: skeptic:manual")
})
test_that("inspect commands open and close overlay state", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(session, view_mode = "inspect")
aisdk:::console_app_start_turn(app_state, "Inspect latest turn")
aisdk:::console_app_append_assistant_text(app_state, "Overlay me")
aisdk:::console_app_finish_turn(app_state)
turn_result <- aisdk:::handle_command(
"/inspect turn",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE,
app_state = app_state
)
overlay <- aisdk:::console_app_get_active_overlay(app_state)
expect_true(turn_result$refresh_status)
expect_equal(overlay$type, "inspector")
expect_match(overlay$title, "Inspector Overlay")
expect_equal(app_state$focus_target, "overlay:inspector")
close_result <- aisdk:::handle_command(
"/inspect close",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE,
app_state = app_state
)
expect_true(close_result$refresh_status)
expect_null(aisdk:::console_app_get_active_overlay(app_state))
expect_equal(app_state$focus_target, "composer")
})
test_that("inspect next and prev navigate overlay tools", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(session, view_mode = "inspect")
aisdk:::console_app_start_turn(app_state, "Inspect tools")
aisdk:::console_app_append_assistant_text(app_state, "Two tools ran")
aisdk:::console_app_record_tool_start(app_state, "execute_r_code", list(code = "1 + 1"))
aisdk:::console_app_record_tool_result(app_state, "execute_r_code", "2")
aisdk:::console_app_record_tool_start(app_state, "bash", list(command = "pwd"))
aisdk:::console_app_record_tool_result(app_state, "bash", "/tmp")
aisdk:::console_app_finish_turn(app_state)
turn_result <- aisdk:::handle_command(
"/inspect turn",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE,
app_state = app_state
)
expect_true(turn_result$refresh_status)
expect_null(aisdk:::console_app_get_active_overlay(app_state)$payload$tool_index)
next_result <- aisdk:::handle_command(
"/inspect next",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE,
app_state = app_state
)
overlay <- aisdk:::console_app_get_active_overlay(app_state)
expect_true(next_result$refresh_status)
expect_equal(overlay$payload$tool_index, 1)
expect_match(overlay$title, "Tool 1")
next_result_2 <- aisdk:::handle_command(
"/inspect next",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE,
app_state = app_state
)
overlay <- aisdk:::console_app_get_active_overlay(app_state)
expect_true(next_result_2$refresh_status)
expect_equal(overlay$payload$tool_index, 2)
expect_match(overlay$title, "Tool 2")
prev_result <- aisdk:::handle_command(
"/inspect prev",
session,
stream = TRUE,
verbose = FALSE,
show_thinking = FALSE,
app_state = app_state
)
overlay <- aisdk:::console_app_get_active_overlay(app_state)
expect_true(prev_result$refresh_status)
expect_equal(overlay$payload$tool_index, 1)
})
test_that("console status line reflects app state snapshot", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(
session,
sandbox_mode = "strict",
stream_enabled = FALSE,
local_execution_enabled = TRUE,
view_mode = "inspect"
)
app_state$model_id <- "openai:gpt-5"
app_state$tool_state <- "running"
line <- aisdk:::build_console_status_line(app_state)
expect_match(line, "Model: openai:gpt-5")
expect_match(line, "Sandbox: strict")
expect_match(line, "View: inspect")
expect_match(line, "Stream: off")
expect_match(line, "Local: on")
expect_match(line, "Tools: running")
})
test_that("console status lines fold for narrow widths", {
session <- aisdk::create_chat_session(model = "openai:gpt-5-mini")
app_state <- aisdk:::create_console_app_state(
session,
sandbox_mode = "strict",
stream_enabled = FALSE,
local_execution_enabled = TRUE,
view_mode = "inspect"
)
app_state$tool_state <- "running"
wide_lines <- aisdk:::build_console_status_lines(app_state, width = 140)
narrow_lines <- aisdk:::build_console_status_lines(app_state, width = 58)
expect_length(wide_lines, 1)
expect_true(length(narrow_lines) >= 2)
expect_true(all(nchar(narrow_lines, type = "width") <= 58))
expect_true(any(grepl("^Model:", narrow_lines)))
expect_true(any(grepl("Tools: running", narrow_lines, fixed = TRUE)))
})
test_that("console frame groups status timeline and overlay sections", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(session, view_mode = "inspect")
aisdk:::console_app_start_turn(app_state, "Inspect tools")
aisdk:::console_app_append_assistant_text(app_state, "Two tools ran")
aisdk:::console_app_record_tool_start(app_state, "execute_r_code", list(code = "1 + 1"))
aisdk:::console_app_record_tool_result(app_state, "execute_r_code", "2")
aisdk:::console_app_finish_turn(app_state)
aisdk:::console_app_open_turn_overlay(app_state)
frame <- aisdk:::build_console_frame(app_state)
expect_equal(frame$status$type, "status")
expect_equal(frame$status$tone, "muted")
expect_equal(frame$timeline$type, "timeline")
expect_equal(frame$timeline$tone, "subtle")
expect_equal(frame$overlay$type, "overlay")
expect_equal(frame$overlay$tone, "primary")
expect_true(length(frame$status$lines) >= 2)
expect_true(any(grepl("Model:", frame$status$lines, fixed = TRUE)))
expect_true(any(grepl("execute_r_code", frame$timeline$lines, fixed = TRUE)))
expect_true(any(grepl("Inspector Overlay", frame$overlay$lines, fixed = TRUE)))
expect_true(isTRUE(frame$meta$has_overlay))
expect_equal(frame$meta$focus_target, "overlay:inspector")
})
test_that("tool events are captured into the current turn timeline", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(session, view_mode = "inspect")
aisdk:::console_app_start_turn(app_state, "Check package version")
old_opts <- options(
aisdk.console_app_state = app_state,
aisdk.tool_log_mode = "compact"
)
on.exit(options(old_opts), add = TRUE)
aisdk:::cli_tool_start("execute_r_code", list(code = "packageVersion('ggtree')"))
aisdk:::cli_tool_result("execute_r_code", "4.0.1", success = TRUE)
aisdk:::console_app_finish_turn(app_state)
turn <- aisdk:::console_app_get_current_turn(app_state)
lines <- aisdk:::format_console_tool_timeline(turn)
expect_length(turn$tool_calls, 1)
expect_equal(turn$tool_calls[[1]]$status, "done")
expect_match(turn$tool_calls[[1]]$args_summary, "Running R code")
expect_match(turn$tool_calls[[1]]$result_summary, "R code completed")
expect_length(lines, 1)
expect_match(lines[[1]], "execute_r_code")
expect_match(lines[[1]], "\\[done\\]")
})
test_that("invalid tool arguments use a distinct compact result label", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(session, view_mode = "inspect")
aisdk:::console_app_start_turn(app_state, "Run diagnostic code")
old_opts <- options(
aisdk.console_app_state = app_state,
aisdk.tool_log_mode = "compact"
)
on.exit(options(old_opts), add = TRUE)
raw_result <- list(error = TRUE, error_type = "invalid_tool_arguments")
aisdk:::cli_tool_start("r_eval", list())
aisdk:::cli_tool_result(
"r_eval",
"Error: invalid arguments for tool 'r_eval': Missing required argument `code`.",
success = FALSE,
raw_result = raw_result
)
aisdk:::console_app_finish_turn(app_state)
turn <- aisdk:::console_app_get_current_turn(app_state)
expect_match(turn$tool_calls[[1]]$result_summary, "r_eval call had invalid arguments", fixed = TRUE)
expect_false(grepl("r_eval failed", turn$tool_calls[[1]]$result_summary, fixed = TRUE))
})
test_that("tool diagnostics are stored separately from tool summaries", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(session, view_mode = "inspect")
aisdk:::console_app_start_turn(app_state, "Run diagnostic code")
old_opts <- options(
aisdk.console_app_state = app_state,
aisdk.tool_log_mode = "compact"
)
on.exit(options(old_opts), add = TRUE)
raw_result <- structure(
"Result: 4\nMessage: heads up\nWarning: careful",
aisdk_messages = "heads up",
aisdk_warnings = "careful"
)
aisdk:::cli_tool_start("execute_r_code", list(code = "message('heads up'); warning('careful'); 2 + 2"))
aisdk:::cli_tool_result("execute_r_code", raw_result, success = TRUE, raw_result = raw_result)
aisdk:::console_app_finish_turn(app_state)
turn <- aisdk:::console_app_get_current_turn(app_state)
tool <- turn$tool_calls[[1]]
lines <- aisdk:::format_console_tool_timeline(turn)
expect_equal(tool$messages, "heads up")
expect_equal(tool$warnings, "careful")
expect_equal(turn$messages, "heads up")
expect_equal(turn$warnings, "careful")
expect_match(lines[[1]], "messages: 1")
expect_match(lines[[1]], "warnings: 1")
})
test_that("streaming chunks accumulate into app state assistant text", {
StreamingMockModel <- R6::R6Class(
"StreamingMockModelForConsoleTests",
inherit = aisdk:::LanguageModelV1,
public = list(
provider = "mock",
model_id = "stream-mock",
chunks = NULL,
initialize = function(chunks) {
self$chunks <- chunks
},
do_generate = function(params) {
list(text = paste(self$chunks, collapse = ""), tool_calls = NULL, finish_reason = "stop")
},
do_stream = function(params, callback) {
for (chunk in self$chunks) {
callback(chunk, FALSE)
}
callback(NULL, TRUE)
list(
text = paste(self$chunks, collapse = ""),
tool_calls = NULL,
finish_reason = "stop",
usage = list(total_tokens = length(self$chunks))
)
},
format_tool_result = function(tool_call_id, tool_name, result) {
list(role = "tool", tool_call_id = tool_call_id, name = tool_name, content = result)
}
)
)
model <- StreamingMockModel$new(c("hello ", "world"))
session <- aisdk::create_chat_session(model = model)
app_state <- aisdk:::create_console_app_state(session, view_mode = "inspect")
aisdk:::console_app_start_turn(app_state, "Say hello")
aisdk:::with_console_chat_display(app_state = app_state, code = {
session$send_stream(
"Say hello",
callback = function(text, done) {
if (!isTRUE(done)) {
aisdk:::console_app_append_assistant_text(app_state, text)
}
}
)
})
aisdk:::console_app_finish_turn(app_state)
turn <- aisdk:::console_app_get_current_turn(app_state)
expect_equal(turn$assistant_text, "hello world")
expect_equal(turn$phase, "done")
expect_equal(app_state$phase, "idle")
})
test_that("console typed stream events render assistant text deltas once", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
aisdk:::console_app_start_turn(app_state, "Say hello")
aisdk:::console_handle_stream_event(
list(type = "text_delta", text = "hello "),
app_state = app_state,
md_renderer = NULL
)
aisdk:::console_handle_stream_event(
list(type = "text_delta", text = "world"),
app_state = app_state,
md_renderer = NULL
)
aisdk:::console_handle_stream_event(
list(type = "final_text", text = "hello world", already_streamed = TRUE),
app_state = app_state,
md_renderer = NULL
)
turn <- aisdk:::console_app_get_current_turn(app_state)
expect_equal(turn$assistant_text, "hello world")
})
test_that("console stream events render thinking without storing it as final text", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
aisdk:::console_app_start_turn(app_state, "Think first")
aisdk:::console_handle_stream_event(
list(type = "thinking_text", text = "<think>\nprivate reasoning\n</think>\n\n"),
app_state = app_state,
md_renderer = NULL
)
aisdk:::console_handle_stream_event(
list(type = "final_text", text = "Visible answer."),
app_state = app_state,
md_renderer = NULL
)
turn <- aisdk:::console_app_get_current_turn(app_state)
expect_equal(turn$assistant_text, "Visible answer.")
expect_equal(turn$intermediate_text, "")
expect_false(grepl("private reasoning", turn$assistant_text, fixed = TRUE))
})
test_that("console stream events filter text tool protocol markup", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
filter <- aisdk:::new_console_tool_call_markup_filter()
aisdk:::console_app_start_turn(app_state, "Use a tool")
chunks <- c(
"我先检查。\n<tool_",
"call>\n{\"name\":\"r_eval\",\"arguments\":{\"code\":\"pwd\"}}\n",
"</tool_call>\n",
"继续处理。\n"
)
for (chunk in chunks) {
aisdk:::console_handle_stream_event(
list(type = "text_delta", text = chunk),
app_state = app_state,
md_renderer = NULL,
tool_markup_filter = filter
)
}
aisdk:::console_handle_stream_event(
list(type = "done", done = TRUE),
app_state = app_state,
md_renderer = NULL,
tool_markup_filter = filter
)
turn <- aisdk:::console_app_get_current_turn(app_state)
expect_match(turn$assistant_text, "我先检查", fixed = TRUE)
expect_match(turn$assistant_text, "继续处理", fixed = TRUE)
expect_false(grepl("<tool_call>", turn$assistant_text, fixed = TRUE))
expect_false(grepl("r_eval", turn$assistant_text, fixed = TRUE))
})
test_that("console agent accepts native plain final text after tools", {
echo_tool <- tool(
name = "echo",
description = "Echo a message",
parameters = z_object(message = z_string("Message to echo")),
execute = function(args) paste("Echo:", args$message)
)
model <- MockModel$new(list(
list(
text = "",
tool_calls = list(list(
id = "call_1",
name = "echo",
arguments = list(message = "console")
)),
finish_reason = "tool_calls",
usage = list(total_tokens = 10)
),
list(
text = "Console protocol worked.",
tool_calls = NULL,
finish_reason = "stop",
usage = list(total_tokens = 10)
)
))
model$capabilities <- list(native_tool_calling = TRUE)
session <- aisdk::create_chat_session(model = model, tools = list(echo_tool))
session$merge_metadata(list(console_agent_enabled = TRUE))
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
ok <- aisdk:::console_send_user_message(
"Use the echo tool",
session = session,
stream = TRUE,
app_state = app_state
)
turn <- aisdk:::console_app_get_last_turn(app_state)
expect_true(ok)
expect_match(turn$assistant_text, "Console protocol worked", fixed = TRUE)
expect_equal(length(gregexpr("Console protocol worked", turn$assistant_text, fixed = TRUE)[[1]]), 1L)
expect_false(grepl("<final_answer>", turn$assistant_text, fixed = TRUE))
expect_length(model$responses, 0)
})
test_that("console agent hides text tool fallback markup while streaming", {
echo_tool <- tool(
name = "echo",
description = "Echo a message",
parameters = z_object(message = z_string("Message to echo")),
execute = function(args) paste("Echo:", args$message)
)
model <- MockModel$new(list(
list(
text = paste0(
"我先调用工具。\n",
"<tool_call>\n",
"{\"name\":\"echo\",\"arguments\":{\"message\":\"console\"}}\n",
"</tool_call>\n"
),
tool_calls = NULL,
finish_reason = "stop",
usage = list(total_tokens = 10)
),
list(
text = "工具结果已经处理完。",
tool_calls = NULL,
finish_reason = "stop",
usage = list(total_tokens = 10)
)
))
model$capabilities <- list(native_tool_calling = FALSE)
session <- aisdk::create_chat_session(model = model, tools = list(echo_tool))
session$merge_metadata(list(console_agent_enabled = TRUE))
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
ok <- aisdk:::console_send_user_message(
"Use the echo tool",
session = session,
stream = TRUE,
app_state = app_state
)
turn <- aisdk:::console_app_get_last_turn(app_state)
expect_true(ok)
expect_match(turn$assistant_text, "工具结果已经处理完", fixed = TRUE)
expect_false(grepl("<tool_call>", turn$assistant_text, fixed = TRUE))
expect_false(grepl("echo", turn$assistant_text, fixed = TRUE))
})
test_that("console agent dedupes repeated final-looking intermediate text", {
tool_seen <- character()
echo_tool <- tool(
name = "echo",
description = "Echo a message",
parameters = z_object(message = z_string("Message to echo")),
execute = function(args) {
tool_seen <<- c(tool_seen, args$message)
paste("Echo:", args$message)
}
)
report <- "FINAL REPORT\nThe package names are available."
model <- MockModel$new(list(
list(
text = report,
tool_calls = list(list(
id = "call_1",
name = "echo",
arguments = list(message = "first")
)),
finish_reason = "tool_calls",
usage = list(total_tokens = 10)
),
list(
text = report,
tool_calls = list(list(
id = "call_2",
name = "echo",
arguments = list(message = "second")
)),
finish_reason = "tool_calls",
usage = list(total_tokens = 10)
),
list(
text = paste0("<final_answer>", report, "</final_answer>"),
tool_calls = NULL,
finish_reason = "stop",
usage = list(total_tokens = 10)
)
))
model$capabilities <- list(native_tool_calling = TRUE)
session <- aisdk::create_chat_session(model = model, tools = list(echo_tool))
session$merge_metadata(list(console_agent_enabled = TRUE))
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
ok <- aisdk:::console_send_user_message(
"Use the echo tool twice",
session = session,
stream = TRUE,
app_state = app_state
)
turn <- aisdk:::console_app_get_last_turn(app_state)
expect_true(ok)
expect_equal(tool_seen, c("first", "second"))
intermediate_matches <- gregexpr("FINAL REPORT", turn$intermediate_text, fixed = TRUE)[[1]]
assistant_matches <- gregexpr("FINAL REPORT", turn$assistant_text, fixed = TRUE)[[1]]
expect_equal(sum(intermediate_matches > 0), 1L)
expect_equal(sum(assistant_matches > 0), 1L)
})
test_that("turn and tool inspector helpers expose structured details", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(session, view_mode = "inspect")
aisdk:::console_app_start_turn(app_state, "Run diagnostic code")
aisdk:::console_app_append_assistant_text(app_state, "Version check completed.")
raw_result <- structure(
"Result: 4\nMessage: heads up\nWarning: careful",
aisdk_messages = "heads up",
aisdk_warnings = "careful"
)
aisdk:::console_app_record_tool_start(app_state, "execute_r_code", list(code = "2 + 2"))
aisdk:::console_app_record_tool_result(app_state, "execute_r_code", raw_result, raw_result = raw_result)
aisdk:::console_app_finish_turn(app_state)
turn <- aisdk:::console_app_get_last_turn(app_state)
turn_lines <- aisdk:::format_console_turn_detail(turn)
tool_lines <- aisdk:::format_console_tool_detail(turn, 1)
expect_true(any(grepl("^Turn:", turn_lines)))
expect_true(any(grepl("^Assistant: Version check completed\\.", turn_lines)))
expect_true(any(grepl("^Messages: heads up$", turn_lines)))
expect_true(any(grepl("^Warnings: careful$", turn_lines)))
expect_true(any(grepl("^Tool: execute_r_code$", tool_lines)))
expect_true(any(grepl("^Args raw:", tool_lines)))
expect_true(any(grepl("^Result raw:", tool_lines)))
})
test_that("overlay helpers build boxed inspector content", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(session, view_mode = "inspect")
aisdk:::console_app_start_turn(app_state, "Run diagnostic code")
aisdk:::console_app_append_assistant_text(app_state, "Version check completed.")
aisdk:::console_app_finish_turn(app_state)
overlay <- aisdk:::console_app_open_turn_overlay(app_state)
lines <- aisdk:::build_console_overlay_box(app_state, overlay)
expect_equal(overlay$type, "inspector")
expect_true(length(lines) >= 5)
expect_true(any(grepl("Inspector Overlay", lines, fixed = TRUE)))
expect_true(any(grepl("Close: /inspect close", lines, fixed = TRUE)))
})
test_that("console frame hides timeline outside inspect mode", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
aisdk:::console_app_start_turn(app_state, "Inspect tools")
aisdk:::console_app_record_tool_start(app_state, "execute_r_code", list(code = "1 + 1"))
aisdk:::console_app_record_tool_result(app_state, "execute_r_code", "2")
aisdk:::console_app_finish_turn(app_state)
frame <- aisdk:::build_console_frame(app_state)
expect_length(frame$timeline$lines, 0)
expect_false(isTRUE(frame$meta$has_overlay))
})
test_that("console_frame_section_changed detects unchanged status sections", {
session <- aisdk::create_chat_session(model = "openai:gpt-5-mini")
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
frame1 <- aisdk:::build_console_frame(app_state)
frame2 <- aisdk:::build_console_frame(app_state)
expect_true(aisdk:::console_frame_section_changed(NULL, frame1, "status", force = FALSE))
expect_false(aisdk:::console_frame_section_changed(frame1, frame2, "status", force = FALSE))
expect_true(aisdk:::console_frame_section_changed(frame1, frame2, "status", force = TRUE))
})
test_that("inspector lines include navigation hints", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(session, view_mode = "inspect")
aisdk:::console_app_start_turn(app_state, "Inspect tools")
aisdk:::console_app_record_tool_start(app_state, "execute_r_code", list(code = "1 + 1"))
aisdk:::console_app_record_tool_result(app_state, "execute_r_code", "2")
aisdk:::console_app_finish_turn(app_state)
turn <- aisdk:::console_app_get_last_turn(app_state)
turn_lines <- aisdk:::build_console_inspector_lines(turn)
tool_lines <- aisdk:::build_console_inspector_lines(turn, tool_index = 1)
expect_true(any(grepl("/inspect next opens the first tool", turn_lines, fixed = TRUE)))
expect_true(any(grepl("/inspect prev | /inspect next", tool_lines, fixed = TRUE)))
expect_true(any(grepl("/inspect turn returns to the turn summary", tool_lines, fixed = TRUE)))
})
test_that("with_console_chat_display derives visibility from app state", {
session <- aisdk::create_chat_session()
app_state <- aisdk:::create_console_app_state(session, view_mode = "inspect")
aisdk:::with_console_chat_display(app_state = app_state, code = {
expect_equal(getOption("aisdk.tool_log_mode"), "compact")
expect_false(getOption("aisdk.show_thinking"))
expect_identical(getOption("aisdk.console_app_state"), app_state)
})
aisdk:::console_app_set_view_mode(app_state, "debug")
aisdk:::with_console_chat_display(app_state = app_state, code = {
expect_equal(getOption("aisdk.tool_log_mode"), "detailed")
expect_true(getOption("aisdk.show_thinking"))
})
})
test_that("console_send_user_message cancels turn and restores history on interrupt", {
interrupt_model <- MockModel$new(list(function(params) {
stop(structure(list(message = "interrupt"), class = c("interrupt", "condition")))
}))
session <- aisdk::create_chat_session(model = interrupt_model)
session$append_message("user", "existing")
session$append_message("assistant", "history")
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
ok <- aisdk:::console_send_user_message(
"please stop",
session = session,
stream = TRUE,
app_state = app_state
)
expect_false(ok)
expect_equal(vapply(session$get_history(), `[[`, character(1), "content"), c("existing", "history"))
turn <- aisdk:::console_app_get_current_turn(app_state)
expect_equal(turn$phase, "cancelled")
expect_equal(app_state$phase, "cancelled")
expect_equal(app_state$tool_state, "idle")
})
test_that("console_send_user_message restores history on send errors", {
error_model <- MockModel$new(list(function(params) {
stop("boom")
}))
session <- aisdk::create_chat_session(model = error_model)
session$append_message("user", "existing")
app_state <- aisdk:::create_console_app_state(session, view_mode = "clean")
ok <- aisdk:::console_send_user_message(
"will fail",
session = session,
stream = FALSE,
app_state = app_state
)
expect_false(ok)
expect_equal(length(session$get_history()), 1L)
expect_equal(session$get_history()[[1]]$content, "existing")
expect_equal(aisdk:::console_app_get_current_turn(app_state)$phase, "error")
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.