Repository navigation
v0.33.0
Subagent capabilities for UI builders: metadata reads, background snapshots, cooperative callback cancellation, and the task/background/permission signals the CLI already emits — all raw data and controls, no status model. Ruby-ahead of the Python SDK (0.2.153).
Added
get_subagent_metadata/get_subagent_metadata_from_store: read optional subagent metadata with original string keys, including type, spawning tool ID, parent agent, depth, and future CLI fields. Disk reads reuse transcript scoping without parsing the transcript; store reads select the latest metadata entry even before messages arrive. These are historical reads, not live-status queries.background_tasksandsession_cronsonStopHookInput/SubagentStopHookInput. Raw snapshots preserve unavailable (nil) versus explicitly empty ([]) and describe the parent session, not all foreground/background agents.- Cooperative permission cancellation through
ToolPermissionContext#signal(CancellationSignal#cancelled?/#wait) and the associatedrequest_id. CLI cancellation, disconnect, EOF, and failed dispatch invalidate pending requests, including callbacks running on worker threads; normal decisions do not. User threads are not forcibly stopped, and late decisions after observed cancellation cannot become allow responses. HookContext#signal/#request_iduse the same per-invocation cancellation contract, including hook timeouts. Thread callbacks can cooperate with cancellation; late hook output is discarded.- A minimal subagent event subscription example and capability reference covering lifecycle events, metadata, background snapshots, and permission cancellation. UI and application status aggregation remain outside the SDK.
- Opt-in real-CLI subagent contract tests for ID/metadata/text correlation, permission cancellation on interrupt, and background completion/stop after a parent result. They self-skip without CLI credentials.
- Subagent UI signals the CLI already emits, read from the schema embedded in Claude Code CLI 2.1.278 (the Python SDK has none of these as of Python SDK 0.2.153; only partly verified live — a smoke run against CLI 2.1.278 confirmed the
task_startedfields and the targeted-missbackground_tasksresponse; the rest is schema-derived). The SDK exposes raw data and controls only — no status aggregation.TaskStartedMessage#subagent_type/#is_backgrounded/#spawn_depth,TaskProgressMessage#subagent_type,TaskNotificationMessage#reason/#resource_links(raw Array, symbol keys with the wire spelling), the#skip_transcript/#ambientdisplay flags on bothTaskStartedMessageandTaskNotificationMessage(hints for the host — the SDK never filters frames or computes activity), andTaskUpdatedMessage#is_backgrounded/#error/#end_time/#total_paused_ms/#descriptionderived frompatchlikestatus.is_backgroundedkeepsnil(not reported) distinct from an explicitfalse(foreground, spawning tool call blocking).TaskUpdatedMessagenow also reads a string-keyedpatchon hand-built messages.BackgroundTasksChangedMessage(background_tasks_changed): the full set of live background tasks, a level signal with REPLACE semantics. The SDK's own stdin-close bookkeeping still deliberately ignores this frame.PermissionDeniedMessage(permission_denied): a tool call auto-denied without an interactive prompt, withagent_idfor subagent routing (not a permissionrequest_id). A best-effort advisory, not a complete denial feed —ResultMessage#permission_denialsstays authoritative.Client#background_tasks(tool_use_id: nil)/Query#background_tasks: send in-flight foreground tasks to the background (Ctrl+B). Keyed by the spawningtool_use_id, nottask_id/agent_id. The targeted form returns{ backgrounded: true }or{ backgrounded: false }— a definitive miss, after which no event is coming;nilis the explicit all-tasks form and returns{}. Because the CLI treats''as "all tasks", anything other thannilor a non-empty String raisesArgumentErrorbefore a request is written.TaskUpdatedMessage#is_backgrounded/BackgroundTasksChangedMessagereport the lifecycle state that follows.ClaudeAgentOptions#agent_progress_summaries: request model-generated progress summaries for subagents;TaskProgressMessage#summarymay then be present, and stays optional. Tri-state;nilomitsagentProgressSummariesfrom theinitializerequest andtrue/falseare forwarded verbatim. An enable switch, not a live toggle: CLI 2.1.278 only acts on a truthy value, sofalseis equivalent to unset.- Compatibility:
background_tasks_changedandpermission_deniedframes previously parsed as a genericSystemMessage. Both new classes subclass it, sowhen SystemMessageand#dataconsumers are unaffected; code matching onmessage.class == SystemMessagewill no longer see them.
Fixed
- Best-effort control error/cancellation replies no longer leak a secondary connection error when the CLI has already exited. Original transport read errors still propagate to the consumer.
- Control-request tracking is identity-guarded: if the CLI ever reused an in-flight request ID, the first handler finishing no longer untracks the later request's cancellation signal or task, so EOF, disconnect, and
control_cancel_requeststill invalidate it and its late decision cannot be sent as a success. ClaudeAgentSDK.configuredefaults now merge by option, not by literal key spelling.ClaudeAgentOptionsaccepts symbol/string and snake_case/camelCase names, but the defaults merge compared raw keys, so a differently spelled per-call key rode along as a second entry:'permissionMode' => nilwiped a configuredpermission_mode:instead of inheriting it, and a Hash option such as'env' => {...}replaced the configuredenv:instead of merging into it. Unknown option names are still reported with the caller's own spelling.
Usage
# Send one blocking subagent to the background (the Ctrl+B control request).
# The key is the spawning tool_use id — never pass "" or a missing id: nil means ALL foreground tasks.
result = client.background_tasks(tool_use_id: task_started.tool_use_id)
result[:backgrounded] # => true, or false for a definitive miss
# Cancellation-aware permission callback
can_use_tool = lambda do |_tool, _input, context|
loop do
break ClaudeAgentSDK::PermissionResultDeny.new(message: 'Request cancelled') if context.signal.cancelled?
decision = decisions_for(context.request_id).pop(timeout: 0.1)
break decision if decision
end
endWhat's changed
- #60 Add subagent metadata, background snapshots, and callback cancellation — @ya-luotao
- #61 Expose subagent UI signals the CLI already emits — @ya-luotao
- #62 Identity-guard control-request tracking against a reused request ID — @ya-luotao
- #63 Merge configured defaults by option, not by literal key spelling — @ya-luotao
Full Changelog: v0.32.0...v0.33.0
gem install claude-agent-sdk -v 0.33.0