跪拜 Guibai
← Back to the summary

One Markdown File Is One Employee: The Orchestrator-Workers Pattern in AgentScope-Java

P3: One Markdown File Is One Employee -- AgentScope-Java Multi-Agent Orchestration in Practice (Core)

Third post in the series. P1 got a single Agent running, P2 gave it hands (tool calls), and this one solves the real problem: when one Agent cannot do the job, how do you get a team of Agents to divide the work?


What you will get


Why a single Agent is not enough

In P2, the Agent could already call tools, but you quickly hit three ceilings:

  1. Context pollution. Let one Agent research materials, write a first draft, and critique itself; its context fills with intermediate processes and quality visibly drops in the second half -- it keeps being disturbed by its own earlier draft.
  2. You cannot have both temperatures. Drafting needs divergence (a higher temperature gives more creativity), and review needs rigor (a lower temperature gives stability). One Agent has only one temperature; you cannot have it both ways.
  3. Responsibilities are not reusable. Review logic is hard-coded into the system prompt; the next time you build a “code review assistant”, you have to write it again.

The classic solution is the orchestrator-workers pattern. One editor-in-chief Agent understands the requirement, breaks down tasks, and assigns work; each worker is an independent mini-Agent with its own system prompt, its own temperature, and its own context, and only reports back the result when done. This is the same idea as “API gateway + microservices” -- except the “services” here can reason.

AgentScope-Java makes this pattern a ready-to-use capability, and declaring a worker needs only one markdown file.

Core mechanism: the agents/ directory is the team roster

Lead with conclusion: putting a markdown file in the agents/ directory under the workspace is equivalent to hiring a new employee.

The file has two parts: YAML front matter defines the “HR information”, and the body is the system prompt. Taking the outliner (structure planner) of our content team as an example, this is the original text of .agentscope/workspace/agents/outliner.md:

---
name: outliner
description: Article structure planner. Expands a writing topic into a structured outline, including opening hook, core arguments, case suggestions, and closing. Assign it when an outline is needed.
temperature: 0.7
---
You are a senior WeChat official-account article structure planner, good at breaking a topic into a structure with communication power.

The user gives you a writing topic, and you need to output an executable outline:
1. **Opening hook**: one sentence that makes people stop and read (pain-point question / counter-intuitive conclusion / data impact, pick one)
2. **Core arguments**: 3, each with one sentence of explanation
3. **Case and material suggestions**: what code, screenshots, or data to pair with each argument
4. **Ending**: closing method + next-issue teaser hook

Requirement: output only the outline itself; do not write the body, do not add extra explanation.

The details of each front matter field:

Field Purpose Notes
name Employee ID, the “recipient” when the editor assigns work Required, unique within the directory
description The job description -- the editor relies on it to decide “who should get this task” Required, and should state “when to assign it”
temperature How divergent this role is Set by responsibility, see below
tools List of tool names available to this role Referenced by name to tools registered in the parent Toolkit; if not written, no tools

description is the most underestimated field. The editor-in-chief does not know these workers personally; it selects people entirely by the description. If the description is vague (for example just “help me work”), the editor assigns work arbitrarily. The correct approach is to write description like a JD: responsibility + trigger condition. In the outliner description above, “Assign it when an outline is needed” is a routing rule for the editor.

Next look at the writer declaration, and note its tools:

---
name: writer
description: Writer. Writes a first draft of a WeChat official-account article from an outline or key points; professional but readable, with short paragraphs and subheadings. Assign it when a first draft is needed.
temperature: 0.8
tools:
  - save_draft
---
You are a technical WeChat official-account writer, professional but not dry: short paragraphs, many subheadings, and code or examples at key places.
......
If the user asks to “save”, call the save_draft tool to store the draft in the drafts folder, using the article title as the title.

save_draft is the custom tool we wrote with @Tool in P2 (stored in the drafts folder). How parent and child tools connect: register it once in the editor's Toolkit, and child Agents reference it by name in their declarations. That is, the tool is written once and shared by the whole team.

The three roles have different temperatures; this is deliberate design:

Role temperature Reason
outliner 0.7 Producing an outline needs some creativity, but not too flighty
writer 0.8 Writing a draft needs the most divergence
reviewer 0.3 Review must be stable and reproducible; with a high temperature it “overlooks problems”

This is the solution to the “one Agent cannot have both temperatures” problem in Section 2 -- split different temperatures across different roles.

How the editor-in-chief is built: one builder handles it

Workers are declared with markdown; the editor-in-chief is built in Java. The complete code comes from MultiAgentDemo.java in the repository:

// 1. Parent Toolkit: register custom tools; child Agents reference them by name through the tools list in their declarations
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new DraftTools(Paths.get(".agentscope/workspace/drafts")));

// 2. Editor-in-chief: the agents/ directory under workspace is the “team roster”
HarnessAgent editorInChief = HarnessAgent.builder()
        .name("editor-in-chief")
        .sysPrompt("""
                You are the editor-in-chief of a WeChat official-account content team, with three capable subordinates:
                - outliner: structure planner, responsible for producing outlines
                - writer: writer, responsible for writing drafts (can save finished work to the drafts folder)
                - reviewer: reviewer, responsible for checking for errors

                Workflow: after receiving a request, first send outliner to produce an outline; once the outline is satisfactory, send writer to write the draft; after the draft is complete, send reviewer for quality check. Every step must wait for the previous step's result before the next round is sent. Finally, consolidate the conclusions of each stage into a briefing and reply to the user.""")
        .model(ModelFactory.build())
        .workspace(Paths.get(".agentscope/workspace"))
        .toolkit(toolkit)
        .compaction(CompactionConfig.builder()
                .triggerMessages(30)
                .keepMessages(10)
                .build())
        .build();

Four key points:

  1. If there is an agents/ subdirectory in the directory pointed to by .workspace(), the framework scans and loads it automatically at runtime; there is no need to manually register any sub-Agent. This is what “declarative” means: adding a new employee means adding an md file, with zero Java changes.
  2. Write the workflow in the editor's sysPrompt, not how each stage is done -- the professional knowledge of each stage lives in each worker's prompt. The editor only manages “who goes first, who goes second, wait for the previous step's result”.
  3. .compaction() configures context compression: when more than 30 messages accumulate, compress and keep only the latest 10. An entire orchestration run has many events; this setting prevents long sessions from blowing up the context (P4 will expand on it).
  4. Not a single line of code touches orchestration tools like agent_spawn -- the framework gives HarnessAgent a complete set of orchestration tools by default (agent_spawn / agent_send / agent_list, plus file read/write, memory, and session series, 21 built-in tools in total). The editor can assign work out of the box.

Running it: a complete three-baton relay

Send a requirement to the editor:

UserMessage message = new UserMessage(
        "Topic: “How Java developers can build a multi-agent system with zero Python foundation.”"
                + "Please go through the team workflow: produce an outline -> write a first draft -> review, and finally give me an execution briefing.");

Use the streamEvents() method from P1 to listen to the event stream and print the “assign work” and “return result” process:

editorInChief.streamEvents(message, ctx)
        .doOnNext(event -> {
            switch (event.getType()) {
                case TEXT_BLOCK_DELTA ->
                        System.out.print(((TextBlockDeltaEvent) event).getDelta());
                case TOOL_CALL_START ->
                        System.out.println("\n[Delegate] " + ((ToolCallStartEvent) event).getToolCallName()
                                + " <- editor is assigning work");
                case TOOL_RESULT_END ->
                        System.out.println("\n[Sub-agent result] " + ((ToolResultEndEvent) event).getToolCallName());
                default -> { }
            }
        })
        .blockLast();

Run command:

mvn -q exec:java -Dexec.mainClass="com.mk.agentscope.agent.MultiAgentDemo"

Real console output from a run (abridged):

=== Multi-agent orchestration demo ===
Sub-agent declaration directory: .agentscope/workspace/agents/

[Delegate] agent_spawn <- editor is assigning work
[Sub-agent result] agent_spawn:
1. Opening hook: “Your colleague is writing Agents in Python, and you are still writing CRUD?” ...

[Delegate] agent_spawn <- editor is assigning work
[Sub-agent result] agent_spawn:
# Let Agents Grow Hands: ... (first draft body)

OK, I will use save_draft to save the draft.

[Delegate] agent_spawn <- editor is assigning work
[Sub-agent result] agent_spawn:
P0 must be changed: Section 2 “Schema Auto-generation” is loosely worded ...

[Execution briefing] Outline confirmed; draft saved to drafts folder (drafts/p3-xxx.md);
Review raised 1 P0 issue and 2 P1 suggestions, with a fix list attached.

Notice three details:

What happens behind the scenes

Break down this run; the complete sequence of a three-baton relay:

User requirement
  |
  v
Editor-in-chief (editor-in-chief, ReAct loop)
  | 1. Reasoning: this job should go to outliner -> call agent_spawn("outliner", task)
  |      '- outliner (temp 0.7, isolated context) -> returns outline text
  | 2. Reasoning: outline OK -> call agent_spawn("writer", outline)
  |      '- writer (temp 0.8, isolated context) -> returns draft + calls save_draft to persist
  | 3. Reasoning: draft OK -> call agent_spawn("reviewer", draft)
  |      '- reviewer (temp 0.3, isolated context) -> returns graded issue list
  | 4. Reasoning: information is complete -> integrate execution briefing
  v
Final briefing returned to user

Several mechanisms worth mentioning:

Four orchestration design tips

  1. Write worker description like a JD: responsibility + trigger condition. It is the only basis the editor uses to assign work.
  2. Push domain expertise down to workers and workflow up to the editor. Do not write them on the wrong side: if the editor's prompt contains “the outline should include a hook”, that is overreach.
  3. Assign temperature by responsibility: creative roles 0.7 to 0.9, quality-check roles 0.1 to 0.3. This is one of the most practical benefits of multi-Agent over single-Agent.
  4. Control the size of returns: do not have workers dump raw results to the editor; let each worker converge first (reviewer outputs a graded list instead of full-text annotations), or truncate the returned preview as in the Demo. Long returns quickly eat the editor's context.

Pitfall record

Pitfall 1: the static declaration directory is agents/, not subagents/. When setting up, we first created subagents/ under the workspace by intuition, and it simply did not work. Only after reading the framework source constant (WorkspaceConstants.AGENTS_DIR = "agents") did we find out: the static declaration directory is agents/, while subagents/ is another mechanism for dynamically generating sub-Agents at runtime. Many early tutorials found online write the latter; be careful to distinguish them.

Pitfall 2: missing description in front matter does not report an error, but the editor assigns work arbitrarily. The declaration parser is quite tolerant of missing fields; a file without description still loads. But the editor routes entirely by description -- when it is missing, “work that should go to reviewer may be assigned to writer”. Use AgentSpecLoader to write unit tests as a fallback.

Pitfall 3: sub-Agents do not automatically inherit all parent tools. We initially thought tools registered in the Toolkit were available to the whole team by default; in fact they must be referenced by name in the sub-Agent's tools list. When the writer initially had no tools configured and we asked it to “save a draft”, it could only output text and stare in vain. Minimal permissions are by design, but it is easy to step on this the first time.

Next post preview

With the team running, the new problem is persistence and recovery: can sessions resume from a breakpoint? What if the context is too long? How should memory be stratified? In P4, we replace JsonFileAgentStateStore with Redis and discuss AgentScope-Java's engineering foundation -- session persistence, context compression, and layered memory.


Repository address: https://github.com/525128633/agentScope Series: P1 Hands-on Test · P2 Tool Call Practice · P3 (this post)