NOTE: This file is translated from tutorial.md by the doc_translator agent.

User Tutorial

The Cangjie Agent DSL is a specialized language for defining and managing Agents. It allows developers to enhance Agent capabilities through structured system prompts, tools, and various collaboration strategies. This manual introduces how to use the various features of Cangjie Agent DSL and provides examples to help users get started quickly.

Cangjie Agent DSL is designed as an eDSL (embedded Domain-Specific Language) within the Cangjie language, implemented through metaprogramming mechanisms in Cangjie, with Cangjie serving as its host language. This means that code written in Agent DSL is ultimately transformed into standard Cangjie code and compiled by the Cangjie compiler.

Agent Definition

Currently, we use the @agent macro to decorate a class type to define an Agent type.

@agent class Foo { }

The @agent macro supports the following attributes. For specific attributes, refer to the corresponding sections.

Attribute Name Value Type Description
description String Functional description of the Agent; if not set by default, it will be automatically summarized by the LLM from the prompt
model String Configures the LLM model service to be used; defaults to gpt-4o
tools Array Configures external tools that can be used
rag Map Configures external knowledge sources
memory Bool Whether to use memory, i.e., save multiple Q&A records of the Agent (currently, memory only supports in-memory non-persistent data); defaults to false
executor String Planning mode; defaults to react
temperature Float The temperature value when the Agent uses the LLM; defaults to 0.5
enableToolFilter Bool Enables tool filtering functionality; the Agent will automatically select appropriate tools based on the input question before execution; defaults to false
skillRoot String or expression Path to a directory of skills the Agent may load on demand. Accepts a string literal or a runtime expression. Defaults to None; with the default, the skill feature is fully disabled and no skill tools are injected. When set, the Agent gets a skillManager property, the skill instructions block is appended to the system prompt, and the runSkill tool is injected automatically. See Skills.
skillBuiltinTools Bool or expression When skillRoot is set, controls whether the general-purpose file/shell helpers (listDirectory, fileRead, globSearch, grepSearch, shellExecute) are injected alongside runSkill. Accepts a literal true/false or a runtime Bool expression; defaults to true. Ignored when skillRoot is None.
dump Bool For debugging purposes, whether to print the transformed AST of the Agent; defaults to false

Writing Prompts

The core of each Agent is its system prompt, which defines the Agent's role information and execution steps, enabling the large language model (LLM) to answer questions more accurately and quickly. In Agent definitions, @prompt is used to write the Agent's system prompt.

  • Within the scope of the @prompt macro, all string literals (including interpolated strings) will be concatenated sequentially to form the complete system prompt.
  • Functions and member variables of the Cangjie language can be accessed within @prompt.
  • Each Agent can have at most one @prompt definition.

Example: String Concatenation The following code concatenates three strings as the complete Agent system prompt, with the third interpolated string calling the function bar.

@agent
class Foo {
    @prompt(
        "# This is a Foo agent"
        "## Description"
        "balabala ${bar()}"
    )
}

Example: Accessing Member Variables

@agent
class Calculator {
    @prompt(
        """
        You are a calculator capable of performing calculations.
        Your name is ${name}-${version}.
        """
        "For example, you can perform addition, 1 + 2 = 3 ..."
    )
    private let name: String
    private let version: Int64
    ...
}

let calculator = Calculator(name: "aha", version: 1)

The @prompt macro supports setting the include attribute, whose value is a string representing a file path. The file content will be used as the Agent's system prompt.

  • When the include attribute is configured, the literals written in @prompt will be ignored and not used as the system prompt.
  • If the file pointed to by include does not exist, an exception will be thrown.

Example: Using External Files to Write System Prompts

@agent
class Foo {
    @prompt[include: "./a.md"]()
}

Using Prompt Patterns

Well-structured prompts can significantly improve LLM performance. By defining a unified prompt syntax, developers can write more structured prompts.

Using Prompt Patterns

The @prompt macro supports setting the pattern attribute, whose value should be a prompt pattern type. When using prompt patterns, prompt elements that satisfy the pattern must be written within the @prompt scope instead of string literals.

Note: The include attribute and pattern attribute cannot be used simultaneously; if both are present, an exception will be thrown.

Example: Using Prompt Patterns

@agent
class Foo {
    @prompt[pattern: APE] (
        action: "Help users plan travel routes",
        purpose: "Allow users to visit as many attractions as possible within the planned time while getting adequate rest",
        expectation: "Generate a reasonable travel route, including time, attractions, commuting information, etc."
    )
}

The following are the currently available prompt patterns.

Prompt Pattern Description

APE

action: Defines the work or activity to be completed purpose: Defines why this action is initiated expectation: States the expected outcome

BROKE

background: Describes the background and provides sufficient information role: Specifies the role of the agent objectives: Defines the task objectives to be achieved keyResult: Defines key measurable results to guide how the agent evaluates the achievement of objectives evolve: Tests results through experimentation and adjustment, optimizing as needed

COAST

context: Sets the background for the conversation objective: Describes the goal action: Explains the required action scenario: Describes the scenario task: Describes the task

TAG

task: Defines the specific task action: Describes what needs to be done goal: Explains the ultimate goal

RISE

role: Specifies the role of the agent input: Describes the information or resources steps: Requests detailed steps expectation: Describes the expected result

TRACE

task: Defines the specific task request: Describes your request action: Explains the action you need context: Provides background or context example: Gives an example to illustrate your point

ERA

expectation: Describes the expected result role: Specifies the role of the agent action: Specifies the action to be taken

CARE

context: Sets the background or context for the discussion action: Describes what you want to do result: Describes the expected result example: Gives an example to illustrate your point

ROSES

role: Specifies the role of the agent objective: States the goal or purpose scenario: Describes the scenario expectation: Define the expected outcome steps: The steps required to achieve the solution

ICIO

instruction: Specific task instructions for the AI context: Provide additional background information to the AI input: Specify the data the model needs to process output: Specify the expected output type or format

CRISPE

capacityAndRole: The role the agent should assume insight: Provide insights, background, and context statement: What you are asking the agent to do personality: The style, personality, or manner in which you want the agent to respond experiment: Request the agent to provide multiple response examples

RACE

role: Specify the agent's role action: Detail the actions to be taken context: Provide detailed information about the relevant context expectation: Describe the expected outcome

SAGE

situation: Describe the background or environment for task execution action: Specify the required operations or steps goal: State the purpose or effect to be achieved upon task completion expectation: Specify the requirements for the output result

Custom Prompt Pattern

The macro @promptPattern applies to class types and can define new prompt patterns. Within the modified class definition, the macro @element is used to modify member variables, defining prompt elements.

  • Each element must be of type String.
  • The description attribute explains the element and does not affect the final prompt.

The prompt pattern type must implement the toString method, which constructs the prompt.

Example: Custom Prompt Pattern

@promptPattern
class APE {
    @element[description: "Define the task"]
    let action: String

    @element[description: "Define the task's purpose"]
    let purpose: String

    @element[description: "Clearly define the expected outcome"]
    let expectation: String

    public func toString(): String {
        return "...${action}...${purpose}...${expectation}..."
    }
}

Agent Interaction Methods

An Agent defined with @agent has a default method func chat(question: ToString): String as the interaction entry point.

@agent class Foo { ... }

let agent = Foo()
let result = agent.chat("What's the weather today?")
println(result)

Additionally, chatGet allows the Agent to return a data type directly instead of just a string. If the Agent fails to generate the required data type, it returns None. The method is defined as follows:

func chatGet<T>(question: String): Option<T> where T <: Jsonable<T>

Here, the Jsonable interface (see section) constrains the data type to be convertible to/from a JSON object. Basic types Int/Int64/String already implement this interface.

The macro @jsonable is used for custom types to automatically implement this interface:

  • @jsonable modifies class types and automatically implements the Jsonable interface through code transformation.
  • Within the modified type, @field can be used to add descriptions for member variables. If not used, member variables will not carry descriptions.

Example: Returning a Data Structure

@jsonable
class MyDate {
    @field["Year of the foundation"]
    let year: Int64
    let month: Int64
}

@agent
class Foo { }

let agent = Foo()
let date = agent.chatGet<MyDate>("Huawei's founding date")
println(date.year)
println(date.month)

Input Template

When defining an Agent type with @agent, an input template can be provided, which templates the input question with placeholder variables. When calling the interaction interface, only the values of the placeholder variables need to be provided. The macro @user defines the input template:

  • Similar to @prompt, @user concatenates all string literals as the complete input template.
  • In the input template, {variable} represents a placeholder variable, where the variable name consists of letters, numbers, and underscores.
  • Like @prompt, @user supports the include attribute, where the attribute value is a file path. If set, the file content serves as the input template.

When calling func chat(variables: Array<(String, ToString)>): String, placeholder variables and their corresponding values must be provided.

  • If the Agent does not provide an input template, calling this method will throw an UnsupportedException.

Example: Using an Input Template

@agent
class Foo {
    @prompt(
        "System: ..."
    )
    @user(
        "The rectangle's length is: {length} cm, and its width is {width} cm"
        "Calculate the rectangle's area"
    )
}
let agent = Foo()
let area = agent.chat(
    ("length", 3),
    ("width", 4),
)

Conversation History

A single chat call with an Agent constitutes a ChatRound, and Conversation maintains multiple dialogue processes to form a continuous conversation history.

When calling an Agent, Conversation can be passed as a parameter of AgentRequest to enable the Agent to respond based on the conversation history. Meanwhile, the execution.chatRound property of AgentResponse updates the conversation history.

Example: Conversation History Example

let agent = FooAgent()
let conversation = Conversation()
let resp = agent.chat(
    AgentRequest("Hello", conversation: conversation)
)
// Update the conversation
conversation.addChatRound(resp.execution.chatRound)
let resp2 = agent.chat(
    AgentRequest("How are you", conversation: conversation)
)

MCP Protocol and Tools

Tools can be understood as code that an Agent can execute during its operation. Currently, Agent tools come from two sources:

  • Tool functions written directly in DSL
  • Tools provided by an MCP server (an MCP server can be considered a collection of tools)

Writing Tool Functions

The macro @tool modifies functions to convert them into tool functions. The following functions can be modified:

  • Global functions
  • Member methods of Agent classes defined with @agent
  • Member methods of Toolset types defined with @toolset

All tool functions have the following attributes:

  • The description attribute describes the tool's functionality (required).
  • The parameters attribute describes the meaning of function parameters, accepting key-value pairs in the format <parameter-name>: <parameter-description> (optional).
  • filterable: Whether the tool can be filtered by the Agent, used with the enableToolFilter attribute of @agent (optional).
  • terminal: Whether to terminate Agent execution. When set to true, the Agent will end immediately after executing this tool, and the function's return value becomes the Agent's execution result (optional).
  • compactable: Whether to (use LLM) summarize and compact the tool's execution result. compaction occurs only when this attribute is true and the result length exceeds Config.resultSummarizeThreshold (optional).

If the tool function is a global function or part of a Toolset, it must be explicitly specified in the tools attribute for the Agent to use it.

Example: Defining and Configuring Global Tools

@tool[description: "...",
      parameters: { arg: "..."}]
func foo(arg: String): String { ... }

@agent[
    tools: [foo]
]
class A { ... }

Example: Defining a Toolset Type and Configuring It

@toolset
class FooToolset {
    @tool[description: "..."]
    func foo(arg: String): String { ... }

    @tool[description: "..."]
    func bar(): String { ... }
}

@agent[
    tools: [FooToolset()]
]
class A { ... }

Example: Defining Internal Tools

@agent
class A {
    @tool[description: "...",
          parameters: { str: "..." }]
    func bar(str: String): String { ... }
}

Restrictions for tool functions:

  • Currently, the parameter types of tool functions must satisfy the Jsonable interface.
  • The return value of a tool function must satisfy the ToString interface, and the return value of this method will serve as the tool's execution result.

Using Tools and MCP Servers

Agents configure MCP servers and custom tool functions via the tools attribute. This attribute accepts multiple MCP servers/tool functions, each configured using the following syntax:

  • MCP server with stdio protocol: stdioMCP(<command>, <env-kv-pair>*), specifying the command line to start the MCP server and optional environment variables. For example, stdioMCP("command and arguments", ENV_1: "value1", ENV_2, "value2").
  • MCP server with http/sse protocol: mcpHttp(<url>), specifying the MCP server's address. For example, httpMCP("https://abc.com/mcp").
  • Tool function <func-id>+, e.g., foo, bar. Note ⚠️: If the tool is defined inside the Agent class, it can be used directly by the owning Agent without explicit specification in the tools attribute.
  • Toolset construction <expr>, typically an instantiation of a Toolset type, e.g., MyToolset().
@agent[
    tools: [
        stdioMCP("node index.js args" ),
        stdioMCP("python main.py args", SOME_API_KEY: "xxx"),
        httpMCP("http://abc.mcp.server.com"),
        toolA,
        toolB,
        SomeToolset()
    ]
]
class Foo { ... }

MCP tools can also be configured for Agents via API.

// Initialize MCP client
let client = MCPClient("node", ["args"])
let agent = SomeAgent()
// Add MCP tools
agent.toolManager.addTools(client.getTools())

⚠️ Note: Currently, MCP servers only support MCP protocols related to tools.

Additionally, the tools configuration also supports JSON syntax for setting up MCP servers.

  • stdio transmission, configured by: consisting of command (startup command) and args (startup arguments), with optional environment variables env.
  • HTTP SSE transmission, configured by: specifying the MCP server address via url.
@agent[
    tools: [
        { command: "node", args: [ "index.js", "args" ] },
        { command: "python", args: [ "main.py", "args" ], env: { SOME_API_KEY: "xxx" } },
        { url: "http://abc.mcp.server.com" }
    ]
]
class Foo { ... }

Additional Tool Property Settings

All tools allow saving additional property values via the special member variable extra: HashMap<String, String>. Currently, there are two special property values:

  • filterable: "true" | "false" Whether the tool can be filtered by the Agent, used in conjunction with agent.toolManager.enableFilter.
  • terminal: "true" | "false" Whether to terminate Agent execution. When set to true, the Agent will end directly after executing this tool, and the function's return value will serve as the Agent's execution result.

Example: Setting Additional Tool Properties

let tool: Tool = getSomeTool()
tool.extra["filterable"] = "false"
tool.extra["terminal"] = "true"

Skills

A skill is a folder of instructions, scripts, and resources the Agent can load on demand. Instead of stuffing every domain workflow into the system prompt, you put each workflow in its own folder under a skill root and let the Agent decide which one to invoke. At runtime the Agent sees a short listing of every available skill; when it picks one, it calls the runSkill tool with the skill name and the full body of SKILL.md is appended to the conversation.

Skill Folder Layout

skills/
  pdf/
    SKILL.md          # YAML frontmatter + markdown instructions
    scripts/
      extract.py
  xlsx/
    SKILL.md
    data/
      template.xlsx

Each SKILL.md starts with YAML frontmatter and is followed by markdown instructions:

---
name: pdf
description: Extract text from PDF files. Use this whenever the user asks to read or summarize a PDF.
---
# pdf Skill

1. Use `shellExecute` to run `python scripts/extract.py <path>`.
2. Return the extracted text.

Required fields: name (matches the folder name, max 64 chars, no leading or consecutive -) and description (non-empty, max 1024 chars). Optional fields: license, compatibility, metadata, allowed_tools.

Attaching a Skill Root to an Agent

skillRoot defaults to None. With the default, the skill feature is fully disabled: no skillManager property is generated, nothing is appended to the system prompt, and no skill tools are injected — neither runSkill nor the file/shell built-ins. In that case, skillBuiltinTools is ignored.

To enable skills, point @agent at the skill root with skillRoot. The macro will:

  1. Build a SkillManager over that directory at first use.
  2. Append a Skill System prompt block (usage rules + an <available_skills> listing with names, descriptions, locations, and per-skill directory structure) to the Agent's system prompt.
  3. Inject the runSkill tool, plus by default a set of general-purpose file/shell helpers (see below).
@agent[
    model: "deepseek:deepseek-chat",
    executor: "tool-loop",
    skillRoot: "./skills"
]
class SkillfulAgent {
    @prompt("You are a careful assistant. Use the available skills when relevant.")
}

skillRoot also accepts an expression — for example skillRoot: somePath when the path is computed at runtime.

Skill Built-in Tools

When skillRoot is set, the macro injects these tools:

Tool Description
runSkill Loads a skill body by name and returns it to the model. Always injected.
listDirectory Lists entries of an absolute directory path.
fileRead Reads a file at an absolute path; optional 1-based startLine/endLine.
globSearch Recursively matches files against a glob pattern.
grepSearch Regex search across file contents, with optional fileType filter.
shellExecute Runs a shell command (PowerShell on Windows, /bin/sh elsewhere) with optional workDir and timeoutMs.

The five general-purpose tools can be disabled with skillBuiltinTools: false, in which case only runSkill is added. Like skillRoot, skillBuiltinTools also accepts a runtime expression — for example skillBuiltinTools: config.skillBuiltinTools to drive it from configuration. skillBuiltinTools only takes effect when skillRoot is set; if you omit skillRoot, none of these tools are injected regardless of skillBuiltinTools.

@agent[
    model: "deepseek:deepseek-chat",
    skillRoot: "./skills",
    skillBuiltinTools: false   // only runSkill will be injected
]
class MinimalSkillAgent {
    @prompt("...")
}

If you supply a tool with the same name as a built-in (for example, your own fileRead), your tool wins the collision — built-ins are added first and then overwritten by name. The macro logs a status line at agent construction so silent overrides are visible. See the skill and agent_executor.common package docs for the underlying API.

Planning

Each Agent has an executor property to specify which executor to use (different executors employ different planning strategies). Currently, the following executors are supported:

Planning Name Description
naive Direct Q&A
react The Agent selects a tool to complete a solving step each time, then evaluates the tool's execution result to determine if the task is completed, iterating this process until the task is solved.
tool-loop Functionally similar to react but without an explicit thinking process.

Among these, the react and tool-loop executors can specify the maximum number of iterations in the form react:<number>, such as react:5.

Example: Configuring Planning Methods

@agent[executor: "naive"]
class Foo{ }

@agent[executor: "react"]
class Bar{ }

Agent Execution DSL (Experimental)

In addition to using the pre-provided planning methods in Magic, you can also use the planning DSL to control the Agent's execution process more granularly.

Agent Execution DSL Definition: A "programming language" for defining LLM Agent execution flows, achieving complex strategies through combined operations.

  • Avoid Repetitive Code: Prevent writing redundant template code manually.
  • Flexible Customization: Easily write complex planning strategies.

Basic Rules

  • The planning DSL is used within @agent inside @execution; when using the planning DSL, the executor property configuration is ignored.
  • The pipe operator |> connects multiple planning operations.
  • The Agent execution state is a sequence of Prompts.
    • After each LLM operation is completed, the operation result is added to this Prompt sequence.

Usage Example

@agent class Foo {
  @execution(
    plan |> repeat(think |> action, until: "Task is done") |> answer
  )
}

Flow Diagram

             plan         -> think         -> action ->       think -> ... -> answer
| SysPrompt | -> | SysPrompt | -> | SysPrompt |        | SysPrompt |
                 | Plan: ... |    | Plan: ... |        | Plan: ... |
                                  | Think: ... |       | Think: ... |
                                                       | Action: ... |
                                                       | Result: ... |

Planning operations are extracted from existing planning methods, abstracting commonly used logic into composable operations, including three categories: Basic Operations, Task Decomposition Operations, and Condition Control Operations.

Basic Operations Overview

Operator Function
think Generate reasoning steps
action Select and execute a tool
answer Return the final answer
plan Create a plan
repeat Loop internal operation sequences
done Check for termination

Complex Operations: Task Decomposition & Merging

@agent class ResearchAssistant {
  @execution(
    orchestrate(action(tool: web_search)) |> answer
  )
  @tool
  func web_search(...) { ... }
}
Operator Function
orchestrate Split the task into subproblems via LLM, with the number of subproblems determined automatically.

Complex Operations: Conditional Control

@agent class Assistant {
  @execution(
    decide(
      when("Is the question about weather?") |> action(tool: weather_api),
      when("Is the question about order inquiry?") |> action(tool: db_query |> db_summary),
      otherwise |> think |> answer
    )
  )
}
  • decide accepts multiple when clauses.
  • Each when clause consists of a condition (expressed in natural language) and an operation sequence.
    • When the condition in when is met (based on the current execution state), the corresponding operation sequence continues.
    • when clauses are executed top-down.
  • If no onCase is met, the otherwise clause is executed.

External Knowledge

In addition to system prompts, external knowledge can also enhance the Agent's problem-solving capabilities. Agents can extract necessary and useful information from various knowledge sources.

Currently, the Agent's rag property indicates the data source of external knowledge. It accepts multiple data source configurations, each containing the following key-value pairs:

Property Value Description
source String | Expr Data source
mode String Usage mode, supporting "static" and "dynamic"; defaults to "static".
description String Further describes the data source to help the Agent retrieve data more accurately.

The source property indicates the actual data source, supporting two types:

  • A valid path pointing to predefined file types
    • Currently supported file types include markdown and SQLite databases.
  • An expression of type Retriever.
@agent[
  rag: { source: "path/to/some.md", mode: "dynamic" }
]
class Foo { }

⚠️ Note: Using SQLite database functionality requires configuring sqlite = "enable" in cfg.toml. Since the database uses SQLite, third-party dependencies must be installed. For details, see third_party_libs.md.

Examples

Example 1: Command Line Assistant Agent

@agent[executor: "react"]
class CJCAgent {
    @prompt(
        """
        You are a CJC command line assistant.
        You help users generate command lines based on their questions.
        """
    )

    @tool[description: "Retrieve the CJC manual"]
    private func getManual(): String {
        let subProcess: SubProcess = Process.start(
            "cjc", ["--help"], stdOut: ProcessRedirect.Pipe
        )
        let strReader: StringReader<InputStream> = StringReader(subProcess.stdOut)
        let result = strReader.readToEnd().trimAscii()
        return result
    }
}

let agent = CJCAgent()
let result = agent.chat("Compile a file for the ARM platform")

Multi-Agent Collaboration

Multiple Agents can be organized into groups for efficient collaboration. These collaborations generally fall into three categories:

  1. Linear Collaboration: Agents operate sequentially, with each Agent receiving the previous Agent's message (including results and tasks), processing it, and passing the result to the next Agent.
  2. Master-Slave Collaboration: One Agent acts as the leader, supervising other Agents' activities, while other Agents report to the leader.
  3. Free Collaboration: All Agents act as equal collaborative units, engaging in group discussions where each Agent can see all messages.

The AgentGroup interface abstracts all these collaboration methods (see API manual for details).

Linear Collaboration

The pipe expression |> is used to form multiple Agents into a LinearGroup.

let linearGroup: LinearGroup = ag1 |> ag2 |> ag3

Master-Slave Collaboration

Use the <= operator to form multiple Agents into a LeaderGroup, with the Agent before the operator as the leader and the following value as an array of subordinate Agents.

let leaderGroup: LeaderGroup = ag1 <= [ag2, ag3]

Free Collaboration

Use the | operator to form multiple Agents into a FreeGroup.

let freeGroup: FreeGroup = ag1 | ag2 | ag3

FreeGroup also provides a more flexible discuss method.

public enum FreeGroupMode {
    | Auto // The speaker will be selected by LLM automatically
    | RoundRobin
}
class FreeGroup {
    public func discuss(topic!: String, initiator!: String, speech!: String,
                        mode!: FreeGroupMode = FreeGroupMode.Auto): String
    ...
}

The discuss method can specify:

  • topic The discussion topic (i.e., the problem to solve).
  • initiator The first Agent to speak.
  • speech The content of the first Agent's speech.
  • mode The discussion mode, either automatically selecting Agents to speak or using a round-robin approach.

The following code implements a number-guessing game between two Agents, referencing AutoGen.

@agent class AgentWithNumber {
    @prompt(
        "You are playing a game of guess-my-number. You have the "
        "number 33 in your mind, and I will try to guess it. "
        "If I guess too high, say 'too high', if I guess too low, say 'too low'."
    )
}

@agent class AgentGuessNumber {
    @prompt(
        "I have a number in my mind, and you will try to guess it. "
        "If I say 'too high', you should guess a lower number. If I say 'too low', "
        "you should guess a higher number. "
    )
}

func game() {
    let group = AgentWithNumber() | AgentGuessNumber()
    group.discuss(topic: "Number guessing game",
                  initiator: "AgentWithNumber",
                  speech: "I have a number between 1 and 70. Guess it!",
                  mode: FreeGroupMode.RoundRobin)
}

Agent Collaboration Subgroup Construction

When building linear collaboration, not only can Agents participate, but AgentGroups can also directly participate in construction. For example,

ag1 |> (ag2 <= [ag3]) |> ag4

The above code constructs a linear collaboration group, but the second unit is a master-slave collaboration group. Here, the master-slave collaboration group is a subgroup of the linear collaboration.

However, when building master-slave and free collaborations, AgentGroup cannot be directly included in the construction. In this case, the function func subGroup(g: AgentGroup, description!: String): Agent must be used to convert an Agent collaboration group into a subgroup object that can participate in building Agent collaboration groups.

ag1 | (ag2 <= [ag3]) | ag4 // Compilation error
ag1 | subGroup(ag2 <= [ag3], description: "A subgroup attempts to ...") | ag4 // Okay

Quick AI Functions

@ai can be used to decorate functions, indicating that the function's execution will be completed by the LLM. Functions decorated with @ai must be foreign functions, meaning the function's implementation resides on the model side and is an external function to the current code. Requirement: The function's parameter types and return type must satisfy the Jsonable interface. Additionally, @ai allows properties:

Property Name Value Type Description
prompt String Additional knowledge for the AI function
model String Configure the LLM model service to be used; defaults to gpt-4o
tools Array Configure external tools that can be utilized
temperature Float Temperature value when the Agent uses the LLM; defaults to 0.5
dump Bool For debugging purposes, whether to print the transformed AST of the Agent; defaults to false

Example:

@tool[description: "Fetches the html content of a URL."]
func fetch(url: String): String { ... }

@ai[
    prompt: "No more than 3 keywords",
    tools: [fetch]
]
foreign func keywordsOf(url: String): Array<String>

main() { keywordsOf("https://cangjie-lang.cn/") }

Model Configuration

Model configuration follows the format <provider>:<model>. Currently supported model providers are listed below.

Provider Name Example Configuration Notes Service URL Configuration
Alibaba Cloud dashscope:qwen-plus DASHSCOPE_API_KEY DASHSCOPE_BASE_URL, default https://dashscope.aliyuncs.com/compatible-mode/v1
DeepSeek deepseek:deepseek-chat DEEPSEEK_API_KEY DEEPSEEK_BASE_URL, default https://api.deepseek.com
Volcano Ark ark:doubao-lite-4k ARK_API_KEY ARK_BASE_URL, default https://ark.cn-beijing.volces.com/api/v3
Llama.cpp llamacpp No model name or API Key required LLAMACPP_BASE_URl, default http://localhost:8080
Ollama ollama:phi-3 No API Key required OLLAMA_BASE_URl, default http://localhost:11434
OpenAI openai:gpt-4o OPENAI_API_KEY OPENAI_BASE_URL, default https://api.openai.com/v1
Anthropic anthropic:claude-3-5-sonnet ANTHROPIC_API_KEY ANTHROPIC_BASE_URL, default https://api.anthropic.com
SiliconFlow siliconflow:deepseek-ai/DeepSeek-V3 SILICONFLOW_API_KEY SILICONFLOW_BASE_URL, default https://api.siliconflow.cn/v1
Zhipu AI zhipuai:glm-4 ZHIPUAI_API_KEY ZHIPUAI_BASE_URL, default https://open.bigmodel.cn/api/paas/v4
Google google:gemini-2.0-flash GOOGLE_API_KEY GOOGLE_BASE_URL, default https://generativelanguage.googleapis.com/v1beta/openai
Moonshot moonshot:kimi-k2-0711-preview MOONSHOT_API_KEY MOONSHOT_BASE_URL, default https://api.moonshot.cn/v1
OpenRouter openrouter:qwen/qwen3-coder:free OPENROUTER_API_KEY OPENROUTER_BASE_URL, default https://openrouter.ai/api/v1

Model configuration can be used not only in the model property of @agent, but also directly constructed via static methods of ModelManager: static func createChatModel(modelName: String): ChatModel.

Model Support Matrix

Chat Embedding Image
Alibaba Cloud ✔️ ✔️
DeepSeek ✔️ ❌️
Volcano Ark ✔️ ✔️
Llama.cpp ✔️
Ollama ✔️ ✔️
OpenAI ✔️ ✔️ ✔️
Anthropic ✔️
SiliconFlow ✔️ ✔️ ✔️
Zhipu AI ✔️
Google ✔️
Moonshot ✔️
OpenRouter ✔️

To integrate new models, refer to direct API configuration (see below).

Common APIs

This section introduces commonly used APIs. For a complete reference, see API Reference.

Global Configuration

The class magic.config.Config provides the following global configurations, all of which are readable and writable.

Configuration Name Type Description Default Value
logLevel LogLevel Logging level LogLevel.ERROR
logFile String Log file path stdout
enableAgentLog Bool Whether to save individual Agent logs false
agentLogDir String Directory for individual Agent logs ./logs/agent-logs
saveModelRequest Bool Whether to save each model request false
modelRequestDir String Directory for model requests ./logs/model-requests
defaultChatModel Option<ChatModel> Default LLM model None
defaultEmbeddingModel Option<EmbeddingModel> Default embedding model None
externalScriptDir String Directory for external scripts ./external_scripts
defaultContextLen Int LLM context length 32000
defaultTokenizer Option<Tokenizer> Default tokenizer for calculating prompt tokens UnicodeTokenizer()
enableFunctionCall Bool Whether to use LLM function call capability in Agent executors (currently only tool-loop/dsl executors) false
maxReactNumber Int Maximum iterations for React mode 10
modelRetryNumber Int Maximum retries for failed model requests 3
env HashMap<String,String> Environment variables -

Agent Types

All types defined with @agent automatically implement the interface Agent, which provides the following APIs for accessing Agent properties.

public interface Agent {
    /**
     * Name of the agent
     */
    prop name: String

    /**
     * Functionality description of the agent
     */
    prop description: String

    /**
     * Temerature the agent will pass to the LLM
     */
    mut prop temperature: Option<Float64>

    /**
     * System prompt of the agent
     */
    mut prop systemPrompt: String

    /**
     * Tools the agent can use
     */
    prop toolManager: ToolManager

    /**
     * Chat model the agent will use
     */
    mut prop model: Option<ChatModel>

    /**
     * The underlying agent executor
     */
    mut prop executor: AgentExecutor

    /**
     * Retreiver the agent can use
     */
    mut prop retriever: Option<Retriever>

    /**
     * Memory the agent will use
     */
    prop memory: Option<Memory>

    /**
     * Personal data the agent will use
     */
    prop personal: Option<Personal>

    /**
     * Set the agent interceptor
     */
    mut prop interceptor: Option<Interceptor>

    /**
     * Query the agent and get the answer
     */
    func chat(request: AgentRequest): String
}

The method func chat(request: AgentRequest): String is the message processing interface. Note that the interaction method func chat(question: String): String introduced in this section is a wrapper around this interface method.

class AgentRequest {
    // The current user question
    public let question: String
    ...
}

Agent Interception Mechanism

The Agent has a mutable property mut prop interceptor: Interceptor for setting message interception.

enum InterceptorMode {
    | Always
    | Periodic(Int64)
    | Conditional((Request) -> Bool)
}

class Interceptor {
    public init(interceptorAgent: Agent, mode!: InterceptorMode = InterceptorMode.Always)
}

When an interceptor Agent is set, whenever the Agent receives a message (represented as Request), if the interception condition is met, the message will be handled by the interceptor Agent instead of the original Agent. There are three interception modes:

  • Always: Always intercept
  • Periodic: Periodically intercept (e.g., intercept every Nth message)
  • Conditional: Use a predicate function to determine whether to intercept
let ag1 = Foo()
let ag2 = Bar()
ag1.interceptor = Interceptor(ag2, mode: InterceoptorMode.Periodic(2))

ag1.chat("msg 1")
ag1.chat("msg 2")
ag1.chat("msg 3") // ag2 will handle this request message

Built-in Agents

In addition to defining Agents via @agent, the framework provides the following built-in Agents.

BaseAgent

BaseAgent is used to construct Agents via API calls.

class BaseAgent <: Agent {
    public init(
        name!:         String                = "Base Agent",
        description!:  String                = "",
        temperature!:  Option<Float64>       = None,
        systemPrompt!: String                = "",
        toolManager!:  ToolManager           = SimpleToolManager(),
        model!:        Option<ChatModel>     = None,
        executor!:     Option<AgentExecutor> = None,
        retriever!:    Option<Retriever>     = None,
        memory!:       Option<Memory>        = None,
        interceptor!:  Option<Interceptor>   = None
    )
}

Example: Constructing an Agent via BaseAgent

let agent= BaseAgent()
agent.systemPrompt = "New system prompt ..."
agent.model = ModelManager.createChatModel("ollama:phi3")
agent.toolManager.addTool(fooTool)

DispatchAgent

DispatchAgent is specialized for task dispatching in master-slave collaboration mode.

class DispatchAgent {
    public init(model!: String)
}

Example

let group = DiapatchAgent(model: "deepseek:deepseek-chat") <=[
    FooAgent(),
    BarAgent(),
    ...
]

ToolAgent

ToolAgent does not use an LLM to respond to queries but directly executes provided functions to generate responses.

class ToolAgent<T> where T <: Jsonable<T> {
    public init(fn!: (String) -> T)
}

Using this Agent with linear collaboration can achieve functionality similar to Langchain's orchestration.

let group = FooAgent() |> ToolAgent(fn: { q: String => ...; }) |> BarAgent()

HumanAgent

HumanAgent allows users to participate in Agent collaboration as an Agent. It can be viewed as a specialized ToolAgent.

class HumanAgent {
    public init(qaFunc!: Option<(String) -> String> = None)
}

The parameter qaFunc can be customized. The default implementation prints the user question to the terminal and accepts user input as the response.

let humanAgent = HumanAgent(qaFunc: { q: String => println(q); return "answer" })
let result = humanAgent.chat("question")

Jsonable Interface

The Jsonable interface ensures that types can be converted to and from JSON data. The macro @jsonable automatically implements this interface for decorated class/struct/enum types.

public interface Jsonable<T> {
    /**
     * Get the type schema of T
     */
    static func getTypeSchema(): TypeSchema

    /**
     * Validate semantic constraints on the value
     */
    func validate(): ConstraintValidation

    /**
     * Deserialize from a Json string
     */
    static func fromJsonValue(json: JsonValue): T

    /**
     * Serialize to a Json string
     */
    func toJsonValue(): JsonValue
}

For @jsonable types, you can also declare JSON constraints on fields:

@jsonable
class ScoreCard {
    @constraint[score > 0]
    @constraint[score <= maxScore]
    let score: Int64

    let maxScore: Int64
}

Constraint expressions have the following effects:

  • They are included in the schema returned by getTypeSchema() as field-level constraints
  • They are checked automatically after fromJsonValue() finishes deserialization
  • A failed check throws JsonableException

For Agent outputs, chatGet<T>() passes T.getTypeSchema() to the model as the output schema. If the model returns JSON with the correct structure but violates semantic constraints declared by @constraint, the framework asks the model to regenerate and output corrected JSON only.

Integrating New Models

New models can implement the interface ChatModel and then be configured via the agent.model property.

Model-related types are located in the magic.core.model package.

interface ChatModel <: Model {
    func create(req: ChatRequest): ChatResponse
    func asyncCreate(req: ChatRequest): AsyncChatResponse
}

Message types used are defined in magic.core.message.

public class ChatMessage <: ToString {
    public let name: String          // name of the sender
    public let role: ChatMessageRole // role of the sender
    public let content: String       // Content of the message
}

Example: Custom Chat Model

@agent
class Foo { }

class NewModel <: ChatModel {
    public func create(req: ChatRequest): ChatResponse { ... }
    public func asyncCreate(req: ChatRequest): AsyncChatResponse { ... }
}

let foo = Foo()
foo.model = NewModel()

After customizing the model, you can register a name for it, allowing direct configuration via the @agent attribute. The registration function is the member method func registerChatModel(name: String, buildFn: () -> ChatModel) of ModelManager. ⚠️Note: Ensure model registration occurs before invoking Agent instance methods.

Example: Registering a Custom Model

@agent[model: "newModel"]
class Foo { }

main() {
    ModelManager.register("newModel", { => NewModel() })
    let agent = Foo()
}

Custom Planning Methods

When the built-in naive and react planning methods are insufficient, you can develop new executors by implementing the interface AgentExecutor and configure them via the agent.executor property.

Related types for this interface are in the magic.core.agent package.

interface AgentExecutor {
    func run(agent: Agent, request: AgentRequest): AgentResponse

    func asyncRun(agent: Agent, request: AgentRequest): AsyncAgentResponse
}

Example: Custom Agent Executor

@agent
class Foo { }

class NewExecutor <: AgentExecutor {
    func run(agent: Agent, request: AgentRequest): AgentResponse { ... }

    func asyncRun(agent: Agent, request: AgentRequest): AsyncAgentResponse { ... }
}

let foo = Foo()
foo.executor = NewExecutor()

After customizing the executor, you can register a name for it, enabling direct configuration via the @agent attribute. The registration function is the member method func registerAgentExecutor(name: String, buildFn: () -> AgentExecutor) of AgentExecutorManager. ⚠️Note: Ensure executor registration occurs before invoking Agent instance methods.

Example: Registering a Custom Executor

@agent[executor: "newExecutor"]
class Foo { }

main() {
    AgentExecutorManager.register("newExecutor", { => NewExecutor() })
    let agent = Foo()
}

Semantic Retrieval Functionality

Semantic retrieval functionality is divided into the following modules:

  • Vector Model: Constructs semantic vectors vector for data structures' semantic information (String type)
  • Vector Database: Builds vector indexes, maintaining vector -> index mappings; provides vector search
  • Index Mapping Table: Maintains index-to-data mappings, i.e., index -> data
  • Semantic Data Structures: Encapsulates the above modules with convenient interfaces

Except for vector models, all types in this section are defined in the vdb subpackage.

Vector Model

Vectors are defined as follows.

class Vector {
    public init(data: Array<Float32>)
}

Use VectorBuilder to construct vectors.

public class VectorBuilder {
    public VectorBuilder(model!: EmbeddingModel)

    public func createEmbeddingVector(content: String): Vector
}

Currently, the following two embedding model services are supported, located in the model.openai/ollama subpackage.

class OpenAIEmbeddingModel <: EmbeddingModel {
    ...
}

class OllamaEmbeddingModel <: EmbeddingModel {
    ...
}

Use ModelManager.createEmbeddingModel to conveniently construct model instances.

Example: Building a Vector

let model = ModelManager.createEmbeddingModel("openai:text-embedding-ada-002")
let vecBuilder = VectorBuilder(model: model)
let vector= vecBuilder.createEmbeddingVector("First vector")

Vector Database

The vector database is abstracted as the following interface.

public interface VectorDatabase<Self> {
    /**
     * Add the vector to the database
     * ATTENTION: index must start from 0
     */
    func addVector(vector: Vector): Unit

    /**
     * Query the database and find indexes of similar data
     */
    func search(queryVec: Vector, number!: Int64): Array<Int64>

    /**
     * Save to the file
     */
    func save(filePath: String): Unit

    /**
     * Load from the file
     */
    static func load(filePath: String): Self
}

Currently, InMemoryVectorDatabase and FaissVectorDatabase are supported.

class FaissVectorBase {
    public init(dimension: Int64)
}

class InMemoryVectorDatabase {
    public init()
}

Note: If using the faiss vector database, configure faiss = "enable" in cfg.toml and install third-party dependencies. See third_party_libs.md for details.

Index Mapping Table

The index mapping table maintains index -> data relationships and is abstracted as follows.

public interface IndexMap<Self, T> where T <: ToString {
    /**
     * The index is determined by the order in which it was added.
     */
    func add(content: T): Unit

    func get(index: Int64): T

    func save(filePath: String): Unit

    static func load(filePath: String): Self
}

Currently, two types of index mapping tables are provided:

SimpleIndexMap supports saving data of type String, maintaining index -> String mappings. When persisting, it directly saves mappings as JSON files.

class SimpleIndexMap <: IndexMap<SimpleIndexMap, String> { ... }

JsonlIndexMap supports saving any data type satisfying Jsonable. When persisting, it saves data as JSONL files, with indexes corresponding to line numbers.

class JsonlIndexMap<T> <: IndexMap<JsonlIndexMap<T>, T> where T <: Jsonable<T> & ToString

Semantic Data Structures

Vector datasets are generally not used directly but are encapsulated in two data structures: SemanticMap and SemanticSet.

public class SemanticMap<VDB, IMAP, T> where VDB <: VectorDatabase<VDB>,
                                             IMAP <: IndexMap<IMAP, T>,
                                             T <: ToString {
    /**
     * Instantiate the object
     * @param vectorDB Vector database for similarity search
     * @param embeddingModel Embedding model for vectorization; defaults to OpenAI's text-embedding-ada-002
     */
    public init(vectorDB!: VDB,
                indexMap: IMAP,
                embeddingModel!: Option<EmbeddingModel> = None)

    /**
     * Primarily used to set the embedding model
     */
    public mut prop embeddingModel: EmbeddingModel

    /**
     * Insert new key-value pairs
     */
    public func put(key: String, value: T): Unit

    /**
     * Perform semantic search on the map based on key to find similar values;
     * number is the maximum number of results
     * minDistance is the minimum similarity distance
     */
    public func search(query: String,
                       number!: Int64 = 5,
                       minDistance!: Float64 = 0.3): Array<T>

    /**
     * Construct a Retriever object
     */
    public func asRetriever(): Retriever

    /**
     * Save to the specified directory
     */
    public func save(dirPath: String): Unit

    /**
     * Load data from the directory path
     */
    public static func load(dirPath: String): SemanticMap<VDB, IMAP, T>
}

The other data structure, SemanticSet, has similar APIs, with the difference being that the content it searches and retrieves is the value itself.

public class SemanticSet<VDB, IMAP, T> where VDB <: VectorDatabase<VDB>,
                                             IMAP <: IndexMap<IMAP, T>,
                                             T <: ToString {
    public init(vectorDB!: VDB,
                indexMap: IMAP,
                embeddingModel!: Option<EmbeddingModel> = None)
    public mut prop embeddingModel: EmbeddingModel
    public func put(value: T): Unit
    public func search(query: String, number!: Int64 = 5, minDistance!: Float64 = 0.3): Array<T>
    public func save(dirPath: String): Unit
    public static func load(dirPath: String): SemanticSet<VDB, IMAP, T>
}

Usage Example

import magic.rag.vdb.*

main() {
    let smap = SemanticMap(vectorDB: InMemoryVectorDatabase())
    smap.put("Go to Shanghai", "Plan A")
    smap.put("Have a meal", "Plan B")
    smap.put("Go to Beijing", "Plan C")
    smap.put("Sleep", "Plan D")
    let c = smap.search("Go to Shanghai", number: 2)
    println(c)
}

Add the vector database as a retriever to the agent for use. Currently, the vector database can only be used in Static mode.

let agent = FooAgent()
agent.retriever = smap.asRetriever()