AS AgentScopeAPI 手册 / API Reference
MODULE C3 · contractor · 200 XP · 35 MIN

工具契约与截断防护Tool contracts and truncation defence

先说问题The problem

模型输出被截断,tool call 的 JSON 少了一半。你的框架「好心」把它修成合法 JSON,然后真的执行了。一次半截的 write_file 变成了真实的文件覆盖。这是社区里最危险的事故形状。

The model output is truncated and the tool-call JSON loses half its body. Your framework kindly repairs it into valid JSON and executes it. A half-written write_file becomes a real overwrite. This is the most dangerous failure shape reported in the wild.

先动手Do it first

先在页面上接线——页面只懂规则。然后跑下面那条判题,那才是真会失败的东西。Wire it on the page — the bench only knows the rules. Then run the check below; that is the part that can actually fail.

工具工厂Tool factory extra_agent_tools= AgentToolFactory 选填optional

给服务构造的每个 agent 追加自定义工具,业务能力从这里进来。Adds your own tools to every agent the service builds. This is where business capability enters.

Empty: fine for now空着:以后再插也行
你拼出来的东西What you assembled
create_app(…)

为什么这样设计Why it is built this way

01工具从 extra_agent_tools 进来Tools enter through extra_agent_tools

服务给每个 agent 装配工具集的入口是 AgentToolFactory。业务能力的边界在这里,不在提示词里。

The service hands tools to every agent through an AgentToolFactory. Your business capability's boundary lives here, not in a prompt.

02参数解析失败必须是数据A bad argument must come back as data

ToolCallBlock 带 state;解析不出合法参数时停在 PENDING,而不是拿一个补全过的 dict 去调用真副作用。

ToolCallBlock carries a state. When arguments will not parse it stays PENDING, instead of calling a real side effect with a completed dict.

03工具名要能审计Tool names have to be auditable

每个工具一个动词 + 一个资源;is_read_only 决定它能不能被权限引擎放行。「万能工具」让所有闸门形同虚设。

One verb and one resource per tool; is_read_only decides whether the permission engine may let it through. A catch-all tool makes every gate decorative.

用到的 APIAPIs in play

代码The code

C3 · Python
async def refund_order(order_id: str, amount_cents: int, idempotency_key: str) -> ToolChunk:
    """Refund one order. The idempotency key is required: a replay must not double-pay."""
    if not idempotency_key.strip():
        return ToolChunk(content=[TextBlock(type="text", text="idempotency_key required")],
                         state=ToolResultState.ERROR)
    ...
    return ToolChunk(content=[TextBlock(type="text", text="refunded")],
                     state=ToolResultState.SUCCESS)

toolkit = Toolkit(tools=[FunctionTool(refund_order, is_read_only=False)])
# await toolkit.add_tool(...) is the other entry point — there is no toolkit.register().
# Returning a ToolResponse (or anything that is not a ToolChunk) does not work: the
# toolkit either str()-ifies it into a nested repr or raises
# "The tool function must return a ToolChunk object".

判题The check

<span class="ui-zh">运行这条</span><span class="ui-en">Run this</span>
python lab.py run c3

它断言了什么What it asserts

  • schema requires order_id/amount_cents/idempotency_key
  • complete call -> one ledger row
  • truncated json -> state=error, ledger rows unchanged
  • missing idempotency_key -> refused, not defaulted
  • the lenient tool accepted the same shape: ledger now holds 2 rows
  • same key replayed -> refunded_cents 0, idempotent_replay true
  • read-only flags separate the readers from the movers
  • factory binds the tenant: lookup_order/read_policy/refund_order/write_back_note

会踩的坑Where it hurts

  • 在工具里做「宽容解析」(缺字段就补默认值)等于把截断变成 mutation。缺字段要报错回去,让模型重试,而不是自己猜。

    Lenient parsing inside a tool — filling defaults for missing fields — converts a truncation into a mutation. Error back so the model retries; never guess.

费曼闸门Say it back

为什么「修好半截 JSON 再执行」比「直接失败」危险得多?用一句能被反驳的话说清代价。

Why is repairing half a JSON and executing far more dangerous than failing outright? State the cost in one falsifiable sentence.

页面只存通行码的哈希。判题没真跑过,这里就解不开。The page stores a hash, not the code. Nothing unlocks until the command really ran.

基于官方固定版本源码静态生成Generated from pinned official sources · Python 2.0.11 · Java 2.0.4
签名与文档为静态抽取;如有疑问请以源码为准。Signatures and docs are extracted statically; verify against the source when in doubt.