Skip to content

Tell the program what your tools return

Lesson 3 showed a program that read reading.temperature_celsius from a tool that returns temp_c. Lesson 2 showed Mistral reading temp_limit_c from a tool that returns max_temp_c, and reporting a confident zero.

Two different models, the same mistake, so it is not carelessness. They guessed because nothing in front of them named the field.

A direct tool call and a callTool are told different things. This lesson prints both.

UC AI writes a list of your tools into the description of uc_ai__run_code. That list is the catalog, and it is everything a program is told. Print it:

l_tools := uc_ai_tools_api.get_tools_array(
p_provider => uc_ai.c_provider_anthropic
, p_tool_tags => apex_t_varchar2('coldchain')
, p_enable_tools => true
, p_programmatic_tools => true
);

The catalog part of that description reads:

Callable tools (await callTool code -> arguments):
await callTool("CC_LIST_SHIPMENTS", { product_class }) // List every cold-chain shipment with its numeric id, its shipment number, its carrier, its lane and its product class.
await callTool("CC_GET_READINGS", { shipment_id }) // Return the temperature readings of ONE shipment, in time order.
await callTool("CC_GET_LIMITS", { }) // Return the temperature limit and the claim rule of each product class.

Each catalog line contains the tool code, argument names, and description.

What is not there:

  • no types, and no required flags
  • no JSON schema, and no parameter descriptions from it
  • nothing at all about the shape of the result

A direct tool call gets the JSON schema, which holds a description for each parameter. A callTool gets a bare name. Neither of them documents what the tool returns. For a program, the description is the only place that fact can live.

Every description now names the fields the tool returns:

l_tool_id := uc_ai_tools_api.merge_tool_from_schema(
p_tool_code => 'CC_GET_READINGS'
, p_description => 'Return the temperature readings of ONE shipment, in time order. '
|| 'Argument shipment_id is the numeric shipment_id from CC_LIST_SHIPMENTS, '
|| 'not the shipment number. Returns { shipment_id, readings } where readings '
|| 'is an array of { ts, temp_c }, about 49 rows. temp_c is degrees Celsius '
|| 'and ts is the time of the reading.'
, p_function_call => 'return cc_analyst_pkg.get_readings(:ARGUMENTS);'
, p_json_schema => json_object_t('{ ... }')
, p_tags => apex_t_varchar2('coldchain')
, p_code_mode_access => 'code'
);

Two rules produce a description a program can use:

  1. Name the fields of the result. Returns { shipment_id, readings } where readings is an array of { ts, temp_c }. Nothing else can tell it.
  2. Say what an argument is, not what it is called. shipment_id is the numeric shipment_id from CC_LIST_SHIPMENTS, not the shipment number is the difference between a program that works and one that passes "SHP-2047" to a number.

The same call changed something else. p_code_mode_access is now 'code':

ValueMeaning
directA normal tool only. A program cannot call it.
codeNot in the tool list of the model. Only a program can call it.
bothAvailable either way. This is the default for a new tool.

CC_LIST_SHIPMENTS and CC_GET_READINGS are now code. CC_GET_LIMITS stays both, because it returns two small rows and a person can reasonably ask about it.

Tools the model can call directly:
CC_GET_LIMITS
uc_ai__run_code

The 48 KB of readings can no longer reach the context window at all, because no path exists that puts them there.

run program calls in out trips secs finish_reason
------------------------------------------------------------------------------------
classic, 40 calls no 26 34413 3723 3 46.6 stop
code mode yes 5 20280 3380 5 48 stop
code, bulk hidden yes 1 3981 991 2 15.4 stop

One tool call. 3981 input tokens. 15.4 seconds.

Against the classic run in the first row: 26 times fewer tool calls, 8.6 times fewer input tokens, and three times faster. The answer is the same:

Recorded answerclaude-sonnet-4-615.4s2026-08-25
ShipmentClassCarrierBreach DurationClaim (EUR)
SHP-2047FROZENMeridian Cargo150 min€720
SHP-2052CHILLEDNordfracht90 min€240
SHP-2061FROZENBaltic Reefer210 min€960

RESULT: SHP-2047, SHP-2052, SHP-2061 | 1920

Your wording will differ. What must match is the data, and the checks below.

And the program now reads the right fields:

const readings = readingResults[i].readings;
const { max_temp_c, sample_minutes, max_minutes_above, claim_per_hour_eur } = limit;
for (const reading of readings) {
if (reading.temp_c > max_temp_c) {
runLength++;
} else {

Not one word of the prompt changed between lesson 2 and this run. The descriptions changed.

The whole catalog has to fit in the description of one tool, so UC AI gives it about 31 KB. One description cannot fill that alone, because uc_ai_tools.description holds at most 4000 characters. About eight descriptions of that size do fill it.

The section The catalog has a size limit in 04_catalog.sql registers nine filler tools with 3900 characters of padding each, and then counts what survived:

preamble + catalog: 29082 characters
code-callable tools: 12
in the catalog: 10
LEFT OUT: 2 (CC_FILLER_2, CC_FILLER_3)
Nothing failed, and no tool in that last line is callable
from a program. The log holds the only warning.
The NAMES on that line are not predictable. Another database
with the same tools leaves out a different pair.

Twelve tools are code-callable and ten reached the program. Two did not, and a program can no longer call them.

The first number counts the fixed preamble of about 900 characters as well as the catalog. So it stays under the budget even though UC AI dropped two tools. UC AI skips a tool that does not fit and carries on with the next one. The space left at the end is therefore smaller than one more description.

The run did not fail, the model saw no error, and the only record is one line in the log:

Code-mode catalog size limit (31855 bytes) reached: 2 tool(s) left out and NOT
callable from a program. Shorten the descriptions of code-mode tools to fit more
of them in.

Three ways to stay inside the limit:

  • Write short descriptions. Name the fields, and stop.
  • Set a tool that a program never calls to direct. It then costs the catalog nothing.
  • Use tags. A run that needs four tools must not carry forty.
select code, code_mode_access, active
from uc_ai_tools
where code like 'CC\_%' escape '\'
order by code;
CODE CODE_MODE_ACCESS ACTIVE
CC_GET_LIMITS both 1
CC_GET_READINGS code 1
CC_LIST_SHIPMENTS code 1

Then print the tool names the model can call directly. CC_GET_READINGS must not be among them, and uc_ai__run_code must be.

  • The catalog holds tool codes, argument names and descriptions. It holds no types, no schema, and nothing about the result.
  • For a code tool, the description is the whole API reference. Name the fields it returns and say what each argument means.
  • code_mode_access of code removes a bulk tool from the context window completely. That is where most of the saving is.
  • merge_tool_from_schema keeps the access value it is not given. State it explicitly in a deployment script.
  • The catalog is capped near 31 KB. UC AI drops the tools that do not fit and writes one warning to the log. Which tools it drops is not defined.

Full reference: Choosing which tools are available where.