posit-dev / posit-dev/shinychat

History save fails for app-local S7 `Content` subclasses: "Unable to find the S7 class"

Open
#393 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

ai-triage:done Priority: Medium
Dominant language
TypeScript
Stars
139
Forks
28
Avg merge
23h 44m
Merged PRs (30d)
50

Description

When a chat contains ellmer::Content subclasses defined in a Shiny app's R/ files (i.e., not in a package), saving history fails with:

Warning: Could not save conversation
Caused by error in `FUN()`:
! Unable to find the S7 class: `ContentPromptStyle`.
Repro
library(S7)
library(ellmer)
# Shiny sources R/ helpers into new.env(parent = globalenv()), not globalenv()
app_env <- new.env(parent = globalenv())
eval(quote({
  ContentPromptStyle <- new_class(
    "ContentPromptStyle",
    parent = ellmer::ContentText,
    properties = list(slug = new_property(class_character, default = "friendly"))
  )
}), envir = app_env)
turn <- with(app_env, Turn("user", list(ContentPromptStyle(text = "hi"))))
recorded <- ellmer::contents_record(turn)
# shinychat replays recorded turns at save time from its own namespace frame,
# e.g. extend_record_linear() -> lapply(turns, ellmer::contents_replay, tools)
f <- eval(
  quote(function(x) lapply(x, ellmer::contents_replay, tools = list())),
  new.env(parent = asNamespace("shinychat"))
)
f(list(recorded))
#> Error: Unable to find the S7 class: `ContentPromptStyle`.
What's happening
  1. ellmer::contents_record() serializes a non-package class as a bare name ("ContentPromptStyle", no pkg:: prefix).
  2. shinychat:::extend_record_linear() and shinychat:::derive_node_ui_from_turns() call ellmer::contents_replay() without .envir, so it defaults to parent.frame() — a frame inside shinychat's namespace.
  3. contents_replay() resolves bare class names with get0(name, envir, inherits = TRUE). The namespace parent chain reaches globalenv() but never descends into the app environment where Shiny sourced the class, so the lookup fails.

The current workaround is for apps to assign("MyClass", MyClass, envir = globalenv()), which works but is non-obvious and easy to get wrong.

Possible directions — not sure what the right fix is
  • Capture the app's environment at chat_server() setup (where the client/turns live) and pass it as .envir to contents_replay().
  • Expose a replay environment on history_options() so apps defining custom content classes can say where they live.
  • At minimum, document that custom Content subclasses must be resolvable from globalenv() (or a package namespace) for history to work.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with shinychat:::extend_record_linear() and shinychat:::derive_node_ui_from_turns(), then inspect ellmer::contents_replay() and its .envir lookup. Reproduce the failure using an S7 Content subclass in a child app environment, and determine how the replay context should be supplied. Done means history replay succeeds for app-local classes without requiring assignment to globalenv(), with regression coverage.

Written by the indexing model from the issue text.

Assessment

Tech stack
r
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.