Skip to main content

Plan Synthesis

Plan synthesis turns a selected target tool into an executable path. It decides which arguments are already known, which fields can be supplied by collection context, which upstream producer tools can satisfy missing values, and which values must be requested from the user.

The synthesizer is deterministic and transport-agnostic. It consumes graph and tool metadata. It does not call HTTP APIs, read product databases, resolve runtime auth, or talk to a UI. Those responsibilities belong to the adapter.

Use this page when a tool was selected correctly but the system still needs to explain how to call it safely.

Where It Runs

Plan synthesis starts after target selection and ends before execution.

StageInputOutputOwned By
Intent parsinguser querytarget hint, entitiesLLM or adapter
Target selectioncandidates and evidencefinal selected targetgraph-tool-call
Plan synthesistarget, entities, graph contractsPlan or structured errorgraph-tool-call
User inputmissing slots and enum mappingsresumed input contextproduct adapter
Executionplan and runtime authrunner eventsproduct adapter + PlanRunner

If plan synthesis fails, the error should say whether the missing piece is an unknown target, an unsatisfied required field, an enum mapping, a dynamic option, a cycle, or a depth limit.

Public API

from graph_tool_call.plan import PathSynthesizer

synthesizer = PathSynthesizer(
graph_payload,
context_defaults={"locale": "ko_KR"},
enum_field_names={"statusCode"},
)

plan = synthesizer.synthesize(
target="getOrderDetail",
entities={"orderNo": "A-100"},
goal="Find order detail",
)

Input Contract

PathSynthesizer is initialized with a graph artifact and optional collection settings.

Constructor ArgumentTypePurpose
graphdictGraph artifact containing tools, metadata, and edges
max_depthintMaximum producer-chain depth. Default is 5
context_defaultsdictAmbient collection values such as locale, tenant, site, or channel
enum_field_namesset[str]Fields that should use adapter/user enum mapping instead of producer chaining

synthesize() accepts the selected target and runtime entities.

Method ArgumentTypePurpose
targetstrFinal selected tool name
entitiesdictValues extracted from the user request or supplied by the adapter
goalstrHuman-readable goal stored on the plan
exclude_toolsset[str]Optional producer tools to avoid during repair or retry

The synthesizer expects tools to include metadata.api_contract.consumes and metadata.api_contract.produces when available. It can still operate with weak metadata, but the plan will be less explainable and more likely to ask for user input.

Input Priority

For each required consume field, the synthesizer checks sources in a stable order.

PrioritySourceExample
1user or LLM-extracted entities{"orderNo": "A-100"}
2context_defaults for context fieldssiteNo, locale, channel
3producer tool with matching semantic tagmember_id produced by a search step
4producer tool with matching field nameordNo -> ordNo
5graph edge or workflow fallbackcurated producer relation
6user input fallback when allowed${user_input.statusCode}

This order keeps explicit user facts ahead of inferred graph paths and keeps collection context outside library-specific code.

Plan Shape

A synthesized plan is a serializable artifact composed of Plan and PlanStep.

from graph_tool_call.plan import Plan, PlanStep

plan = Plan(
id="plan-001",
goal="Find order detail",
steps=[
PlanStep(
id="s1",
tool="searchOrders",
args={"keyword": "A-100"},
),
PlanStep(
id="s2",
tool="getOrderDetail",
args={"orderNo": "${s1.items.0.orderNo}"},
depends_on=["s1"],
),
],
output_binding="${s2}",
metadata={
"target": "getOrderDetail",
"synthesis": {
"target": "getOrderDetail"
}
},
)

PlanStep.args may contain binding expressions:

BindingMeaning
${input.orderNo}value from input_context
${user_input.statusCode}value collected from a user slot
${s1.items.0.orderNo}value from a previous step output
${s2}whole output of step s2

PlanRunner resolves bindings at execution time. The synthesizer only declares the dependency.

Metadata To Preserve

Adapters should persist Plan.metadata.synthesis. This is the main debugging contract for plan construction.

FieldPurpose
targetFinal selected target
selected_producersProducer tools used to satisfy required fields
candidate_signalsWhy producer candidates were ranked
fallbacksFields that fell back to user input
user_input_slotsFlattened UI-ready missing input list
context_defaultsContext keys used. Do not persist secret values
enum_field_namesFields requiring enum mapping
target_selectorLLM target, final target, override diagnostics

For product logs, store field names, kinds, semantic tags, and reason codes. Do not store raw auth headers, cookies, or user identifiers.

User Input Slots

When a required value cannot be filled automatically but user input is allowed, the plan records user_input_slots.

{
"step_id": "s2",
"tool": "getOrderDetail",
"field_name": "statusCode",
"required": true,
"kind": "data",
"location": "query",
"semantic_tag": "order_status",
"reason": "enum_required"
}

Adapters can render one popup, collect values, and resume the same plan with:

for event in runner.run_stream(
plan,
input_context={"statusCode": "COMPLETE"},
):
handle(event)

The library does not prescribe UI behavior. It only exposes stable slot metadata.

Failure Reasons

PlanSynthesisError exposes to_dict() so adapters can store structured diagnostics instead of parsing exception messages.

from graph_tool_call.plan import PlanSynthesisError

try:
plan = synthesizer.synthesize(target="getOrderDetail", entities={})
except PlanSynthesisError as exc:
result = exc.to_dict()
print(result["reason"])

Common reason codes:

ReasonMeaningAdapter Response
unknown_targetTarget tool is not present in the graphrerun retrieval or inspect selector
unsatisfied_fieldRequired field cannot be filledask user, add context default, or improve producer contract
enum_requiredRequired enum needs user or adapter mappingshow enum mapping UI
dynamic_option_requiredA dynamic option list should be fetched firstcall option producer and ask user to choose
cycleProducer search revisited a tool already in progressinspect graph edges
max_depthProducer chain exceeded configured depthreduce graph noise or increase depth carefully
user_input_fallbackPlan can continue only after user inputrender user slot and resume

Structured failure reasons are part of the public contract. Use them in Quality Lab, SSE/log events, and operator diagnostics.

Dynamic Option Flow

Some fields cannot be guessed from text and should not be filled through long producer chains. Common examples are enum-like codes, product options, or UI selection values.

When a single-hop producer can fetch a useful option list, synthesis can raise a dynamic option diagnostic with:

FieldMeaning
field_namefield that needs a value
semantic_tagsemantic category of the field
producer_nametool that can fetch options
options_pathresponse path containing options
label_field_hintscandidate fields for display labels

The adapter should call the producer, show choices, and resume plan execution with the selected value.

Runner Handoff

Plan synthesis produces the plan. PlanRunner executes it through an adapter callback.

def call_tool(tool_name: str, args: dict) -> dict:
# Adapter responsibility:
# - resolve base URL
# - attach auth/session headers
# - execute HTTP/MCP/function call
# - normalize response shape
return execute_collection_tool(tool_name, args)

runner = PlanRunner(call_tool, validate_args="coerce")

events = list(
runner.run_stream(
plan,
input_context=entities,
trace_metadata={
"collection_id": collection_id,
"graph_tool_call_version": graph_tool_call.__version__,
},
)
)

Runner events should preserve plan_id, step_id, tool, stage, error kind, duration, and trace metadata. See Runner Events for the event schema.

Adapter Boundary

graph-tool-call owns deterministic plan construction. Product adapters own runtime systems.

ResponsibilityOwner
field dependency analysisgraph-tool-call
producer searchgraph-tool-call
plan artifact and synthesis metadatagraph-tool-call
auth profile lookupadapter
session/header resolutionadapter
HTTP executionadapter
user popup and resume UXadapter
persistence and audit retentionadapter

Keep product-specific DB, auth, cookies, user IDs, and SSE UI code outside the library. Pass only product-neutral graph metadata into the engine.

Quality Checks

Plan synthesis quality should be tested separately from execution. A downstream API 401 should not be counted as a plan synthesis failure.

CheckExpected Result
known targetplan final step equals selected target
entity fillexplicit entity values are used before producers
context defaultcontext fields are filled without user prompts
producer pathmissing data field creates upstream producer step
enum fieldenum mapping creates a user slot or structured error
cycle guardcyclic graph fails with cycle
max depthlong chain fails with max_depth
metadataPlan.metadata.synthesis contains target, producers, and slots

Useful commands:

poetry run pytest tests/test_plan_synthesizer.py -q
poetry run pytest tests/test_runner.py -q
poetry run pytest tests/test_graphify_contract_025.py -q

Product validation should also run Quality Lab plan cases and compare:

  • selected target
  • plan tools
  • user input slots
  • failure reason
  • synthesis latency
  • execution status only after auth readiness passes

Troubleshooting

SymptomLikely CauseWhat To Inspect
unknown_targetselector chose a name not present in the graphtarget selector output and graph artifact
required field missingentity extraction or contract coverage is weakapi_contract.consumes, user_input_slots
too many producer stepsgraph has broad structural edgesedge quality and producer candidate signals
enum values guessed incorrectlyenum field was treated as ordinary dataenum_field_names, user input slots
runner binding failureresponse shape differs from contractproducer output, response envelope, binding path
plan passes but API failsauth or downstream request issuerunner events and auth readiness

Operational Guidance

A production-ready collection should have:

  • stable target selection before synthesis
  • request and response contract coverage for important operations
  • context defaults for ambient runtime fields
  • enum mappings for fields that require operator choice
  • Quality Lab plan fixtures for high-frequency workflows
  • runner traces that preserve stage and failure reason
  • no raw secrets in plan metadata or trace records

When improving a large API collection, debug in this order:

  1. confirm the expected tool appears in retrieval Top-K
  2. inspect target selector evidence
  3. run plan synthesis with explicit entities
  4. inspect missing fields and producer candidates
  5. add context defaults or enum mappings
  6. run execution only after auth readiness passes