Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/tools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,11 @@ export function getToolDefinitions() {
type: "string",
description: "Token ID of the NFT to transfer",
},
fromAddress: {
type: "string",
description:
"Optional 0x address of the NFT owner to transfer from. Omit it to transfer from the agent's own wallet; set it only for a token owned by another address that has approved this account.",
},
},
required: ["to", "contractAddress", "tokenId"],
},
Expand Down
52 changes: 52 additions & 0 deletions test/native-tools-no-model.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,58 @@ describe("Native tool-calling — schema validation", () => {
assert.deepEqual(getTokenBal.parameters.required, ["token"]);
assert.ok(getTokenBal.parameters.properties.token);
});

test("transfer_nft declares optional fromAddress (issue #112)", () => {
const tools = getToolDefinitions();
const transfer = tools.find((t) => t.name === "transfer_nft");
assert.ok(transfer, "transfer_nft tool not found");

const { properties, required } = transfer.parameters;
assert.deepEqual(
new Set(required),
new Set(["to", "contractAddress", "tokenId"]),
"fromAddress must stay out of required"
);
assert.ok(properties.fromAddress, "transfer_nft schema is missing optional fromAddress");
assert.equal(properties.fromAddress.type, "string", "fromAddress must be a string property");
assert.ok(
!required.includes("fromAddress"),
"fromAddress must not be required — the prompt advertises it as optional"
);
// The prompt promises a fallback when it is omitted, so the schema must too.
assert.ok(properties.fromAddress.description, "fromAddress needs a description");
});

test("no tool schema drifts from ACTIONS args + optionalArgs", () => {
// getToolDefinitions() is hand-written while systemPrompt() and
// nativeSystemPrompt() are generated from ACTIONS, so the two surfaces can
// disagree (issue #112). Every tool must declare exactly the arguments its
// prompt line advertises, with optionalArgs optional rather than required.
// hasRequiredArgs() in src/tools.mjs lets these two actions run with no
// arguments at all, so their `args` are opt-in and stay out of `required`.
const noRequiredArgs = new Set(["account", "get_nfts"]);

for (const tool of getToolDefinitions()) {
const spec = ACTIONS[tool.name];
assert.ok(spec, `${tool.name} has no ACTIONS entry`);
const optional = spec.optionalArgs ?? [];
const expected = [...spec.args, ...optional];
const expectedRequired = noRequiredArgs.has(tool.name)
? []
: spec.args.filter((arg) => !optional.includes(arg));

assert.deepEqual(
Object.keys(tool.parameters.properties).sort(),
expected.sort(),
`${tool.name} properties drifted from ACTIONS`
);
assert.deepEqual(
[...tool.parameters.required].sort(),
expectedRequired.sort(),
`${tool.name} required drifted from ACTIONS`
);
}
});
});

// ─────────────────────────────────────────────────────────────────────────────
Expand Down
Loading