Skip to content

Commit a842ae9

Browse files
arcaputo3claude
andcommitted
feat: Update to Claude Agent SDK 0.2.22
Updates Scalagent to support Claude Agent SDK 0.2.22 with the following changes: **New Message Types:** - TaskNotification: Task/subagent completion notifications with status, outputFile, summary - ToolUseSummary: Aggregate tool use info with summary and preceding tool use IDs - TaskStatus enum: Completed, Failed, Stopped, Custom **New System Events:** - HookStarted: Hook execution started notification - HookProgress: Hook execution progress with stdout/stderr/output - Enhanced HookResponse: Added hookId, output, outcome fields - HookOutcome enum: Success, Error, Cancelled, Custom **New QueryStream Methods:** - close(): Abort running query - reconnectMcpServer(): Reconnect a specific MCP server - toggleMcpServer(): Enable/disable MCP server - rewindFiles(): Restore files to previous state - setMcpServers(): Configure MCP servers dynamically **Other Enhancements:** - PermissionMode.Delegate for delegated permission handling - McpConnectionStatus.Disabled for disabled servers - Enhanced McpServerStatus with error, scope, tools fields - McpToolInfo and McpToolAnnotations types - AgentOptions.withMainAgent() for main thread agent config **Dependencies:** - @anthropic-ai/claude-code-sdk: 0.2.22 - zod: 4.0.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f5b8a68 commit a842ae9

12 files changed

Lines changed: 590 additions & 29 deletions

File tree

build.mill

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ object agent extends ScalaJSModule with PublishModule {
3131
// Publishing configuration - read from PUBLISH_VERSION env var (set by CI)
3232
// Task.Input ensures env var is re-evaluated each run (not cached)
3333
override def publishVersion = Task.Input {
34-
Task.env.get("PUBLISH_VERSION").getOrElse("0.2.1-SNAPSHOT")
34+
Task.env.get("PUBLISH_VERSION").getOrElse("0.2.2-SNAPSHOT")
3535
}
3636

3737
// Skip scaladoc generation - Scala.js facades cause doc errors

bun.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "scalagent",
3-
"version": "0.2.1",
3+
"version": "0.2.2-SNAPSHOT",
44
"private": true,
55
"type": "module",
66
"description": "Scalagent - Type-safe Scala.js SDK for Claude Code CLI",
@@ -10,8 +10,8 @@
1010
},
1111
"dependencies": {
1212
"@a2a-js/sdk": "^0.3.7",
13-
"@anthropic-ai/claude-agent-sdk": "^0.2.1",
14-
"zod": "3.24.1"
13+
"@anthropic-ai/claude-agent-sdk": "^0.2.22",
14+
"zod": "^4.0.0"
1515
},
1616
"devDependencies": {
1717
"@types/bun": "^1.3.5"

src/com/tjclp/scalagent/config/AgentOptions.scala

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,11 @@ final case class AgentOptions(
8282
plugins: List[PluginConfig] = List.empty,
8383

8484
// Subagents
85-
agents: Map[String, AgentDefinition] = Map.empty
85+
agents: Map[String, AgentDefinition] = Map.empty,
86+
87+
// Agent name for the main thread (equivalent to --agent CLI flag)
88+
// The agent must be defined in the `agents` option or in settings
89+
agent: Option[String] = None
8690
):
8791
/** Convert to raw JavaScript object for SDK */
8892
def toRaw: js.Object =
@@ -155,6 +159,8 @@ final case class AgentOptions(
155159
if agents.nonEmpty then
156160
obj.agents = js.Dictionary(agents.view.mapValues(_.toRaw).toSeq*)
157161

162+
agent.foreach(a => obj.agent = a)
163+
158164
// Note: Hooks are converted separately in ClaudeAgent when calling query()
159165
// because they require a ZIO Runtime to bridge Scala→JS callbacks
160166

@@ -542,6 +548,26 @@ object AgentOptions:
542548
model = model
543549
)))
544550

551+
/** Set the main thread agent by name.
552+
*
553+
* The agent's system prompt, tool restrictions, and model will be applied to the main conversation.
554+
* The agent must be defined either in the `agents` option or in settings.
555+
*
556+
* This is equivalent to the `--agent` CLI flag.
557+
*
558+
* Example:
559+
* {{{
560+
* AgentOptions.default
561+
* .withAgentDefinition("reviewer", AgentDefinition(
562+
* description = "Reviews code for best practices",
563+
* prompt = "You are a code reviewer..."
564+
* ))
565+
* .withMainAgent("reviewer")
566+
* }}}
567+
*/
568+
def withMainAgent(agentName: String): AgentOptions =
569+
opts.copy(agent = Some(agentName))
570+
545571
/** Configure structured output with type-safe schema derivation.
546572
*
547573
* Requires a StructuredOutput type class instance for the output type, which provides both JSON Schema generation

src/com/tjclp/scalagent/config/PermissionMode.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ enum PermissionMode:
2323
/** Don't ask for permissions, deny if not pre-approved */
2424
case DontAsk
2525

26+
/** Delegate mode - used for subagent delegation */
27+
case Delegate
28+
2629
/** Custom/unknown permission mode for forward compatibility */
2730
case Custom(value: String)
2831

@@ -33,6 +36,7 @@ enum PermissionMode:
3336
case BypassPermissions => "bypassPermissions"
3437
case Plan => "plan"
3538
case DontAsk => "dontAsk"
39+
case Delegate => "delegate"
3640
case Custom(v) => v
3741

3842
object PermissionMode:
@@ -46,4 +50,5 @@ object PermissionMode:
4650
case "bypassPermissions" => BypassPermissions
4751
case "plan" => Plan
4852
case "dontAsk" => DontAsk
53+
case "delegate" => Delegate
4954
case other => Custom(other)

src/com/tjclp/scalagent/messages/AgentMessage.scala

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,24 @@ enum AgentMessage:
7777
sessionId: SessionId
7878
)
7979

80+
/** Task/subagent completion notification */
81+
case TaskNotification(
82+
taskId: String,
83+
status: TaskStatus,
84+
outputFile: String,
85+
summary: String,
86+
uuid: MessageUuid,
87+
sessionId: SessionId
88+
)
89+
90+
/** Tool use summary (aggregate info) */
91+
case ToolUseSummary(
92+
summary: String,
93+
precedingToolUseIds: List[ToolUseId],
94+
uuid: MessageUuid,
95+
sessionId: SessionId
96+
)
97+
8098
object AgentMessage:
8199
given JsonDecoder[AgentMessage] = DeriveJsonDecoder.gen[AgentMessage]
82100
given JsonEncoder[AgentMessage] = DeriveJsonEncoder.gen[AgentMessage]
@@ -136,6 +154,16 @@ object AgentMessage:
136154
case _: User | _: UserReplay => true
137155
case _ => false
138156

157+
/** Check if this is a task notification message */
158+
def isTaskNotification: Boolean = msg match
159+
case _: TaskNotification => true
160+
case _ => false
161+
162+
/** Check if this is a tool use summary message */
163+
def isToolUseSummary: Boolean = msg match
164+
case _: ToolUseSummary => true
165+
case _ => false
166+
139167
// Extension methods for message lists
140168
extension (messages: List[AgentMessage])
141169
/** Extract all text from all messages */
@@ -165,6 +193,14 @@ object AgentMessage:
165193
case _ => false
166194
}
167195

196+
/** Extract all task notifications from messages */
197+
def taskNotifications: List[AgentMessage.TaskNotification] =
198+
messages.collect { case tn: AgentMessage.TaskNotification => tn }
199+
200+
/** Extract all tool use summaries from messages */
201+
def toolUseSummaries: List[AgentMessage.ToolUseSummary] =
202+
messages.collect { case tus: AgentMessage.ToolUseSummary => tus }
203+
168204
/** API assistant message structure */
169205
final case class ApiAssistantMessage(
170206
id: ApiMessageId,
@@ -232,3 +268,26 @@ enum StreamDelta:
232268
object StreamDelta:
233269
given JsonDecoder[StreamDelta] = DeriveJsonDecoder.gen[StreamDelta]
234270
given JsonEncoder[StreamDelta] = DeriveJsonEncoder.gen[StreamDelta]
271+
272+
/** Task status for task notifications */
273+
enum TaskStatus:
274+
case Completed
275+
case Failed
276+
case Stopped
277+
case Custom(value: String)
278+
279+
def toRaw: String = this match
280+
case Completed => "completed"
281+
case Failed => "failed"
282+
case Stopped => "stopped"
283+
case Custom(v) => v
284+
285+
object TaskStatus:
286+
given JsonEncoder[TaskStatus] = JsonEncoder[String].contramap(_.toRaw)
287+
given JsonDecoder[TaskStatus] = JsonDecoder[String].map(fromString)
288+
289+
def fromString(s: String): TaskStatus = s match
290+
case "completed" => Completed
291+
case "failed" => Failed
292+
case "stopped" => Stopped
293+
case other => Custom(other)

src/com/tjclp/scalagent/messages/SystemEvent.scala

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,31 @@ enum SystemEvent:
3434

3535
/** Hook response event */
3636
case HookResponse(
37+
hookId: String,
3738
hookName: String,
3839
hookEvent: String,
3940
stdout: String,
4041
stderr: String,
41-
exitCode: Option[Int]
42+
output: String,
43+
exitCode: Option[Int],
44+
outcome: HookOutcome
45+
)
46+
47+
/** Hook execution started */
48+
case HookStarted(
49+
hookId: String,
50+
hookName: String,
51+
hookEvent: String
52+
)
53+
54+
/** Hook execution progress */
55+
case HookProgress(
56+
hookId: String,
57+
hookName: String,
58+
hookEvent: String,
59+
stdout: String,
60+
stderr: String,
61+
output: String
4262
)
4363

4464
object SystemEvent:
@@ -112,38 +132,67 @@ object ApiKeySource:
112132
final case class McpServerStatus(
113133
name: String,
114134
status: McpConnectionStatus,
115-
serverInfo: Option[McpServerInfo]
135+
serverInfo: Option[McpServerInfo],
136+
error: Option[String] = None,
137+
scope: Option[String] = None,
138+
tools: Option[List[McpToolInfo]] = None
116139
)
117140

118141
object McpServerStatus:
119142
given JsonDecoder[McpServerStatus] = DeriveJsonDecoder.gen[McpServerStatus]
120143
given JsonEncoder[McpServerStatus] = DeriveJsonEncoder.gen[McpServerStatus]
121144

145+
/** MCP tool information from server status */
146+
final case class McpToolInfo(
147+
name: String,
148+
description: Option[String] = None,
149+
annotations: Option[McpToolAnnotations] = None
150+
)
151+
152+
object McpToolInfo:
153+
given JsonDecoder[McpToolInfo] = DeriveJsonDecoder.gen[McpToolInfo]
154+
given JsonEncoder[McpToolInfo] = DeriveJsonEncoder.gen[McpToolInfo]
155+
156+
/** MCP tool annotations */
157+
final case class McpToolAnnotations(
158+
readOnly: Option[Boolean] = None,
159+
destructive: Option[Boolean] = None,
160+
openWorld: Option[Boolean] = None
161+
)
162+
163+
object McpToolAnnotations:
164+
given JsonDecoder[McpToolAnnotations] = DeriveJsonDecoder.gen[McpToolAnnotations]
165+
given JsonEncoder[McpToolAnnotations] = DeriveJsonEncoder.gen[McpToolAnnotations]
166+
122167
/** MCP connection status */
123168
enum McpConnectionStatus:
124169
case Connected
125170
case Failed
126171
case NeedsAuth
127172
case Pending
173+
case Disabled
128174
case Custom(value: String)
129175

130176
def toRaw: String = this match
131177
case Connected => "connected"
132178
case Failed => "failed"
133-
case NeedsAuth => "needs_auth"
179+
case NeedsAuth => "needs-auth"
134180
case Pending => "pending"
181+
case Disabled => "disabled"
135182
case Custom(v) => v
136183

137184
object McpConnectionStatus:
138185
given JsonEncoder[McpConnectionStatus] = JsonEncoder[String].contramap(_.toRaw)
139186
given JsonDecoder[McpConnectionStatus] = JsonDecoder[String].map(fromString)
140187

141188
def fromString(s: String): McpConnectionStatus = s match
142-
case "connected" => Connected
143-
case "failed" => Failed
144-
case "needs_auth" => NeedsAuth
145-
case "pending" => Pending
146-
case other => Custom(other)
189+
case "connected" => Connected
190+
case "failed" => Failed
191+
case "needs-auth" => NeedsAuth
192+
case "needs_auth" => NeedsAuth // Legacy format support
193+
case "pending" => Pending
194+
case "disabled" => Disabled
195+
case other => Custom(other)
147196

148197
/** MCP server information */
149198
final case class McpServerInfo(
@@ -164,3 +213,26 @@ final case class PluginInfo(
164213
object PluginInfo:
165214
given JsonDecoder[PluginInfo] = DeriveJsonDecoder.gen[PluginInfo]
166215
given JsonEncoder[PluginInfo] = DeriveJsonEncoder.gen[PluginInfo]
216+
217+
/** Hook outcome values */
218+
enum HookOutcome:
219+
case Success
220+
case Error
221+
case Cancelled
222+
case Custom(value: String)
223+
224+
def toRaw: String = this match
225+
case Success => "success"
226+
case Error => "error"
227+
case Cancelled => "cancelled"
228+
case Custom(v) => v
229+
230+
object HookOutcome:
231+
given JsonEncoder[HookOutcome] = JsonEncoder[String].contramap(_.toRaw)
232+
given JsonDecoder[HookOutcome] = JsonDecoder[String].map(fromString)
233+
234+
def fromString(s: String): HookOutcome = s match
235+
case "success" => Success
236+
case "error" => Error
237+
case "cancelled" => Cancelled
238+
case other => Custom(other)

0 commit comments

Comments
 (0)