Convert agent and plugin metadata/configs to YAML

Replace JSON-based agent and plugin metadata/config files with YAML equivalents and update code paths to read/write YAML. Added python/helpers/yaml for YAML load/dump and files.read_file_yaml helper; subagents, plugins, and files helpers now prefer agent.yaml/plugin.yaml and default_config.yaml. Migration now includes convert_agents_json_yaml and startup_migration is invoked from initialize to perform conversions. Plugin toggle/config handling was refactored (META_FILE_NAME/CONFIG_DEFAULT_FILE_NAME changes, new determined_toggle_from_paths helper) and memory plugin defaults and extensions were updated to use per-agent plugin config lookups with guards for missing configs.

frdel committed Feb 23, 2026 at 19:36 UTC 6e2a44222203eb97b876110ede1c1c3a08ab7902
33 files changed +200 -129
agents/agent0/_context.md deleted
-4
@@ -1,4 +0,0 @@
1 -# Agent 0
2 -- main agent of the system
3 -- communicates to user and delegates to subordinates
4 -- general purpose assistant, communication skills, formatted output
\ No newline at end of file
agents/agent0/agent.json deleted
-5
@@ -1,5 +0,0 @@
1 -{
2 - "title": "Agent 0",
3 - "description": "Main agent of the system communicating directly with the user.",
4 - "context": ""
5 -}
agents/agent0/agent.yaml new
+3
@@ -0,0 +1,3 @@
1 +title: Agent 0
2 +description: Main agent of the system communicating directly with the user.
3 +context: ''
agents/default/_context.md deleted
-3
@@ -1,3 +0,0 @@
1 -# Default prompts
2 -- default prompt file templates
3 -- should be inherited and overriden by specialized prompt profiles
\ No newline at end of file
agents/default/agent.json deleted
-5
@@ -1,5 +0,0 @@
1 -{
2 - "title": "Default",
3 - "description": "Default prompt file templates. Should be inherited and overriden by specialized prompt profiles.",
4 - "context": ""
5 -}
agents/default/agent.yaml new
+4
@@ -0,0 +1,4 @@
1 +title: Default
2 +description: Default prompt file templates. Should be inherited and overriden by specialized
3 + prompt profiles.
4 +context: ''
agents/developer/_context.md deleted
-2
@@ -1,2 +0,0 @@
1 -# Developer
2 -- agent specialized in complex software development
\ No newline at end of file
agents/developer/agent.json deleted
-5
@@ -1,5 +0,0 @@
1 -{
2 - "title": "Developer",
3 - "description": "Agent specialized in complex software development.",
4 - "context": "Use this agent for software development tasks, including writing code, debugging, refactoring, and architectural design."
5 -}
agents/developer/agent.yaml new
+4
@@ -0,0 +1,4 @@
1 +title: Developer
2 +description: Agent specialized in complex software development.
3 +context: Use this agent for software development tasks, including writing code, debugging,
4 + refactoring, and architectural design.
agents/hacker/_context.md deleted
-2
@@ -1,2 +0,0 @@
1 -# Hacker
2 -- agent specialized in cyber security and penetration testing
\ No newline at end of file
agents/hacker/agent.json deleted
-5
@@ -1,5 +0,0 @@
1 -{
2 - "title": "Hacker",
3 - "description": "Agent specialized in cyber security and penetration testing.",
4 - "context": "Use this agent for cybersecurity tasks such as penetration testing, vulnerability analysis, and security auditing."
5 -}
agents/hacker/agent.yaml new
+4
@@ -0,0 +1,4 @@
1 +title: Hacker
2 +description: Agent specialized in cyber security and penetration testing.
3 +context: Use this agent for cybersecurity tasks such as penetration testing, vulnerability
4 + analysis, and security auditing.
agents/researcher/_context.md deleted
-2
@@ -1,2 +0,0 @@
1 -# Researcher
2 -- agent specialized in research, data analysis and reporting
\ No newline at end of file
agents/researcher/agent.json deleted
-5
@@ -1,5 +0,0 @@
1 -{
2 - "title": "Researcher",
3 - "description": "Agent specialized in research, data analysis and reporting.",
4 - "context": "Use this agent for information gathering, data analysis, topic research, and generating comprehensive reports."
5 -}
agents/researcher/agent.yaml new
+4
@@ -0,0 +1,4 @@
1 +title: Researcher
2 +description: Agent specialized in research, data analysis and reporting.
3 +context: Use this agent for information gathering, data analysis, topic research,
4 + and generating comprehensive reports.
initialize.py
+1 -1
@@ -142,7 +142,7 @@ def initialize_preload():
142 def initialize_migration():
143 from python.helpers import migration, dotenv
144 # run migration
145 - migration.migrate_user_data()
145 + migration.startup_migration()
146 # reload .env as it might have been moved
147 dotenv.load_dotenv()
148 # reload settings to ensure new paths are picked up
plugins/example_agent/agents/plugin_example/agent.json deleted
-5
@@ -1,5 +0,0 @@
1 -{
2 - "title": "Plugin Example Agent",
3 - "description": "This is an example agent profile loaded from the 'example_agent' plugin.",
4 - "context": "You are an example agent provided by the 'example_agent' plugin. Your purpose is to demonstrate how plugins can distribute agent profiles."
5 -}
plugins/example_agent/agents/plugin_example/agent.yaml new
+4
@@ -0,0 +1,4 @@
1 +title: Plugin Example Agent
2 +description: This is an example agent profile loaded from the 'example_agent' plugin.
3 +context: You are an example agent provided by the 'example_agent' plugin. Your purpose
4 + is to demonstrate how plugins can distribute agent profiles.
plugins/example_agent/plugin.json deleted
-8
@@ -1,8 +0,0 @@
1 -{
2 - "name": "Example Agent",
3 - "description": "Example agent plugin demonstrating the Agent Zero plugin system.",
4 - "version": "1.0.0",
5 - "settings_sections": [],
6 - "per_project_config": true,
7 - "per_agent_config": false
8 -}
plugins/example_agent/plugin.yaml new
+6
@@ -0,0 +1,6 @@
1 +name: Example Agent
2 +description: Example agent plugin demonstrating the Agent Zero plugin system.
3 +version: 1.0.0
4 +settings_sections: []
5 +per_project_config: true
6 +per_agent_config: false
\ No newline at end of file
plugins/memory/config.default.json deleted
-18
@@ -1,18 +0,0 @@
1 -{
2 - "project_memory_isolation": true,
3 - "memory_recall_enabled": true,
4 - "memory_recall_delayed": false,
5 - "memory_recall_interval": 3,
6 - "memory_recall_history_len": 10000,
7 - "memory_recall_memories_max_search": 12,
8 - "memory_recall_solutions_max_search": 8,
9 - "memory_recall_memories_max_result": 5,
10 - "memory_recall_solutions_max_result": 3,
11 - "memory_recall_similarity_threshold": 0.7,
12 - "memory_recall_query_prep": false,
13 - "memory_recall_post_filter": false,
14 - "memory_memorize_enabled": true,
15 - "memory_memorize_consolidation": true,
16 - "memory_memorize_replace_threshold": 0.9,
17 - "agent_memory_subdir": "default"
18 -}
plugins/memory/default_config.yaml new
+16
@@ -0,0 +1,16 @@
1 +project_memory_isolation: true
2 +memory_recall_enabled: true
3 +memory_recall_delayed: false
4 +memory_recall_interval: 3
5 +memory_recall_history_len: 10000
6 +memory_recall_memories_max_search: 12
7 +memory_recall_solutions_max_search: 8
8 +memory_recall_memories_max_result: 5
9 +memory_recall_solutions_max_result: 3
10 +memory_recall_similarity_threshold: 0.7
11 +memory_recall_query_prep: false
12 +memory_recall_post_filter: false
13 +memory_memorize_enabled: true
14 +memory_memorize_consolidation: true
15 +memory_memorize_replace_threshold: 0.9
16 +agent_memory_subdir: default
\ No newline at end of file
plugins/memory/extensions/python/message_loop_prompts_after/_50_recall_memories.py
+7 -3
@@ -25,11 +25,13 @@ class RecallMemories(Extension):
25
26 async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
27
28 - set = plugins.get_plugin_config("memory")
28 + set = plugins.get_plugin_config("memory", self.agent)
29 + if not set:
30 + return None
31
32 # turned off in settings?
33 if not set["memory_recall_enabled"]:
32 - return
34 + return None
35
36 # every X iterations (or the first one) recall memories
37 if loop_data.iteration % set["memory_recall_interval"] == 0:
@@ -63,7 +65,9 @@ class RecallMemories(Extension):
65 del extras["solutions"]
66
67
66 - set = plugins.get_plugin_config("memory")
68 + set = plugins.get_plugin_config("memory", self.agent)
69 + if not set:
70 + return None
71 # try:
72
73 # get system message and chat history for util llm
plugins/memory/extensions/python/message_loop_prompts_after/_91_recall_wait.py
+3 -1
@@ -6,7 +6,9 @@ from python.helpers import plugins
6 class RecallWait(Extension):
7 async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
8
9 - set = plugins.get_plugin_config("memory")
9 + set = plugins.get_plugin_config("memory", self.agent)
10 + if not set:
11 + return None
12
13 task = self.agent.get_data(DATA_NAME_TASK_MEMORIES)
14 iter = self.agent.get_data(DATA_NAME_ITER_MEMORIES) or 0
plugins/memory/extensions/python/monologue_end/_50_memorize_fragments.py
+6 -2
@@ -16,7 +16,9 @@ class MemorizeMemories(Extension):
16 async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
17 # try:
18
19 - set = plugins.get_plugin_config("memory")
19 + set = plugins.get_plugin_config("memory", self.agent)
20 + if not set:
21 + return None
22
23 if not set["memory_memorize_enabled"]:
24 return
@@ -36,7 +38,9 @@ class MemorizeMemories(Extension):
38 async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
39
40 try:
39 - set = plugins.get_plugin_config("memory")
41 + set = plugins.get_plugin_config("memory", self.agent)
42 + if not set:
43 + return None
44
45 db = await Memory.get(self.agent)
46
plugins/memory/extensions/python/monologue_end/_51_memorize_solutions.py
+7 -2
@@ -15,7 +15,9 @@ class MemorizeSolutions(Extension):
15 async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
16 # try:
17
18 - set = plugins.get_plugin_config("memory")
18 + set = plugins.get_plugin_config("memory", self.agent)
19 + if not set:
20 + return None
21
22 if not set["memory_memorize_enabled"]:
23 return
@@ -34,7 +36,10 @@ class MemorizeSolutions(Extension):
36
37 async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
38 try:
37 - set = plugins.get_plugin_config("memory")
39 + set = plugins.get_plugin_config("memory", self.agent)
40 + if not set:
41 + return None
42 +
43
44 db = await Memory.get(self.agent)
45
plugins/memory/plugin.json deleted
-8
@@ -1,8 +0,0 @@
1 -{
2 - "name": "Memory",
3 - "description": "Provides persistent memory capabilities to Agent Zero agents.",
4 - "version": "1.0.0",
5 - "settings_sections": ["agent"],
6 - "per_project_config": true,
7 - "per_agent_config": false
8 -}
plugins/memory/plugin.yaml new
+7
@@ -0,0 +1,7 @@
1 +name: Memory
2 +description: Provides persistent memory capabilities to Agent Zero agents.
3 +version: 1.0.0
4 +settings_sections:
5 + - agent
6 +per_project_config: true
7 +per_agent_config: false
\ No newline at end of file
python/helpers/files.py
+7 -4
@@ -3,19 +3,16 @@ from fnmatch import fnmatch
3 import json
4 from ntpath import isabs
5 import os
6 -import sys
6 import re
7 import base64
8 import shutil
9 import tempfile
10 from typing import Any, Literal
11 import zipfile
13 -import importlib
14 -import importlib.util
15 -import inspect
12 import glob
13 import mimetypes
14 from simpleeval import simple_eval
15 +from python.helpers import yaml
16
17 AGENTS_DIR = "agents"
18 PLUGINS_DIR = "plugins"
@@ -224,6 +221,12 @@ def read_file_json(relative_path: str, encoding="utf-8"):
221 with open(absolute_path, "r", encoding=encoding) as f:
222 return json.load(f)
223
224 +def read_file_yaml(relative_path: str, encoding="utf-8"):
225 + absolute_path = get_abs_path(relative_path)
226 +
227 + with open(absolute_path, "r", encoding=encoding) as f:
228 + return yaml.loads(f.read())
229 +
230 def read_file_bin(relative_path: str):
231 # Try to get the absolute path for the file from the original directory or backup directories
232 absolute_path = get_abs_path(relative_path)
python/helpers/migration.py
+28
@@ -1,7 +1,15 @@
1 import os
2 +import json
3 from python.helpers import files
4 +from python.helpers import subagents
5 +from python.helpers import yaml as yaml_helper
6 from python.helpers.print_style import PrintStyle
7
8 +
9 +def startup_migration() -> None:
10 + migrate_user_data()
11 + convert_agents_json_yaml()
12 +
13 def migrate_user_data() -> None:
14 """
15 Migrate user data from /tmp and other locations to /usr.
@@ -44,6 +52,26 @@ def migrate_user_data() -> None:
52
53 PrintStyle().print("Migration check complete.")
54
55 +
56 +def convert_agents_json_yaml() -> None:
57 + for root in subagents.get_agents_roots():
58 + rel_root = files.deabsolute_path(root)
59 + for subdir in files.get_subdirectories(rel_root):
60 + agent_yaml = os.path.join(rel_root, subdir, "agent.yaml")
61 + if files.exists(agent_yaml):
62 + continue
63 +
64 + agent_json = os.path.join(rel_root, subdir, "agent.json")
65 + if not files.exists(agent_json):
66 + continue
67 +
68 + try:
69 + agent_obj = json.loads(files.read_file(agent_json))
70 + files.write_file(agent_yaml, yaml_helper.dumps(agent_obj))
71 + except Exception as e:
72 + PrintStyle.error(f"Failed to convert {agent_json} to YAML", e)
73 + continue
74 +
75 # --- Helper Functions ----------------------------------------------------------
76
77 def _move_dir(src: str, dst: str, overwrite: bool = False) -> None:
python/helpers/plugins.py
+50 -35
@@ -2,9 +2,9 @@ from __future__ import annotations
2
3 import re, json
4 from pathlib import Path
5 -from typing import Any, Dict, List, Literal, Optional, TYPE_CHECKING, TypedDict
5 +from typing import Any, Dict, Iterator, List, Literal, Optional, TYPE_CHECKING, TypedDict
6
7 -from python.helpers import files, print_style
7 +from python.helpers import files, print_style, yaml as yaml_helper
8 from pydantic import BaseModel, Field
9
10 if TYPE_CHECKING:
@@ -22,12 +22,12 @@ class PluginAssetFile(TypedDict):
22 project_name: str
23 agent_profile: str
24
25 -META_FILE_NAME = "plugin.json"
25 +META_FILE_NAME = "plugin.yaml"
26 CONFIG_FILE_NAME = "config.json"
27 -CONFIG_DEFAULT_FILE_NAME = "config.default.json"
27 +CONFIG_DEFAULT_FILE_NAME = "default_config.yaml"
28 DISABLED_FILE_NAME = ".toggle-0"
29 ENABLED_FILE_NAME = ".toggle-1"
30 -TOGGLE_FILE_PATTERN = "*.toggle-[01]"
30 +TOGGLE_FILE_PATTERN = ".toggle-[01]"
31
32
33 class PluginMetadata(BaseModel):
@@ -55,11 +55,11 @@ class PluginListItem(BaseModel):
55 toggle_state: ToggleState = "disabled"
56
57
58 -def get_plugin_roots() -> List[str]:
58 +def get_plugin_roots(plugin_name:str="") -> List[str]:
59 """Plugin root directories, ordered by priority (user first)."""
60 return [
61 - files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR),
62 - files.get_abs_path(files.PLUGINS_DIR),
61 + files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR, plugin_name),
62 + files.get_abs_path(files.PLUGINS_DIR, plugin_name),
63 ]
64
65
@@ -90,8 +90,11 @@ def get_enhanced_plugins_list(
90 try:
91 if not d.is_dir() or d.name.startswith("."):
92 continue
93 + meta_file = str(d / META_FILE_NAME)
94 + if not files.exists(meta_file):
95 + continue
96 meta = PluginMetadata.model_validate(
94 - files.read_file_json(str(d / META_FILE_NAME))
97 + files.read_file_yaml(meta_file)
98 )
99 has_main_screen = files.exists(str(d / "webui" / "main.html"))
100 has_config_screen = files.exists(str(d / "webui" / "config.html"))
@@ -112,8 +115,9 @@ def get_enhanced_plugins_list(
115 toggle_state=toggle_state,
116 )
117 )
115 - except:
116 - pass
118 + except Exception as e:
119 + print_style.PrintStyle.error(f"Failed to load plugin {d.name}: {e}")
120 + continue
121
122 if custom:
123 load_plugins(files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR), True)
@@ -127,7 +131,7 @@ def get_plugin_meta(plugin_name: str):
131 if not plugin_dir:
132 return None
133 return PluginMetadata.model_validate(
130 - files.read_file_json(files.get_abs_path(plugin_dir, META_FILE_NAME))
134 + files.read_file_yaml(files.get_abs_path(plugin_dir, META_FILE_NAME))
135 )
136
137
@@ -192,6 +196,10 @@ def get_enabled_plugins(agent: Agent | None):
196 # every plugin is on by default, unless disabled in usr dir
197 enabled = True
198
199 + # root plugin paths
200 + plugin_paths = get_plugin_roots(plugin)
201 +
202 + # + agent paths
203 if agent:
204 from python.helpers import subagents
205
@@ -201,46 +209,51 @@ def get_enabled_plugins(agent: Agent | None):
209 plugin,
210 must_exist_completely=True,
211 include_default=False,
204 - include_user=True,
212 + include_user=False,
213 include_plugins=False,
214 include_project=True,
215 )
216 + plugin_paths = agent_paths + plugin_paths
217
209 - # go through agent paths in reverse order and determine the state
210 - for agent_path in reversed(agent_paths):
211 - if enabled:
212 - enabled = not files.exists(
213 - files.get_abs_path(agent_path, DISABLED_FILE_NAME)
214 - )
215 - else:
216 - enabled = files.exists(
217 - files.get_abs_path(agent_path, ENABLED_FILE_NAME)
218 - )
218 + # go through paths in reverse order and determine the state
219 + enabled = determined_toggle_from_paths(enabled, reversed(plugin_paths))
220
221 if enabled:
222 active.append(plugin)
223
224 return active
225
226 +def determined_toggle_from_paths(default:bool, paths:Iterator[str]):
227 + enabled = default
228 + for plugin_path in paths:
229 + if enabled:
230 + enabled = not files.exists(
231 + files.get_abs_path(plugin_path, DISABLED_FILE_NAME)
232 + )
233 + else:
234 + enabled = files.exists(
235 + files.get_abs_path(plugin_path, ENABLED_FILE_NAME)
236 + )
237 + return enabled
238
239 def get_toggle_state(plugin_name: str) -> ToggleState:
240 meta = get_plugin_meta(plugin_name)
241 if not meta:
242 return "disabled"
243 if meta.always_enabled:
231 - return "enabled"
244 + return "enabled"
245
233 - state = "enabled"
246 + # root plugin paths
247 + plugin_paths = get_plugin_roots(plugin_name)
248 + state = "enabled" if determined_toggle_from_paths(True, reversed(plugin_paths)) else "disabled"
249
235 - # toggles inside of user directory (there should be only one, but let's make it work in any case)
236 - usr_toggles = files.find_existing_paths_by_pattern(files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR, plugin_name, TOGGLE_FILE_PATTERN))
237 - for toggle in usr_toggles:
238 - if toggle.endswith(ENABLED_FILE_NAME):
239 - state = "enabled"
240 - if toggle.endswith(DISABLED_FILE_NAME):
241 - state = "disabled"
250 + # global toggles
251 + usr_toggles = [
252 + files.find_existing_paths_by_pattern(files.get_abs_path(files.PLUGINS_DIR, plugin_name, TOGGLE_FILE_PATTERN)),
253 + files.find_existing_paths_by_pattern(files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR, plugin_name, TOGGLE_FILE_PATTERN))
254 + ]
255
243 - # if there are more toggles in project/agent directories, return advanced
256 + # additional toggles in project/agent directories, return advanced
257 if meta.per_agent_config or meta.per_project_config:
258 configs = find_plugin_assets(
259 TOGGLE_FILE_PATTERN,
@@ -311,7 +324,9 @@ def get_plugin_config(
324 find_plugin_dir(plugin_name), CONFIG_DEFAULT_FILE_NAME
325 )
326 if file_path and files.exists(file_path):
314 - return json.loads(files.read_file(file_path))
327 + return (json.loads if file_path.lower().endswith(".json") else yaml_helper.loads)(
328 + files.read_file(file_path)
329 + )
330 return None
331
332
@@ -322,7 +337,7 @@ def save_plugin_config(
337 plugin_name, project_name, agent_profile, CONFIG_FILE_NAME
338 )
339 if file_path:
325 - files.write_file(file_path, json.dumps(settings))
340 + files.write_file(file_path, yaml_helper.dumps(settings))
341
342
343 def find_plugin_asset(
python/helpers/subagents.py
+15 -4
@@ -1,4 +1,5 @@
1 from python.helpers import files
2 +from python.helpers import yaml as yaml_helper
3 from typing import TypedDict, TYPE_CHECKING
4 from pydantic import BaseModel, model_validator
5 import json
@@ -88,8 +89,13 @@ def _get_agents_list_from_dir(dir: str, origin: Origin) -> dict[str, SubAgentLis
89
90 for subdir in subdirs:
91 try:
91 - agent_json = files.read_file(files.get_abs_path(dir, subdir, "agent.json"))
92 - agent_data = SubAgentListItem.model_validate_json(agent_json)
92 + agent_yaml_path = files.get_abs_path(dir, subdir, "agent.yaml")
93 + if files.exists(agent_yaml_path):
94 + agent_yaml = files.read_file(agent_yaml_path)
95 + agent_data = SubAgentListItem.model_validate(yaml_helper.loads(agent_yaml) or {})
96 + else:
97 + agent_json = files.read_file(files.get_abs_path(dir, subdir, "agent.json"))
98 + agent_data = SubAgentListItem.model_validate_json(agent_json)
99 name = agent_data.name or subdir
100 agent_data.name = name
101 agent_data.path = files.get_abs_path(dir, subdir)
@@ -176,8 +182,13 @@ def delete_agent_data(name: str) -> None:
182
183 def _load_agent_data_from_dir(dir: str, name: str, origin: Origin) -> SubAgent | None:
184 try:
179 - subagent_json = files.read_file(files.get_abs_path(dir, name, "agent.json"))
180 - subagent = SubAgent.model_validate_json(subagent_json)
185 + agent_yaml_path = files.get_abs_path(dir, name, "agent.yaml")
186 + if files.exists(agent_yaml_path):
187 + agent_yaml = files.read_file(agent_yaml_path)
188 + subagent = SubAgent.model_validate(yaml_helper.loads(agent_yaml) or {})
189 + else:
190 + subagent_json = files.read_file(files.get_abs_path(dir, name, "agent.json"))
191 + subagent = SubAgent.model_validate_json(subagent_json)
192 except Exception:
193 # backward compatibility (before agent.json existed)
194 try:
python/helpers/yaml.py new
+24
@@ -0,0 +1,24 @@
1 +import json
2 +import yaml
3 +
4 +
5 +def loads(text: str):
6 + return yaml.safe_load(text)
7 +
8 +
9 +def dumps(obj, **kwargs) -> str:
10 + dump_kwargs = {
11 + "allow_unicode": True,
12 + "sort_keys": False,
13 + **kwargs,
14 + }
15 + return yaml.safe_dump(obj, **dump_kwargs)
16 +
17 +
18 +def from_json(text: str, **yaml_dump_kwargs) -> str:
19 + return dumps(json.loads(text), **yaml_dump_kwargs)
20 +
21 +
22 +def to_json(text: str, **json_dump_kwargs) -> str:
23 + obj = loads(text)
24 + return json.dumps(obj, ensure_ascii=False, **json_dump_kwargs)