Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ object agent extends ScalaJSModule with PublishModule {
// Publishing configuration - read from PUBLISH_VERSION env var (set by CI)
// Task.Input ensures env var is re-evaluated each run (not cached)
override def publishVersion = Task.Input {
Task.env.get("PUBLISH_VERSION").getOrElse("0.2.1-SNAPSHOT")
Task.env.get("PUBLISH_VERSION").getOrElse("0.2.2-SNAPSHOT")
}

// Skip scaladoc generation - Scala.js facades cause doc errors
Expand Down
8 changes: 4 additions & 4 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "scalagent",
"version": "0.2.1",
"version": "0.2.2-SNAPSHOT",
"private": true,
"type": "module",
"description": "Scalagent - Type-safe Scala.js SDK for Claude Code CLI",
Expand All @@ -10,8 +10,8 @@
},
"dependencies": {
"@a2a-js/sdk": "^0.3.7",
"@anthropic-ai/claude-agent-sdk": "^0.2.1",
"zod": "3.24.1"
"@anthropic-ai/claude-agent-sdk": "^0.2.22",
"zod": "^4.0.0"
},
"devDependencies": {
"@types/bun": "^1.3.5"
Expand Down
28 changes: 27 additions & 1 deletion src/com/tjclp/scalagent/config/AgentOptions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ final case class AgentOptions(
plugins: List[PluginConfig] = List.empty,

// Subagents
agents: Map[String, AgentDefinition] = Map.empty
agents: Map[String, AgentDefinition] = Map.empty,

// Agent name for the main thread (equivalent to --agent CLI flag)
// The agent must be defined in the `agents` option or in settings
agent: Option[String] = None
):
/** Convert to raw JavaScript object for SDK */
def toRaw: js.Object =
Expand Down Expand Up @@ -155,6 +159,8 @@ final case class AgentOptions(
if agents.nonEmpty then
obj.agents = js.Dictionary(agents.view.mapValues(_.toRaw).toSeq*)

agent.foreach(a => obj.agent = a)

// Note: Hooks are converted separately in ClaudeAgent when calling query()
// because they require a ZIO Runtime to bridge Scala→JS callbacks

Expand Down Expand Up @@ -542,6 +548,26 @@ object AgentOptions:
model = model
)))

/** Set the main thread agent by name.
*
* The agent's system prompt, tool restrictions, and model will be applied to the main conversation.
* The agent must be defined either in the `agents` option or in settings.
*
* This is equivalent to the `--agent` CLI flag.
*
* Example:
* {{{
* AgentOptions.default
* .withAgentDefinition("reviewer", AgentDefinition(
* description = "Reviews code for best practices",
* prompt = "You are a code reviewer..."
* ))
* .withMainAgent("reviewer")
* }}}
*/
def withMainAgent(agentName: String): AgentOptions =
opts.copy(agent = Some(agentName))

/** Configure structured output with type-safe schema derivation.
*
* Requires a StructuredOutput type class instance for the output type, which provides both JSON Schema generation
Expand Down
5 changes: 5 additions & 0 deletions src/com/tjclp/scalagent/config/PermissionMode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ enum PermissionMode:
/** Don't ask for permissions, deny if not pre-approved */
case DontAsk

/** Delegate mode - used for subagent delegation */
case Delegate

/** Custom/unknown permission mode for forward compatibility */
case Custom(value: String)

Expand All @@ -33,6 +36,7 @@ enum PermissionMode:
case BypassPermissions => "bypassPermissions"
case Plan => "plan"
case DontAsk => "dontAsk"
case Delegate => "delegate"
case Custom(v) => v

object PermissionMode:
Expand All @@ -46,4 +50,5 @@ object PermissionMode:
case "bypassPermissions" => BypassPermissions
case "plan" => Plan
case "dontAsk" => DontAsk
case "delegate" => Delegate
case other => Custom(other)
59 changes: 59 additions & 0 deletions src/com/tjclp/scalagent/messages/AgentMessage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,24 @@ enum AgentMessage:
sessionId: SessionId
)

/** Task/subagent completion notification */
case TaskNotification(
taskId: String,
status: TaskStatus,
outputFile: String,
summary: String,
uuid: MessageUuid,
sessionId: SessionId
)

/** Tool use summary (aggregate info) */
case ToolUseSummary(
summary: String,
precedingToolUseIds: List[ToolUseId],
uuid: MessageUuid,
sessionId: SessionId
)

object AgentMessage:
given JsonDecoder[AgentMessage] = DeriveJsonDecoder.gen[AgentMessage]
given JsonEncoder[AgentMessage] = DeriveJsonEncoder.gen[AgentMessage]
Expand Down Expand Up @@ -136,6 +154,16 @@ object AgentMessage:
case _: User | _: UserReplay => true
case _ => false

/** Check if this is a task notification message */
def isTaskNotification: Boolean = msg match
case _: TaskNotification => true
case _ => false

/** Check if this is a tool use summary message */
def isToolUseSummary: Boolean = msg match
case _: ToolUseSummary => true
case _ => false

// Extension methods for message lists
extension (messages: List[AgentMessage])
/** Extract all text from all messages */
Expand Down Expand Up @@ -165,6 +193,14 @@ object AgentMessage:
case _ => false
}

/** Extract all task notifications from messages */
def taskNotifications: List[AgentMessage.TaskNotification] =
messages.collect { case tn: AgentMessage.TaskNotification => tn }

/** Extract all tool use summaries from messages */
def toolUseSummaries: List[AgentMessage.ToolUseSummary] =
messages.collect { case tus: AgentMessage.ToolUseSummary => tus }

/** API assistant message structure */
final case class ApiAssistantMessage(
id: ApiMessageId,
Expand Down Expand Up @@ -232,3 +268,26 @@ enum StreamDelta:
object StreamDelta:
given JsonDecoder[StreamDelta] = DeriveJsonDecoder.gen[StreamDelta]
given JsonEncoder[StreamDelta] = DeriveJsonEncoder.gen[StreamDelta]

/** Task status for task notifications */
enum TaskStatus:
case Completed
case Failed
case Stopped
case Custom(value: String)

def toRaw: String = this match
case Completed => "completed"
case Failed => "failed"
case Stopped => "stopped"
case Custom(v) => v

object TaskStatus:
given JsonEncoder[TaskStatus] = JsonEncoder[String].contramap(_.toRaw)
given JsonDecoder[TaskStatus] = JsonDecoder[String].map(fromString)

def fromString(s: String): TaskStatus = s match
case "completed" => Completed
case "failed" => Failed
case "stopped" => Stopped
case other => Custom(other)
88 changes: 80 additions & 8 deletions src/com/tjclp/scalagent/messages/SystemEvent.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,31 @@ enum SystemEvent:

/** Hook response event */
case HookResponse(
hookId: String,
hookName: String,
hookEvent: String,
stdout: String,
stderr: String,
exitCode: Option[Int]
output: String,
exitCode: Option[Int],
outcome: HookOutcome
)

/** Hook execution started */
case HookStarted(
hookId: String,
hookName: String,
hookEvent: String
)

/** Hook execution progress */
case HookProgress(
hookId: String,
hookName: String,
hookEvent: String,
stdout: String,
stderr: String,
output: String
)

object SystemEvent:
Expand Down Expand Up @@ -112,38 +132,67 @@ object ApiKeySource:
final case class McpServerStatus(
name: String,
status: McpConnectionStatus,
serverInfo: Option[McpServerInfo]
serverInfo: Option[McpServerInfo],
error: Option[String] = None,
scope: Option[String] = None,
tools: Option[List[McpToolInfo]] = None
)

object McpServerStatus:
given JsonDecoder[McpServerStatus] = DeriveJsonDecoder.gen[McpServerStatus]
given JsonEncoder[McpServerStatus] = DeriveJsonEncoder.gen[McpServerStatus]

/** MCP tool information from server status */
final case class McpToolInfo(
name: String,
description: Option[String] = None,
annotations: Option[McpToolAnnotations] = None
)

object McpToolInfo:
given JsonDecoder[McpToolInfo] = DeriveJsonDecoder.gen[McpToolInfo]
given JsonEncoder[McpToolInfo] = DeriveJsonEncoder.gen[McpToolInfo]

/** MCP tool annotations */
final case class McpToolAnnotations(
readOnly: Option[Boolean] = None,
destructive: Option[Boolean] = None,
openWorld: Option[Boolean] = None
)

object McpToolAnnotations:
given JsonDecoder[McpToolAnnotations] = DeriveJsonDecoder.gen[McpToolAnnotations]
given JsonEncoder[McpToolAnnotations] = DeriveJsonEncoder.gen[McpToolAnnotations]

/** MCP connection status */
enum McpConnectionStatus:
case Connected
case Failed
case NeedsAuth
case Pending
case Disabled
case Custom(value: String)

def toRaw: String = this match
case Connected => "connected"
case Failed => "failed"
case NeedsAuth => "needs_auth"
case NeedsAuth => "needs-auth"
case Pending => "pending"
case Disabled => "disabled"
case Custom(v) => v

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

def fromString(s: String): McpConnectionStatus = s match
case "connected" => Connected
case "failed" => Failed
case "needs_auth" => NeedsAuth
case "pending" => Pending
case other => Custom(other)
case "connected" => Connected
case "failed" => Failed
case "needs-auth" => NeedsAuth
case "needs_auth" => NeedsAuth // Legacy format support
case "pending" => Pending
case "disabled" => Disabled
case other => Custom(other)

/** MCP server information */
final case class McpServerInfo(
Expand All @@ -164,3 +213,26 @@ final case class PluginInfo(
object PluginInfo:
given JsonDecoder[PluginInfo] = DeriveJsonDecoder.gen[PluginInfo]
given JsonEncoder[PluginInfo] = DeriveJsonEncoder.gen[PluginInfo]

/** Hook outcome values */
enum HookOutcome:
case Success
case Error
case Cancelled
case Custom(value: String)

def toRaw: String = this match
case Success => "success"
case Error => "error"
case Cancelled => "cancelled"
case Custom(v) => v

object HookOutcome:
given JsonEncoder[HookOutcome] = JsonEncoder[String].contramap(_.toRaw)
given JsonDecoder[HookOutcome] = JsonDecoder[String].map(fromString)

def fromString(s: String): HookOutcome = s match
case "success" => Success
case "error" => Error
case "cancelled" => Cancelled
case other => Custom(other)
Loading