feat(py): the Commons class, its assembly order, chat() and stream_async() - #311
Merged
Conversation
This was referenced Sep 8, 2026
jat255
force-pushed
the
jat255/pvrd-commons-class
branch
from
September 8, 2026 04:40
66f84a1 to
7c126fb
Compare
jat255
marked this pull request as ready for review
September 8, 2026 05:26
…ync() The agent itself: it validates its inputs, assembles the layers in the order pkg-r/R/commons.R does, registers the tools its composition earns, and sets the system prompt those tools and sources render. A chatlas.Chat is composed as a private ._client rather than subclassed (D1), so the public surface is chat(), stream_async(), citation_corpus(), prewarm(), queue_restore_reminder(), and the three turn accessors the two turn rules hook: add_turn() restarts the citation request when a person asks something, set_turns() drops a reminder queued for history that is being replaced. stream_async() takes chatlas' own signature, less data_model, because shinychat drives a client with stream_async(input, *contents, content="all", controller=controller). It projects every text chunk through the citation scanner, so reserved model markup cannot reach a browser, then derives the provenance tag from the tags its tools set plus the scanner's verified flag and yields the marker last. No tracing and no execution tool, so no network, log or share_with argument: w0t2 retrofits the spans and M6 brings run_python, each with the arguments it needs. No conversation-id accessors, per D5. The tests drive the real chat loop over a scripted chatlas provider, so the tool loop, the turns it appends, the streamed chunk types, and the stop button's controller are all real; only the network call is scripted.
chatlas streams the model's text as a plain str in both content modes, so the citation scanner sees every chunk of it and content="all" changes only which non-text objects come alongside. That is worth pinning rather than reasoning about: the test reads the displayed text off str chunks and off anything carrying .text, so text that started arriving as a content object would fail here instead of reaching a browser unprojected.
… over pkg-r/R/commons.R:231 initializes from the client's provider and model, so an agent there starts from a chat of its own and the object the caller passed in is never touched. Do the same: hold a new chatlas.Chat built from client.provider, which carries the model, instead of calling set_tools() and assigning system_prompt on the caller's object. Keeping the two APIs alike is worth more than the convenience of adopting whatever the client already had. The system-prompt warning stays. The tools half of it goes: a chat built here has none to discard, which is R's position too. Three pieces of client state would otherwise vanish. kwargs_chat and conversation_id are public and come across. set_model_params() state has a setter and no getter in chatlas, so it is read off the attribute behind the setter and written back through the setter itself, which re-checks it against the provider; an attribute that is missing or no longer a mapping warns and names set_model_params(), so a renamed internal costs a warning rather than a silently dropped temperature. Turns do not come across, matching R, but they warn: history is the caller's own data, they can still reach it on the client they kept, and the warning names the agent's set_turns() as the way back.
Chat.__init__ keeps the kwargs_chat it is handed, so passing the caller's dict straight through left both chats sharing one object: adding a key to it later would reach the agent, which is the opposite of what building a separate chat is for. Take a shallow copy instead. The provider stays shared, because it is what carries the model the caller chose, and it now says so where it is passed. A value nested inside the arguments stays shared too: a provider argument can be any object, so a deep copy could fail on one, and the shallow copy already stops the case that happens.
…andoned streams Findings from a review pass over the assembly PR. The ignored-client-state warnings now run straight after the client type check, as pkg-r/R/commons.R does: checked after the other arguments, a bad data_sources raised before the warning fired, where R warns and then fails. get_turns() forwarded nothing of chatlas's own signature, so include_system_prompt and tool_result_role were a TypeError here while R, inheriting ellmer, exposes them. Both are forwarded. _projected() never closed the stream it wraps: a consumer that breaks out without cancelling left the provider's stream to garbage collection. The loop now sits in a try/finally that closes it. The class docstring spoke numpydoc in a package that documents in prose, and never said what construction raises; both fixed, and the module docstring's "port plan" wording goes with them (D1 stands alone, as D8 does in _data_source.py, and D10 now does in _citations.py). The chat() path shared stream_async()'s turn rules with none of its coverage: dropping the reminder prep, the citation-request reset, or the reminder spend from chat() left the suite green, and nothing checked that set_turns() forwards. Tests now pin each, verified by deleting each line and watching the matching test fail.
jat255
force-pushed
the
jat255/pvrd-commons-class
branch
from
September 8, 2026 05:26
84fead3 to
da78380
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Assembles the pieces the rest of M5 landed into an agent that answers a question. Per D1 this composes
chatlas.Chatas a private._clientrather than inheriting from it, so the public surface is a decision R never had to make.Commons(client, data_sources, semantic_layer=None, context_layer=None, *, instructions=None). The constructor validates, then assembles in R's order.data_sourcestakes oneDataSourceor a mapping of them.Implementation
Construction. A bare source is filed under an internal key and offers nothing to inject, which matches R's
have_name()filter. Three arguments in the port plan's signature are absent:networkonly ever gates the execution tool, andrun_pythonbelongs to M6, whilelogandshare_withare tracing arguments and_tracing.pydoes not exist. Each would be a parameter that does nothing until the milestone that gives it meaning adds it with its gate.The client is not taken over. The agent builds its own chat from the client's provider, as
pkg-r/R/commons.Rdoes, so it never changes an object its caller still holds. A system prompt on the client is ignored with a warning, in R's wording. Both warnings about state the client carried fire before any other argument is checked, in R's order, so a bad later argument does not eat them. Model parameters carry over, read from the attribute behindset_model_params()because chatlas has no getter, and written back through the public setter; if that attribute is missing or is no longer a mapping, the agent warns and names the setter rather than dropping a temperature in silence.kwargs_chatis copied one level deep. The provider itself is shared, because it carries the model, which is also true ofclient$get_provider()in R.stream_async()takes chatlas's signature withoutdata_model, whose chunks are JSON to be parsed whole and would not survive an appended marker. It accepts extra positional content, acontent=mode and aStreamController, which is the shape shinychat calls with, so the Shiny surface can drive an agent without an adapter that reimplements the scanner. Everystrchunk goes through the citation scanner and only the projection is yielded, so reserved markup never reaches a browser. The provenance tag is derived after the stream from the collected tags and the scanner's verified flag, and the aside is yielded last. A consumer that walks away without cancelling still closes the provider's stream.prewarm()covers the context store and each source with notry/except, so a cold cache fails a deploy rather than warning.Divergence from R
R starts a new conversation and loses history on the handed-over client in silence. This warns and names
agent.set_turns(). History is data the caller created, and now that the client is not mutated it is recoverable, since the caller still holds it. R has no such warning because R has never had a reason to look.Deliberately absent
commons_agent_create, whichw0t2retrofits in one sweep.semantic_modelsorcalculationsregistry, since both read warehouse semantic objects and that stays deferred.Commonsis not exported from__init__.py. The export and the README example are5mys, which keeps the README from claiming an import that does not resolve.Tests
tests/_provider.pyimplements chatlas'sProvideras a scripted replay, so the realchatlas.Chatruns the real tool loop and only the network call is scripted.anthropicis not a dependency here, soChatAnthropiccannot be constructed in this suite.The turn rules are pinned through both entry points:
chat()andstream_async()each carry the reminder, citation-request-reset, and restore-reminder-spend tests,set_turns()is checked to forward rather than drop, and each of these was verified by deleting the line it guards and watching the test fail.No new shared fixture. Nine existing fixtures already pin everything the assembly order is observable through, and the order has no output of its own. The one unpinned cross-language claim is this stream's emission order, which needs a live streaming harness that R's own streaming tests skip without development ellmer.
5mysis asked to decide whether the end-to-end path earns a fixture, and it will have that harness.Verified:
ruff check,pyrefly check src tests, andpytest(1289 passing). No R files changed.