# FR-047 P2+P3 — Function-calling agent-loop + OpenAI-wire translator (2026-06-01)

**Commits**: `de6f79e` — v01.15.36 → v01.15.37 (agent-loop + OpenAI translator)
            `514153f` — → v01.15.40 (critical reasoning-model fix; see below)
**Parent**: `__agent/feature-requests/FR-047-phase-2-agent-loop-service-base.md` + `FR-047-phase-3-openai-fdpai-local.md`
**Pattern**: `documentations/guidelines/development/ai-function-calling.md`
**Depends on**: `@futdevpro/fsm-dynamo` ≥ 1.15.17 (tool vocabulary + registry; bumped to 1.15.18 here)

## What

Implements the provider-agnostic function-calling agent-loop in `DyNTS_AI_LLM_ServiceBase` and the
first concrete provider translator in `DyNTS_OAI_LLM_ServiceBase`. After this, OpenAI tool calling
works end-to-end through the strategic agnostic layer.

## Why

FR-047 P1 (fsm) shipped the vocabulary; the execution layer had the explicit TODO comment in
`getMessageCreateInput` ("'llm-tool-use' categoria a tools bevezetésekor — jövőbeli FR"). This is that FR.
The proven loop logic is ported from the legacy `FDPNTS_GPT_ControlService.getAnswerWithTools`.

## Design

### Agent-loop in `DyNTS_AI_LLM_ServiceBase` (written ONCE, concrete)
- `requestWithTools({ conversation, tools, toolHandlers, settings?, issuer, maxIterations? })` — public entry.
  Tools are a **request parameter** (not on settings). Resolves model → capability pre-check → `runToolLoop`.
- `runToolLoop` — the loop: `callModelWithTools` → if `toolCalls`, push assistant-tool-call message,
  run each handler (`runToolCall`), push `role:'tool'` result messages, repeat; exit on no-tool-call
  final answer or `maxIterations` (default 8 → `DyFM_Error` `DyNTS-AILSB-TL0`).
- `runToolCall` — **never throws**; missing/throwing handler → `ERROR …` string with `isError:true` (no `[object Object]`).
- `assertToolsSupported(modelId)` — uses `DyFM_AI_ModelRegistry_Util.modelSupportsTools(getModelRegistry(), id)`;
  rejects BEFORE any API call if unsupported (critical for Local).
- `callModelWithTools` + `getModelRegistry` — **non-abstract safe defaults** (throw / `[]`); providers override.
  Chosen over `abstract` so NO existing concrete subclass breaks (incremental rollout).

### OpenAI translator in `DyNTS_OAI_LLM_ServiceBase`
- `getModelRegistry()` → `DyFM_OAI_Models`.
- `callModelWithTools` — builds the native request (system + `toOpenAIMessage`-mapped conversation incl.
  tool-call/tool-result messages + `toOpenAITool`), calls the SDK, normalizes the response (`toolCalls`
  from `message.tool_calls`, args JSON-parsed), emits `'llm-tool-use'` cost-event.
- `toOpenAIMessage` — maps `role:'tool'` → `{role:'tool',tool_call_id,content}`, assistant-with-toolCalls →
  `{role:'assistant',content|null,tool_calls:[…]}`.

### Refinement vs the FR plan
The registry lookup util takes the **registry array as a param** (`(models, id)`), not `(provider, id)` —
so the fsm core never imports a provider submodule. The provider service supplies its own registry.

## CRITICAL fix (`514153f`, v01.15.40) — found in code review

The registry's tool-capable models (gpt-5.5, gpt-5.4-mini, o3-pro) are **reasoning models** that reject
`temperature`/`top_p`/`frequency_penalty`/`presence_penalty` with HTTP 400. `callModelWithTools` sent them
unconditionally (default temperature=0) → **every tool call 400'd on the flagship models**. Fix:
- `isReasoningModel(modelId)` = `/^gpt-5/ || /^o[0-9]/`; reasoning → omit the 4 sampling params.
- Omit `tools`/`tool_choice` when the tool array is empty (empty `[]` → 400).
- Clear error when no model is configured.
This is the local OpenAI-specific application of the FR-048 per-model-settings principle.
See `[[reference_oai_reasoning_models_reject_sampling_params]]`.

## Spec coverage

`oai-llm.service-base.tools.spec.ts` (mocked OpenAI client, no network) — 6 specs:
loop converges / never-throw on handler error / capability-reject for embedding model /
reasoning omits sampling params / non-reasoning sends them / empty-tools omitted.
`npx jasmine`: **1256/0** (146 pending pre-existing).

## What's NOT in this commit

- Anthropic / Google / FdpAI / Local LLM bases + translators (FR-047 P4/P5).
- The NON-tool path (`getMessageCreateInput`/`resolveMessage`) still forwards sampling params → reasoning
  completions would 400 (pre-existing; FR-048 / separate fix).
- Adventor migration (FR-047 P6).
- FR-048 general per-model settings reconciler.

## Consumer pattern

See `documentations/guidelines/development/ai-function-calling.md` ("How to USE it").

## Cross-references

- `documentations/guidelines/development/ai-function-calling.md` (pattern + provider-translator how-to)
- `NPM-packages/dynamo-fsm/__documentations/2026-06-01-fr047-p1-function-calling-vocabulary.md` (vocabulary)
- legacy `NPM-packages/fdp-templates-nts/src/_modules/gpt/_services/gpt-handler.control-service.ts` (logic source)
