Skip to content

chore: bump SDK to 0.2.77 with parity updates#24

Merged
arcaputo3 merged 4 commits intomainfrom
chore/sdk-0.2.77-parity
Mar 18, 2026
Merged

chore: bump SDK to 0.2.77 with parity updates#24
arcaputo3 merged 4 commits intomainfrom
chore/sdk-0.2.77-parity

Conversation

@arcaputo3
Copy link
Copy Markdown
Contributor

Summary

  • PostCompact hook event: fires after context compaction with compact_summary field
  • AgentMessage.ApiRetry: new system/api_retry message emitted on retryable API errors
  • Claude.forkSession(): fork a session to a new branch with fresh UUIDs
  • QueryStream.applyFlagSettings(): apply Settings mid-session (streaming input mode only)
  • AccountInfo.apiProvider: new field ('firstParty' | 'bedrock' | 'vertex' | 'foundry')
  • HookInput.PermissionRequest: new title, displayName, description fields from the bridge
  • InstructionsLoadReason.Compact: new 'compact' load reason value
  • SandboxFilesystemConfig: new allowRead and allowManagedReadPathsOnly fields
  • fix(a2a): publish status-update before message event on task completion (ordering fix)

Test plan

  • ./mill agent.compile — clean compile, no errors
  • ./mill agent.test — all tests pass
  • ./mill examples.compile — all examples compile

🤖 Generated with Claude Code

arcaputo3 and others added 2 commits March 18, 2026 13:47
The A2A SDK's event queue breaks on whichever event arrives first.
Publishing the final status-update before the message event ensures the
task store transitions to "completed" before the message is emitted.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
New SDK types and APIs added in 0.2.76–0.2.77:

- PostCompact hook event: fires after context compaction with compact_summary
- HookInput.PostCompact case class and parseHookInput handler
- AgentMessage.ApiRetry: new system/api_retry message for retryable errors
- Claude.forkSession(): fork a session to a new branch with fresh UUIDs
- QueryStream.applyFlagSettings(): apply Settings mid-session
- AccountInfo.apiProvider: 'firstParty' | 'bedrock' | 'vertex' | 'foundry'
- HookInput.PermissionRequest: new title/displayName/description fields
- InstructionsLoadReason.Compact: new 'compact' load_reason value
- SandboxFilesystemConfig: new allowRead and allowManagedReadPathsOnly fields

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Mar 18, 2026

PR Review: chore: bump SDK to 0.2.77 with parity updates

Good SDK parity update overall. The A2A ordering fix is correct and well-documented. A few observations below:


Bugs / Potential Issues

forkSession always sends an empty object instead of undefined (Claude.scala:297)

val opts = js.Dynamic.literal()
// ...
val jsOpts: js.UndefOr[js.Dynamic] = opts  // always defined, never js.undefined

Assigning a js.Dynamic to a js.UndefOr[js.Dynamic] always produces a defined (non-undefined) value. When all options are None, the SDK receives {} rather than undefined. The established pattern elsewhere (e.g., getSessionInfo) uses a conditional match:

val opts = dir match
  case Some(d) => js.Dynamic.literal(dir = d)
  case None    => js.undefined

This is unlikely to break anything in practice (the SDK should tolerate an empty options object), but it's inconsistent with the codebase convention and worth fixing for correctness.


Design Concerns

ApiRetry.error is non-optional, inconsistent with AgentMessage.Assistant (AgentMessage.scala:164)

AgentMessage.Assistant declares error: Option[AssistantMessageError], but the new ApiRetry uses error: AssistantMessageError (non-optional, defaulting to Unknown when absent). If the SDK could ever emit an api_retry without an error field, this silently maps to Unknown with no way for callers to distinguish "no error field provided" from "genuinely unknown error type". Using Option[AssistantMessageError] would be consistent with the existing pattern.

parseApiRetryMessage swallows missing required fields (MessageConverter.scala:416-417)

attempt = intField(obj, "attempt").getOrElse(0),
maxRetries = intField(obj, "max_retries").getOrElse(0),
retryDelayMs = longField(obj, "retry_delay_ms").getOrElse(0L),

A default of 0 for attempt/maxRetries/retryDelayMs silently hides a parse failure. These are presumably always present in valid SDK messages; requiredInt (if it exists) or at minimum a comment acknowledging the fallback would help. Compare how requiredUuid / requiredSessionId are used in the same function.

applyFlagSettings accepts js.Dynamic (QueryStream.scala:368)

This bypasses Scala's type system entirely. If the codebase has a Settings case class, accepting that (and converting internally) would give compile-time safety. If Settings doesn't map cleanly, at minimum a typed wrapper or a comment explaining what shape is expected would help.

AccountInfo.apiProvider is untyped Option[String] (QueryStream.scala:503)

The SDK documents this as 'firstParty' | 'bedrock' | 'vertex' | 'foundry'. A sealed trait/enum like the existing PermissionMode would make exhaustive matching possible and prevent typos at call sites.


Minor Observations

PostCompact.compactSummary: String vs Option[String] (HookInput.scala:207)

HookCallback parses this as .getOrElse(""). If the SDK can omit the field, Option[String] would be more accurate (an empty string is a valid summary, distinct from "not provided"). Compare PreCompact.customInstructions: Option[String] which correctly models optionality.

forkSession upToMessageId: Option[String] (Claude.scala:289)

This parameter is a message UUID. Using Option[MessageUuid] (if that type exists) would prevent passing an arbitrary string and align with how other session-related IDs are typed in the codebase.

Version constraint loosening (package.json:13)

The constraint changed from the exact pin "0.2.75" to the range "^0.2.77". This means future bun install runs can silently pick up 0.2.78, 0.2.79, etc. If you want reproducible installs, keep the pin exact and bump it explicitly — the bun.lock provides the actual reproducibility, but the package.json range allows bun install --latest or fresh checkouts without a lockfile to drift.


What Looks Good

  • A2A ordering fix: The status-update-before-message reordering is correct, and the comment clearly explains the SDK's constraint. Both success and error paths are updated symmetrically.
  • HookEvent.PostCompact / HookInput.PostCompact: Clean parity with PreCompact — same field set, symmetric parsing in HookCallback.
  • InstructionsLoadReason.Compact: Straightforward addition, all three places (enum, toRaw, fromString) updated consistently.
  • SandboxFilesystemConfig additions: allowRead/allowManagedReadPathsOnly follow the existing conditional-serialization pattern (if nonEmpty / if true).
  • HookInput.PermissionRequest new fields: title, displayName, description added correctly with multi-key fallback in HookCallback.

🤖 Generated with Claude Code

- Pass js.undefined instead of empty {} when all options are None,
  matching the pattern used by getSessionInfo/renameSession
- Change upToMessageId from Option[String] to Option[MessageUuid]
  for type-safe message UUID references

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Mar 18, 2026

Code Review — chore: bump SDK to 0.2.77 with parity updates

Overall this is a clean, focused parity update that mirrors the upstream SDK additions faithfully. The A2A ordering fix is particularly well-documented. A few items worth discussing:


Issues / Concerns

1. Version pin changed to range (package.json)

-"@anthropic-ai/claude-agent-sdk": "0.2.75",
+"@anthropic-ai/claude-agent-sdk": "^0.2.77",

The previous pin was an exact version; now it's a ^-range. This means future bun install runs on a clean environment could silently pull in 0.2.78+ before the Scala bindings are updated, potentially causing runtime mismatches. Consider keeping an exact pin, or document the intentional loosening.


2. forkSession result parsing is unchecked (Claude.scala)

.map(result => SessionId(result.sessionId.asInstanceOf[String]))

If the underlying JS promise resolves to an unexpected shape (or the SDK changes the response field name), this will throw a ClassCastException at runtime and bypass mapError, leaking an untyped failure. Safer pattern used elsewhere in the codebase:

.map(result => SessionId(result.sessionId.asInstanceOf[js.UndefOr[String]].getOrElse("")))
// or assert with a meaningful error:
.flatMap(result =>
  ZIO.fromOption(result.sessionId.asInstanceOf[js.UndefOr[String]].toOption)
    .mapError(_ => AgentError("forkSession: missing sessionId in response"))
)

3. applyFlagSettings accepts raw js.Dynamic (QueryStream.scala)

def applyFlagSettings(settings: js.Dynamic): Task[Unit]

All other configuration entry-points in this SDK accept typed Scala objects (e.g. Settings, AgentOptions) and call .toRaw internally. Accepting js.Dynamic here breaks the pattern and forces callers to construct raw JS objects manually, which is error-prone. Consider:

def applyFlagSettings(settings: Settings): Task[Unit] =
  ZIO.fromPromiseJS(rawQuery.applyFlagSettings(settings.toRaw))

4. AccountInfo.apiProvider could be an enum (QueryStream.scala)

apiProvider: Option[String] = None

The valid values are documented in the PR ('firstParty' | 'bedrock' | 'vertex' | 'foundry'). A simple sealed enum would provide type safety and better exhaustiveness checking:

enum ApiProvider:
  case FirstParty, Bedrock, Vertex, Foundry
  case Unknown(value: String)

This follows the same pattern used for AssistantMessageError, ErrorReason, etc. elsewhere in the codebase.


5. PostCompact.compactSummary defaults to empty string (HookInput.scala)

compactSummary = firstString(raw, "compact_summary", "compactSummary").getOrElse(""),

The companion PreCompact doesn't have a summary field at all. Using "" as the default makes it impossible for callers to distinguish "summary not provided" from "summary was empty". Option[String] would be more idiomatic here:

compactSummary: Option[String] = None,
// parsing:
compactSummary = firstString(raw, "compact_summary", "compactSummary"),

Positive observations

  • A2A ordering fix (A2AServer.scala): The reordering of publishStatusUpdate before bus.publish(jsMsg) is correct and the explanatory comments clearly document the "why" — good defensive comment for a subtle protocol ordering constraint.
  • SandboxFilesystemConfig additions follow the existing toRaw conditional pattern perfectly.
  • InstructionsLoadReason.Compact and HookEvent.PostCompact are added consistently across all three sites (enum definition, toRaw, fromString).
  • The hasOptions short-circuit in forkSession to avoid constructing an empty options object is a nice touch.

Test coverage

The PR checklist confirms ./mill agent.test passes but no new unit tests are added for:

  • Claude.forkSession() (at least a mock/unit test for the JS interop path)
  • AgentMessage.ApiRetry parsing (a MessageConverter round-trip test)
  • HookCallback parsing for PostCompact and the new PermissionRequest fields

These are all parsing paths where a typo in a field name ("compact_summary" vs "compactSummary") would silently produce None/"" with no test failure. The existing test suite style should support adding these cheaply.


Summary: Approve with the above suggestions — items 2 and 3 are the most impactful to address before merge, as they introduce untyped runtime failure modes.

- forkSession result uses js.UndefOr with meaningful error on missing sessionId
- Add MessageConverter tests: api_retry parsing, null error_status handling
- Add HookCallback tests: PostCompact parsing, PermissionRequest title/displayName/description
- Add HookInputEnum test: InstructionsLoadReason.Compact round-trip

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Mar 18, 2026

PR Review: chore: bump SDK to 0.2.77 with parity updates

Overall this is a clean, well-structured parity update. The changes are consistent with existing patterns in the codebase. A few things worth flagging:


Potential Issues

1. Version range change in package.json (minor concern)

-"@anthropic-ai/claude-agent-sdk": "0.2.75",
+"@anthropic-ai/claude-agent-sdk": "^0.2.77",

The original version was pinned exactly (0.2.75), which is typically intentional for SDK dependencies to avoid unexpected breakage. Switching to ^0.2.77 allows patch/minor updates to pull in automatically on bun install in fresh environments (though bun.lock protects CI). If the pinned version was deliberate, this is worth a conscious choice. If not, it's fine.

2. forkSession: double mapError creates a subtle error type conflict

ZIO
  .fromPromiseJS(SdkModule.forkSession(sessionId.value, jsOpts))  // E = Throwable
  .flatMap { result =>
    val sid = result.sessionId.asInstanceOf[js.UndefOr[String]].toOption
    ZIO.fromOption(sid).mapError(_ =>
      new RuntimeException("forkSession: missing sessionId in response")  // E = RuntimeException
    )
  }
  .map(SessionId(_))
  .mapError(AgentError.fromThrowable)  // This only catches the outer Throwable; inner RuntimeException is already a Throwable so it works, but the intent is obscured

This compiles and works (since RuntimeException <: Throwable), but the pattern is a bit confusing — the inner mapError wraps with RuntimeException, and then the outer mapError re-wraps it via fromThrowable. Compare with getSessionInfo which uses a cleaner single mapError. Consider:

ZIO.fromOption(sid).orElseFail(AgentError.fromThrowable(
  RuntimeException("forkSession: missing sessionId in response")
))

3. applyFlagSettings: streaming-input mode constraint not enforced

def applyFlagSettings(settings: js.Dynamic): Task[Unit] =
  ZIO.fromPromiseJS(rawQuery.applyFlagSettings(settings))

The ScalaDoc says "Only available in streaming input mode", but there is no guard here. If called outside of streaming input mode the SDK will likely reject it at runtime. A note about the expected failure mode (or wrapping the SDK error with a more descriptive AgentError) would improve debuggability. At minimum, the existing pattern of mapError(AgentError.fromThrowable) is missing — this returns Task[Unit] (i.e., IO[Throwable, Unit]) while most of the QueryStream API returns typed errors. Inconsistency to consider.

4. HookInput.PostCompact.compactSummary — silent empty default

compactSummary = firstString(raw, "compact_summary", "compactSummary").getOrElse(""),

If the SDK omits compact_summary (e.g., due to a version mismatch), this silently defaults to "". Compare with compactRatio in PreCompact — is an empty summary a reasonable sentinel here, or would Option[String] be a better fit? It's a minor API design point but worth considering for forward compatibility.


Positives

  • A2A ordering fix (A2AServer.scala): The swap of publishStatusUpdate before bus.publish(jsMsg) is a correct fix, and the comments clearly explain the SDK's event-queue behavior. Good.
  • AccountInfo.apiProvider uses Option[String] with a default — good backwards-compatible addition.
  • SandboxFilesystemConfig correctly omits allowManagedReadPathsOnly from the JS object when false (doesn't pollute the config).
  • Test coverage is solid for the new surface area: PostCompact, PermissionRequest new fields, ApiRetry (including the null error_status edge case), and InstructionsLoadReason.Compact roundtrip.
  • longField for retryDelayMs is consistent with the existing pattern used for durationMs.

Missing Tests

  • Claude.forkSession() — no unit/integration test added for the new public API.
  • QueryStream.applyFlagSettings() — no test.

These may be hard to test without a live SDK, but if there are mock patterns elsewhere in the test suite they'd be worth adding.


Summary: The changes are correct and follow existing conventions. The items above are mostly minor quality/consistency notes. The A2A ordering fix and SDK parity additions look good to merge once the version-pin question is resolved intentionally.

@arcaputo3 arcaputo3 merged commit 10a0ff4 into main Mar 18, 2026
3 checks passed
@arcaputo3 arcaputo3 deleted the chore/sdk-0.2.77-parity branch March 18, 2026 22:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant