My Agent Core Design - Codegen Contract
Codegen Contract
本章用于把实现时容易分叉的接口、schema、内置工具和模板资源固定下来。
其他章节描述产品语义和行为, 本章描述第一版生成代码时必须落地的最小稳定契约。实现可以在内部增加字段或 helper, 但对外 API、持久化字段、tool canonical name 和错误码不能随意漂移。
Public SDK Shape
- Python distribution name 使用
soong-agent. - SDK import package name 第一版使用
agent_core. - CLI 命令名使用
soong-agent. - 用户侧主要入口:
AgentRuntimeUserMessageRunHandleRuntimeEventPermissionRequestPermissionDecisionToolDefinitionToolResultAgentDefinition
AgentRuntime最小构造参数:project_dir: str | Path | None = Noneconfig_path: str | Path | None = Nonehome_dir: str | Path | None = Nonesession_db_path: str | Path | None = Nonepermission_callback: Callable[[PermissionRequest], Awaitable[PermissionDecision]] | None = Noneprovider_registry: ProviderRegistry | None = Nonetool_registry: ToolRegistry | None = Nonedebug: bool = False
- 用户级 home 解析:
home_dir显式传入时优先.- 否则使用环境变量
SOONG_AGENT_HOME. - 否则使用
~/.soong-agent. config_path省略时固定读取${SOONG_AGENT_HOME}/config.toml.config_path只表示 SDK/测试显式指定的用户级 config 文件路径, 不表示项目级 config 发现机制.- 用户级
config.toml必须存在; 不存在、解析失败、schema 校验失败都使 runtime / CLI 启动失败. - 第一版不提供
soong-agent init; CLI / runtime 不自动创建默认 config.
<project>解析:project_dir省略时使用当前 cwd.project_dir指向文件时使用其父目录.project_dir不存在时启动失败.- 启动时 resolve symlink, 后续边界判断使用 resolved path.
- runtime 不读取
<project>/.soong-agent/config.toml.
AgentRuntime必须支持:async with AgentRuntime(...) as runtimeawait runtime.close()await runtime.start(message: str | UserMessage, session_id: str | None = None, mode: Literal["normal", "orchestrator"] = "normal") -> RunHandleruntime.register_provider(key: str, factory: ProviderFactory) -> Noneruntime.register_tool(definition: ToolDefinition, handler: ToolHandler) -> Noneruntime.register_agent_definition(definition: AgentDefinition, source: Literal["code"] = "code") -> Noneawait runtime.replay_session(session_id: str, from_seq: int | None = None, to_seq: int | None = None, include_sensitive: bool = False) -> ReplayResultawait runtime.replay_run(run_id: str, include_sensitive: bool = False) -> ReplayResultawait runtime.get_node_path(node_id: str) -> list[Node]await runtime.delete_session(session_id: str) -> DeleteSessionResultawait runtime.cleanup_project_tasks(project: str | Path, dry_run: bool = True, include_failed: bool = False, include_cancelled: bool = False, older_than: datetime | None = None) -> CleanupResultawait runtime.cleanup_artifacts(session_id: str | None = None, dry_run: bool = True, include_all: bool = False, older_than: datetime | None = None, max_bytes: int | None = None) -> CleanupResult
RunHandle最小字段:run_id: strsession_id: stragent_id: strstatus: RunStatusmode: RunMode
RunHandle必须支持:events(debug: bool = False) -> AsyncIterator[RuntimeEvent]cancel() -> Awaitable[CancelResult]inspect_child(child_run_id: str, include_sensitive: bool = False) -> Awaitable[InspectResult]child_events(child_run_id: str, debug: bool = False) -> AsyncIterator[RuntimeEvent]
run.events()是单消费者流, 新订阅不回放历史事件。- 对外 SDK 不暴露 provider SDK 原始 request / response 类型。
Core Type Contract
- 所有对外、tool、provider、storage 边界类型使用 Pydantic。
- 默认
extra="forbid"。 - ID 字段第一版统一是
str, 通过 helper 校验前缀和安全字符。 - 时间字段统一使用 UTC ISO8601 字符串落库和 WAL; Python 对外对象可以暴露
datetime。
ContentBlock
TextBlocktype = "text"text: str
JsonBlocktype = "json"data: Any | Nonesummary: str | Noneartifact_id: str | None
ToolCallBlocktype = "tool_call"tool_call_id: strname: strarguments: dictmetadata: dict = {}
ToolResultBlocktype = "tool_result"tool_call_id: stris_error: bool = Falsecontent: list[ContentBlock]error: ErrorPayload | Nonemetadata: dict = {}
ArtifactRefBlocktype = "artifact_ref"artifact_id: strsummary: str | Nonemime_type: str | None
ToolResult
tool_call_id: strtool_name: strcontent: list[ContentBlock]is_error: bool = Falseerror: ErrorPayload | None = Nonemetadata: dict = {}
简单 handler 返回 str / dict / list / int / bool / None 时, core 自动包装为 ToolResult。复杂 tool 推荐直接返回 ToolResult。
RuntimeEvent
event_id: strseq: int | Nonerun_seq: int | Nonesession_id: stragent_id: str | Nonerun_id: str | Nonelevel: Literal["debug", "info", "warning", "error"]event_type: EventTypenode_id: str | Nonetool_call_id: str | Nonepayload: dictcreated_at: datetime
实时流中的高频 delta event 可以没有 seq, 因为默认不落 SQLite。落库 event 必须有 seq。
PermissionRequest
request_id: strsession_id: stragent_id: strrun_id: strparent_agent_id: str | Noneparent_run_id: str | Noneagent_role: strtool_name: strpermission: Literal["readonly", "write"]tags: list[str]args_summary: strtarget_scope: str | Nonecwd: strenv_summary: dictnetwork_host: str | Nonedangerous: boolhook_summary: dict | Nonesuggested_decision: PermissionDecisionKindmetadata: dict = {}
PermissionDecision
decision: Literal["allow_once", "allow_for_session", "deny"]reason: str | Nonemetadata: dict = {}
AgentDefinition
agent_definition_id: strname: strdescription: strbody: str = ""model_profile: str | dict | None = Nonesuggested_tools: list[str] = []tags: list[str] = []overrides: str | None = Nonesource: Literal["builtin", "user", "code"]metadata: dict = {}
文件形式使用 frontmatter + markdown body。frontmatter 必填 id, name, description; body 是该 agent 的 system instructions。
Built-in File Tools
第一版内置文件工具使用 code.* canonical name。Plan 文件也通过这些普通工具写入。
code.read_file- permission:
readonly - tags:
code,filesystem,readonly - input:
path: strstart_line: int = 1max_lines: int = 200
- constraints:
start_line >= 11 <= max_lines <= 1000- 每行最多返回 4096 bytes; 超过截断该行.
- result:
pathcontenttruncated: boolnext_start_line: int | Nonetruncated_lines: list[int]binary: boolalready_loaded: bool | None, 仅 instruction 文件重复加载时使用.
- permission:
code.list_dir- permission:
readonly - tags:
code,filesystem,readonly - input:
path: strrecursive: bool = Falselimit: int | None
- result:
entriestruncated
- permission:
code.write_file- permission:
write - tags:
code,filesystem,write - input:
path: strcontent: strcreate_dirs: bool = Trueoverwrite: bool = False
- result:
pathbytes_writtencreated: booloverwritten: bool
- errors:
path_conflict, 当目标已存在且overwrite=false.
- permission:
code.edit_file- permission:
write - tags:
code,filesystem,write - input:
path: stredits: list[{old: str, new: str, replace_all: bool = False}] | Noneunified_diff: str | Nonecreate_if_missing: bool = False
- constraints:
edits和unified_diff必须二选一.edits使用 old/new 精确替换; old 默认必须唯一匹配.unified_diff必须只修改当前path.unified_diff只支持单文件 diff; 多文件修改必须多次调用code.edit_file.unified_diff不支持 rename/delete/binary patch.unified_diff必须能干净应用到当前文件内容.
- result:
pathedits_appliedbytes_written
- errors:
file_not_foundtext_not_foundambiguous_editpatch_apply_failedpatch_path_mismatch
- permission:
code.search- permission:
readonly - tags:
code,filesystem,readonly - input:
query: strpath: str | Noneglob: str | Nonelimit: int | None
- result:
matchestruncated
- default path:
<project>
- permission:
code.run_command- permission:
write - tags:
code,dangerous - input:
argv: list[str]cwd: str | Nonetimeout_ms: int | None
- constraints:
- 只接受 argv list, 不接受 shell string.
- 第一版不提供 OS 级 sandbox / container sandbox.
- 第一版安全边界来自 argv list、cwd 限制、allowed roots、env allowlist、timeout、permission/hook、输出截断/artifact 和 secret redaction.
cwd省略时为<project>.cwd必须位于<project>或 allowed roots 内.- 模型不能传 env; env 只能来自 config/tool definition allowlist.
- 默认 timeout 120000ms, 最大 600000ms.
- result:
exit_codestdoutstderrstdout_artifact_idstderr_artifact_idtruncated
- permission:
code.read_file 按行读取, 默认最多 200 行, 单次最多 1000 行, 每行最多 4096 bytes; 超过时截断并返回 metadata。读取超大文本不 artifact 化全文, 因为源文件本身已经存在。除 code.read_file 外, 其他 tool 输出过大时保存 artifact 并在 tool_result 中返回摘要和 artifact_ref。
文件工具必须 normalize path、resolve symlink, 再做边界判断。写工具默认只能写 session.cwd 和配置允许的 write roots。普通 read 不限制在 session.cwd, 但敏感路径需要 permission callback。
敏感路径默认包括:
~/.ssh~/.gnupg~/.aws~/.config/gcloud*.pem*.key.env.env.*
命中敏感路径的 code.read_file / code.list_dir / code.search 必须触发 permission callback; 没有 callback 时 deny。
code.read_file 读取 instruction 文件时, 除普通 tool result 外还要注册 dynamic system block。普通业务文件读取不注册 dynamic system block。
Instruction Catalog Contract
- 启动 / run 开始时构造
instruction_catalogstatic system block. - catalog 只包含路径和 frontmatter 元信息, 不包含正文.
- 没有 frontmatter 的 instruction 文件不跳过, 使用相对路径作为基础 metadata.
- 扫描来源:
${SOONG_AGENT_HOME}/CLAUDE.md${SOONG_AGENT_HOME}/AGENTS.md${SOONG_AGENT_HOME}/rules/**/*.md<project>/**/CLAUDE.md<project>/**/AGENTS.md
- 不扫描
<project>/.soong-agent/rules. - 不存在项目级 skills / agents / tools / hooks / memory / config.
- 同目录
CLAUDE.md优先于AGENTS.md; catalog 只展示胜出的文件. - 扫描跳过
.git,node_modules,.venv,venv,dist,build,target,.next,.cache,__pycache__. - catalog 最大 200 个文件, 超过按路径排序截断并标记 truncated.
- 模型通过普通
code.read_file决定读取哪些 instruction 正文. - 读取 instruction 正文后写 dynamic system block; 可选写 SQLite
node_type=instruction_context. - 同一 active path 重复加载相同 hash 的 instruction 不重复写 node, tool result 标记
already_loaded=true. - 文件 mtime/hash 变化后允许重新加载新版 dynamic system block.
Built-in Agent And Task Tool Schemas
内置 agent tools:
agent.list_agent_definitions(input={})agent.create_sub_agent(agent_definition_id?, task, context?, constraints?, allowed_tools?, expected_output_schema?, timeout_ms?)agent.fork_agent(agent_definition_id?, task, constraints?, allowed_tools?, expected_output_schema?, timeout_ms?)agent.list_workers(worker_pool_id?)agent.dispatch_worker(task_id, worker_pool_id?, worker_agent_id?, allowed_step_ids?, instruction, context?, constraints?, allowed_tools?, expected_output_schema?, timeout_ms?)
内置 Plan / Task tools:
agent.plan_template(goal?, suggested_dir?)agent.task_template(goal?)agent.task_create(task_id, wal_name, title, summary, steps)agent.task_get(task_id, include_terminal_steps=False)agent.task_list(status?, include_terminal=False, limit=50, offset=0)agent.task_update(task_id, operations)agent.task_query_steps(task_id, statuses?, worker_pool_id?, claimed_by_agent_id?, include_terminal_steps=False, limit=50, offset=0)agent.task_claim_step(task_id, step_id)agent.task_update_step(task_id, step_id, status?, result_summary?, artifact_ids?, reason?)agent.task_complete(task_id, result_summary?)agent.task_fail(task_id, reason?)agent.task_cancel(task_id, reason?)
实际 tool name 必须和这里一致。历史章节出现 “plan tool” 时, 第一版 canonical name 使用 agent.plan_template。
Task WAL Payload Contract
每行 WAL 的公共字段:
wal_seqsession_idevent_idevent_typeactor_agent_idactor_run_idtask_idstep_idpayloadcreated_at
事件 payload 第一版固定为:
task_createdtask: 完整 Task DAG snapshot。
task_runningprevious_statusstatus = "running"reason
task_updatedoperationsupdated_after_dispatch_step_idstask_summary
task_reopenedprevious_statusstatus = "pending"reason
task_completedresult_summarycancelled_optional_step_ids
task_failedreasonfailed_step_ids
task_cancelledreasoncancelled_step_ids
task_step_readyprevious_statusstatus = "ready"depends_on_step_ids
task_step_claimedprevious_statusclaimed_by_agent_idclaimed_by_run_idlease_expires_at
task_step_startedprevious_statusstatus = "running"lease_expires_at
task_step_updatedprevious_statusstatusresult_summaryartifact_idslease_expires_at
task_step_blockedprevious_statusreasonresult_summaryartifact_ids
task_step_completedprevious_statusresult_summaryartifact_ids
task_step_failedprevious_statusreasonresult_summaryartifact_ids
task_step_cancelledprevious_statusreasonresult_summary
task_step_reopenedprevious_statusstatus = "pending"reason
task_step_lease_expiredprevious_statusclaimed_by_agent_idclaimed_by_run_idexpired_at
WAL replay 只依赖 event_type 和 payload, 不依赖 SQLite event。
Error Codes
错误码第一版集中定义为 ErrorCode StrEnum。
- Common:
validation_errorschema_errorconfig_errorpermission_deniedtool_not_availabletimeoutcancelledinternal_error
- Provider:
provider_errorunsupported_capabilityprovider_auth_failedprovider_rate_limitedprovider_timeout
- Storage:
storage_errormigration_failedsession_activepath_conflictfile_not_foundtext_not_foundambiguous_editpatch_apply_failedpatch_path_mismatchwrite_outside_allowed_roots
- Agent:
invalid_agent_definitionduplicate_agent_definitioninvalid_agent_overridechild_agent_limit_exceededworker_busyworker_not_availableworker_pool_busy
- Task:
task_not_foundtask_terminaltask_not_dispatchabledependency_cyclestep_not_foundstep_not_readystep_already_claimedstep_already_claimed_by_runstep_has_dependentsno_step_claimedtask_wal_unavailable
- Memory / Skill:
memory_recall_failedmemory_write_failedskill_not_foundskill_load_failed
no_step_claimed 是成功结果中的业务状态, 默认不作为 ToolResult.is_error=true。
Config TOML Minimum Example
配置中的 path 字段支持两个内置占位符:
${SOONG_AGENT_HOME}: runtime 解析出的用户级 home.<project>: runtime 解析出的项目工作目录.
该示例同时作为 src/agent_core/assets/templates/config_default.toml 的默认内容基线; 模板默认 provider 使用本地 Ollama gemma4.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
[runtime]
cancel_timeout_ms = 10000
[model]
provider = "ollama"
base_url = "http://127.0.0.1:11434"
api_key_env = ""
name = "gemma4"
context_window = 8192
max_output_tokens = 4096
temperature = 0.2
timeout_ms = 60000
[model.retry]
max_attempts = 3
initial_backoff_ms = 500
max_backoff_ms = 8000
[model_overrides.compact]
max_output_tokens = 2048
temperature = 0.0
[context]
session_db_path = "${SOONG_AGENT_HOME}/sessions.sqlite"
active_path_only = true
reserve_output_tokens = 4096
dynamic_system_budget = 12000
task_board_token_budget = 1200
task_recent_changes_limit = 20
task_recent_changes_window_minutes = 30
[compact]
enabled = true
reserve_tokens = 8000
keep_recent_tokens = 16000
auto_background = true
recovery_sync = true
model_profile = "compact"
max_summary_tokens = 2048
[memory]
enabled = true
memory_dir = "${SOONG_AGENT_HOME}/memory"
categories = ["user", "feedback", "reference"]
extract_every_messages = 32
extract_every_tokens = 12000
idle_seconds = 120
catalog_max_tokens = 4000
recall_top_k = 5
memory_context_token_budget = 6000
[agents]
max_children_per_run = 4
max_concurrent_children_per_session = 8
default_sub_agent_definition = "default_sub_agent"
default_fork_agent_definition = "default_fork_agent"
default_child_timeout_ms = 600000
child_cancel_timeout_ms = 30000
[[agents.worker_pools]]
pool_id = "default"
[[agents.worker_pools.workers]]
worker_id = "worker_general_1"
agent_definition_id = "default_worker_agent"
allowed_tools = ["agent.task_get", "agent.task_query_steps", "agent.task_claim_step", "agent.task_update_step", "code.read_file", "code.search"]
[plan]
default_dir = "<project>/.soong-agent/plans"
template_name = "default"
[task]
wal_dir = "<project>/.soong-agent/tasks"
step_lease_timeout_ms = 300000
[permissions]
readonly_default = "allow"
write_without_callback = "deny"
remember_scope = "session"
allow_for_session_enabled = true
[permissions.network_policy]
default = "confirm"
allowed_hosts = []
allowed_domains = []
[hooks]
enabled = true
default_timeout_ms = 30000
[tools]
declarative_enabled = true
disabled = []
allowed_write_roots = []
allow_tmp_write = false
default_timeout_ms = 120000
max_timeout_ms = 600000
env_allowlist = ["PATH", "HOME", "TMPDIR"]
stdout_limit_bytes = 65536
stderr_limit_bytes = 65536
sensitive_paths = ["~/.ssh", "~/.gnupg", "~/.aws", "~/.config/gcloud", "*.pem", "*.key", ".env", ".env.*"]
[tools.network]
allowed_hosts = []
allowed_domains = []
CLI 最小入口固定为:
1
soong-agent run [--path <dir-or-file>] [--orchestrator] [--session-id <id>] "message"
--path 缺省使用当前 cwd; 指向文件时使用父目录作为 <project>, 不自动读取该文件正文。
不提供 soong-agent init; 缺少 ${SOONG_AGENT_HOME}/config.toml 时 CLI 启动失败。
CLI 第一版权限确认使用 stdin:
- 输入
1或allow once->allow_once. - 输入
2或allow for session->allow_for_session. - 输入
3或deny->deny. - stdin 不可用、EOF、输入无效或超时时默认
deny.
第一版 CLI 不提供 --json event stream 输出; 只提供人读文本输出。
config_default.toml 默认使用 ollama + gemma4; openai / anthropic 仅作为文档说明或注释示例出现, 且示例保持通用占位符, 不写公司内部 endpoint、模型名或 key 名.
Permission decision 第一版只允许:
allow_onceallow_for_sessiondeny
不实现 always_allow / deny_for_session; allow_for_session 只在当前 session 内存生效, 不写磁盘。
Prompt And Markdown Template Assets
代码生成时不要把大段 system prompt、Plan 模板、Task 模板、内置 AgentDefinition body 硬编码进 Python 业务逻辑。代码只固定模板 ID、加载机制、注入位置和校验规则。
第一版包内资源目录建议:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
src/agent_core/assets/
prompts/
system/
core.md
tool_protocol.md
todo.md
permissions.md
multi_agent.md
memory.md
compact.md
templates/
config_default.toml
plan_default.md
task_dag_default.md
agents/
default_sub_agent.md
default_fork_agent.md
default_worker_agent.md
default_compact_agent.md
实现规则:
asset_id是代码里的稳定引用, 例如system.core,template.plan.default,agent.default_worker_agent。asset_id -> package resource path的映射可以写在代码常量中。- 具体 Markdown 正文放在资源文件里, 不写进 loop、tool registry 或 prompt composer 代码。
- 包内必需资源缺失时, SDK 启动失败或相关 tool 调用失败, 错误码使用
config_error/internal_error。 - 包内提供
template.config.default/config_default.toml作为人读默认配置模板. config_default.toml只作为模板资源提供, 不会被 CLI / runtime 自动写入${SOONG_AGENT_HOME}/config.toml.config_default.toml默认 provider 使用ollama+gemma4.config_default.toml如包含openai/anthropic注释示例, 只能使用通用占位符, 不写公司内部 endpoint、模型名或 key 名.- 模板文件可以在初始代码生成阶段放最小可用占位正文, 但必须满足:
- 文件存在。
- 非空。
- 有清晰标题。
- 不包含会误导模型执行危险操作的内容。
- metadata / event 记录 template id 和 template version。
- 占位模板示例:
1
2
3
4
5
# Default Plan Template
TODO: Replace this package asset with the final plan-writing instructions.
The model should create a concise Markdown plan under the suggested project plan directory.
- system prompt 组装只依赖
PromptBlock:block_idsourcecontentprioritydynamictoken_countmetadata
- static system blocks 从包内
prompts/system/*.md读取。 - static system blocks 还包括启动 / run 开始时根据外部 instruction 文件 frontmatter 构造出的
instruction_catalog. - dynamic system blocks 从 AgentDefinition body、已加载 instruction 正文、skill catalog、memory catalog、runtime state 构造。
- synthetic user/context blocks 从 skill load、memory recall、plan template、task template、task board summary 构造。
- Plan template tool 和 Task template tool 返回的是 synthetic user/context block, 不是直接把模板正文拼到 system prompt。
- 内置
default_compact_agent.md是 internal-only asset, 不进入agent.list_agent_definitions。 - 用户级
${SOONG_AGENT_HOME}/agents/*.md,${SOONG_AGENT_HOME}/skills/*.md, 用户级/项目级CLAUDE.md,AGENTS.md, 用户级${SOONG_AGENT_HOME}/rules/**/*.md, memory md 是外部 source of truth, 不能复制进包内 assets。 - 静态 system prompt 不是写死在 Python 业务逻辑的大字符串; 它由包内 prompt assets 加启动时扫描到的 catalog metadata 组合而成。
Template Tests
实现时至少覆盖:
- 所有 required package assets 存在且非空。
- asset loader 能通过
asset_id读取内容。 - plan template tool 把
template.plan.default写成node_type=plan_instruction。 - task template tool 把
template.task_dag.default写成node_type=task_instruction。 - static system prompt composer 能按顺序加载 core / tool_protocol / todo / permissions 等 block。
- 修改模板正文不需要修改 Python 业务代码。
Implementation Boundary
- 本章的 schema 是第一版生成代码的下限。
- 如果实现时发现某字段暂时用不到, 仍保留类型字段, 可以传空值或默认值。
- 如果实现需要新增内部字段, 放入
metadata或内部 runtime 类型, 不改变 provider/tool/storage 公开契约。 - 如果需要改变 tool canonical name、WAL event type、error code、SQLite 字段, 必须先改文档再改代码。