Skip to content
Draft
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: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ ECA Agent Guide (AGENTS.md)
- Use `clojure.test` + `nubank/matcher-combinators`; keep tests deterministic.
- Put shared test helpers under `test/eca/test_helper.clj`.
- Use java class typing to avoid GraalVM reflection issues
- Avoid adding too many comments, only add essential or when you think is really important to mention something.
- ECA's protocol specification of client <-> server lives in docs/protocol.md
35 changes: 34 additions & 1 deletion docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,12 @@ interface ChatContentReceivedParams {
* The chat session identifier this content belongs to
*/
chatId: string;

/**
* If this chat is a subagent, the parent chat id.
* Useful for clients to associate subagent messages with the parent conversation.
*/
parentChatId?: string;

/**
* The content received from the LLM
Expand Down Expand Up @@ -1000,7 +1006,7 @@ interface ChatToolCallRejectedContent {

type ToolCallOrigin = 'mcp' | 'native';

type ToolCallDetails = FileChangeDetails | JsonOutputsDetails;
type ToolCallDetails = FileChangeDetails | JsonOutputsDetails | SubagentDetails;

interface FileChangeDetails {
type: 'fileChange';
Expand Down Expand Up @@ -1035,6 +1041,33 @@ interface JsonOutputsDetails {
jsons: string[];
}

interface SubagentDetails {
type: 'subagent';

/**
* The chatId of this running subagent, useful to link other chat/ContentReceived
* messages to this tool call.
* Available from toolCallRun afterwards
*/
subagentChatId?: string;

/**
* The model this subagent is using.
*/
model: string;

/**
* The max number of steps this subagent is limited.
* When not set, the subagent runs with no step limit (infinite interaction).
*/
maxSteps?: number;

/**
* The current step.
*/
step: number;
}

/**
* Extra information about a chat
*/
Expand Down
12 changes: 12 additions & 0 deletions resources/prompts/tools/spawn_agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Spawn a specialized agent to perform a focused task in isolated context.

The agent runs independently with its own conversation history and returns a summary of its findings/actions. Use this for:
- Codebase exploration without polluting your context
- Focused research on specific areas
- Delegating specialized tasks (review, analysis, etc.)

The spawned agent:
- Has access only to its configured tools
- Cannot spawn other agents (no nesting)
- Returns a summary when complete
- Does not share your conversation history
17 changes: 14 additions & 3 deletions src/eca/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,11 @@
"gemini-3-flash-preview" {}}}
"ollama" {:url "${env:OLLAMA_API_URL:http://localhost:11434}"}}
:defaultBehavior "agent"
:behavior {"agent" {:prompts {:chat "${classpath:prompts/agent_behavior.md}"}
:behavior {"agent" {:mode :primary
:prompts {:chat "${classpath:prompts/agent_behavior.md}"}
:disabledTools ["preview_file_change"]}
"plan" {:prompts {:chat "${classpath:prompts/plan_behavior.md}"}
"plan" {:mode :primary
:prompts {:chat "${classpath:prompts/plan_behavior.md}"}
:disabledTools ["edit_file" "write_file" "move_file"]
:toolCall {:approval {:allow {"eca__shell_command"
{:argsMatchers {"command" ["pwd"]}}
Expand Down Expand Up @@ -127,7 +129,8 @@
"eca__directory_tree" {}
"eca__grep" {}
"eca__editor_diagnostics" {}
"eca__skill" {}}
"eca__skill" {}
"eca__spawn_agent" {}}
:ask {}
:deny {}}
:readFile {:maxLines 2000}
Expand Down Expand Up @@ -208,6 +211,14 @@

(def ^:private fallback-behavior "agent")

(defn primary-behavior-names
"Returns the names of behaviors that are not subagents (mode is nil or \"primary\")."
[config]
(->> (:behavior config)
(remove (fn [[_ v]] (= "subagent" (:mode v))))
(map key)
distinct))

(defn validate-behavior-name
"Validates if a behavior exists in config. Returns the behavior if valid,
or the fallback behavior if not."
Expand Down
463 changes: 251 additions & 212 deletions src/eca/features/chat.clj

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions src/eca/features/prompt.clj
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,16 @@
(get-in config [:prompts key])))

(defn ^:private eca-chat-prompt [behavior config]
(let [config-prompt (get-config-prompt :chat behavior config)
behavior-config (get-in config [:behavior behavior])
(let [behavior-config (get-in config [:behavior behavior])
subagent-prompt (and (= "subagent" (:mode behavior-config))
(:systemPrompt behavior-config))
config-prompt (get-config-prompt :chat behavior config)
legacy-config-prompt (:systemPrompt behavior-config)
legacy-config-prompt-file (:systemPromptFile behavior-config)]
(cond
subagent-prompt
subagent-prompt

legacy-config-prompt
legacy-config-prompt

Expand Down
31 changes: 25 additions & 6 deletions src/eca/features/tools.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
(:require
[clojure.string :as string]
[clojure.walk :as walk]
[eca.features.tools.agent :as f.tools.agent]
[eca.features.tools.chat :as f.tools.chat]
[eca.features.tools.custom :as f.tools.custom]
[eca.features.tools.editor :as f.tools.editor]
Expand Down Expand Up @@ -147,17 +148,27 @@
f.tools.editor/definitions
f.tools.chat/definitions
f.tools.skill/definitions
(f.tools.agent/definitions config)
(f.tools.custom/definitions config))))

(defn native-tools [db config]
(mapv #(assoc % :server {:name "eca"}) (vals (native-definitions db config))))

(defn ^:private filter-subagent-tools
"Filter tools for subagent execution.
Excludes spawn_agent to prevent nesting."
[tools]
(filterv #(not= "spawn_agent" (:name %)) tools))

(defn all-tools
"Returns all available tools, including both native ECA tools
(like filesystem and shell tools) and tools provided by MCP servers.
Removes denied tools."
Removes denied tools.
When chat is a subagent (has :agent-def), filters tools based on agent definition."
[chat-id behavior db config]
(let [disabled-tools (get-disabled-tools config behavior)
;; presence of :agent-def indicates this is a subagent
agent-def (get-in db [:chats chat-id :agent-def])
all-tools (->> (concat
(mapv #(assoc % :origin :native) (native-tools db config))
(mapv #(assoc % :origin :mcp) (f.mcp/all-tools db)))
Expand All @@ -175,7 +186,11 @@
{:behavior behavior
:db db
:chat-id chat-id
:config config})))))]
:config config})))))
;; Apply subagent tool filtering if applicable
all-tools (if agent-def
(filter-subagent-tools all-tools)
all-tools)]
(remove (fn [tool]
(= :deny (approval all-tools tool {} db config behavior)))
all-tools)))
Expand Down Expand Up @@ -207,6 +222,7 @@
:config config
:messenger messenger
:behavior behavior
:metrics metrics
:chat-id chat-id
:tool-call-id tool-call-id
:call-state-fn call-state-fn
Expand Down Expand Up @@ -280,19 +296,22 @@

(defn tool-call-details-before-invocation
"Return the tool call details before invoking the tool."
[name arguments server db ask-approval?]
[name arguments server db config chat-id ask-approval? tool-call-id]
(try
(tools.util/tool-call-details-before-invocation name arguments server {:db db
:ask-approval? ask-approval?})
:config config
:chat-id chat-id
:ask-approval? ask-approval?
:tool-call-id tool-call-id})
(catch Exception e
;; Avoid failling tool call because of error on getting details.
(logger/error logger-tag (format "Error getting details for %s with args %s: %s" name arguments e))
nil)))

(defn tool-call-details-after-invocation
"Return the tool call details after invoking the tool."
[name arguments details result]
(tools.util/tool-call-details-after-invocation name arguments details result))
[name arguments details result ctx]
(tools.util/tool-call-details-after-invocation name arguments details result ctx))

(defn tool-call-destroy-resource!
"Destroy the resource in the tool call named `name`."
Expand Down
Loading
Loading