Surface message origin on UserMessage and ResultMessage - #1199
Conversation
In streaming-input mode one connection interleaves the turns the
application sends with turns the session injects on its own --
background-task notifications, fired scheduled-task prompts, MCP channel
messages, messages relayed from peer sessions. The CLI attributes these
with an `origin` object on user messages and forwards the triggering
message's origin on each result, so a consumer can tell "this result
answers my prompt" from a task-notification follow-up. The TypeScript
SDK types it as SDKMessageOrigin (a union on `kind`: human, channel,
peer, task-notification with an optional scheduled-trigger /
peer-send-message subkind, coordinator, unclassified, observer,
auto-continuation, observer-activity). The Python parser dropped the
field entirely, so none of it was visible to Python apps.
Add a `MessageOrigin` TypedDict (functional form, since the wire key
`from` is a keyword) with `kind` required and the per-kind keys
optional, plus `MessageOriginKind` / `TaskNotificationOriginSubkind`
literals, and an `origin: MessageOrigin | None` field on UserMessage and
ResultMessage. The parser passes the CLI's dict through untouched when
it is an object with a string `kind` (so newer kinds and fields stay
visible) and treats anything else as absent.
Unit tests cover both user-content shapes, pass-through of unmodeled
keys, absent/malformed origin, and the result arms. An e2e test streams
a user message stamped `{"kind": "human"}` and an unstamped one and
asserts the results echo `{"kind": "human"}` and None respectively.
|
@claude please review this PR for correctness, edge cases, and test coverage. |
|
Claude finished @qing-ant's task in 1m 39s —— View job Code Review: Surface message origin on UserMessage and ResultMessage
Verified: SummaryClean, well-scoped PR that closes the TypeScript-parity gap for types.pyGood choices:
No issues found. The type is faithfully documented with per-kind field semantics in comments, and the message_parser.py —
|
chrislloyd
left a comment
There was a problem hiding this comment.
Stamp via #claude-code-shadow-stamps
Port of Python SDK commit d48fa33 (anthropics/claude-agent-sdk-python#1199). Fixes #583 Co-authored-by: Claude <noreply@anthropic.com>
Summary
TypeScript-parity gap (
SDKMessageOrigin; theunclassifiedkind and thetask-notificationsub-kinds are the most recent additions there, but Python had none of it). In streaming-input mode one connection interleaves the turns the application sends with turns the session injects on its own — background-task notifications, fired scheduled-task prompts, MCP channel messages, messages relayed from peer sessions. The CLI attributes these with anoriginobject on user messages and forwards the triggering message's origin on each result, so a consumer can tell "this result answers my prompt" from a task-notification follow-up. The Python parser dropped the field entirely.types.py:MessageOriginTypedDict (functional form, since the wire keyfromis a keyword) withkindrequired and the per-kind keys optional and documented;MessageOriginKind = Literal["human", "channel", "peer", "task-notification", "coordinator", "unclassified", "observer", "auto-continuation", "observer-activity"];TaskNotificationOriginSubkind = Literal["scheduled-trigger", "peer-send-message"]; neworigin: MessageOrigin | None = Nonefield (appended, defaulted) onUserMessageandResultMessage.message_parser.py:_parse_originpasses the CLI's dict through untouched when it is an object with a stringkind— so newer kinds/fields stay visible — and treats anything else as absent.__init__.py: exports.Usage:
Prompts sent through
query()/ClaudeSDKClient.query()arrive unattributed (origin is None) unless the host stamps"origin": {"kind": "human"}on the message dict itself; only thehumankind is honored from an SDK host.Test plan
tests/test_message_parser.py: origin on user messages for both content shapes incl. pass-through of unmodeled keys; absent / non-object / kind-less origin →None; result origin forhuman, bothtask-notificationsub-kinds, andunclassified.e2e-tests/test_message_origin.py(runs in CI against the real CLI): stream a user message stamped{"kind": "human"}and then an unstamped one throughClaudeSDKClient; assert the results carry{"kind": "human"}andNonerespectively. Passed locally against CLI 2.1.227.ruff check,ruff format --check,mypy src/, fullpytest tests/green locally; also checked that the public typing is usable under mypy/pyright (o["kind"] == "human",o.get("subkind")) on 3.10 (typing_extensions) and 3.13.