Skip to content

feat(py): the Commons class, its assembly order, chat() and stream_async() - #311

Merged
jat255 merged 5 commits into
jat255/hsjq-toolsfrom
jat255/pvrd-commons-class
Sep 8, 2026
Merged

feat(py): the Commons class, its assembly order, chat() and stream_async()#311
jat255 merged 5 commits into
jat255/hsjq-toolsfrom
jat255/pvrd-commons-class

Conversation

@jat255

@jat255 jat255 commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Assembles the pieces the rest of M5 landed into an agent that answers a question. Per D1 this composes chatlas.Chat as a private ._client rather 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_sources takes one DataSource or 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: network only ever gates the execution tool, and run_python belongs to M6, while log and share_with are tracing arguments and _tracing.py does 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.R does, 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 behind set_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_chat is copied one level deep. The provider itself is shared, because it carries the model, which is also true of client$get_provider() in R.

stream_async() takes chatlas's signature without data_model, whose chunks are JSON to be parsed whole and would not survive an appended marker. It accepts extra positional content, a content= mode and a StreamController, which is the shape shinychat calls with, so the Shiny surface can drive an agent without an adapter that reimplements the scanner. Every str chunk 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 no try/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

  • No OpenTelemetry spans, including commons_agent_create, which w0t2 retrofits in one sweep.
  • No conversation-id accessors, per D5.
  • No semantic_models or calculations registry, since both read warehouse semantic objects and that stays deferred.
  • Commons is not exported from __init__.py. The export and the README example are 5mys, which keeps the README from claiming an import that does not resolve.

Tests

tests/_provider.py implements chatlas's Provider as a scripted replay, so the real chatlas.Chat runs the real tool loop and only the network call is scripted. anthropic is not a dependency here, so ChatAnthropic cannot be constructed in this suite.

The turn rules are pinned through both entry points: chat() and stream_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. 5mys is 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, and pytest (1289 passing). No R files changed.

@jat255 jat255 added this to the py-M5: agent, tools, and prompt milestone Sep 7, 2026
@jat255 jat255 added py Affects the Python implementation needs-manual-review Agent-created work that needs a human review labels Sep 7, 2026
@jat255
jat255 force-pushed the jat255/pvrd-commons-class branch from 66f84a1 to 7c126fb Compare September 8, 2026 04:40
@jat255 jat255 removed the needs-manual-review Agent-created work that needs a human review label Sep 8, 2026
@jat255
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
jat255 force-pushed the jat255/pvrd-commons-class branch from 84fead3 to da78380 Compare September 8, 2026 05:26
@jat255
jat255 merged commit ed792d3 into main Sep 8, 2026
8 checks passed
@jat255
jat255 deleted the jat255/pvrd-commons-class branch September 8, 2026 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

py Affects the Python implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant