mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-30 10:09:19 -04:00
193 lines
8.2 KiB
Markdown
193 lines
8.2 KiB
Markdown
# ToolLoopRunner — implementation plan
|
||
|
||
Status: plan for "variant C" (2026-06-13). Supersedes step 5 of
|
||
`context-architecture.md` §6.
|
||
|
||
Context that shapes this plan:
|
||
- The tool loop STAYS in LLMQore — the library remains a complete standalone
|
||
agentic client. Variant C changes its *shape*, not its home: the loop
|
||
becomes a named class, `BaseClient` slims toward transport.
|
||
- 2026-06-12 the variant-A hook (`setContinuationPayloadBuilder` + Session
|
||
feeding assembler-built continuation bodies) was implemented and then
|
||
REVERTED by the project owner: the frozen-replay problem was judged
|
||
contrived (replay carries the full filtered history of its base payload;
|
||
mid-loop file changes reach the model via tool results; growth is bounded
|
||
by `maxToolContinuations`). The reverted llmqore diff is saved at
|
||
`/tmp/llmqore-continuation-builder.patch`.
|
||
- Therefore this plan has two tracks. **Track 1 (the actual ask): the
|
||
structural refactoring.** Track 2 (host payload source) is OPTIONAL,
|
||
parked, and only happens if the 2026-06-12 verdict is explicitly reversed.
|
||
- The context-architecture steps 1–4 implementation (ContextAssembler,
|
||
content cache, pinned providers, EnvBlockFormatter, ~1200 lines incl.
|
||
tests) is parked in `stash@{0}` ("new context refactor") on
|
||
`dev-release-1-0`. It is NOT required for track 1.
|
||
|
||
---
|
||
|
||
## 1. Current anatomy (llmqore @ 0348ac8)
|
||
|
||
- `BaseClient` mixes two responsibilities:
|
||
- **transport** — HTTP/SSE per request, `ActiveRequest { stream, buffers,
|
||
url, mode, usage, … }`, accumulation in protocol subclasses;
|
||
- **loop policy** — `ActiveRequest.originalPayload`,
|
||
`ActiveRequest.continuationCount`, `m_maxToolContinuations`,
|
||
`checkContinuationLimit`, `handleToolContinuation`.
|
||
- Loop entry: protocol clients call `executeToolsFromMessage(id)` at their
|
||
message-end detection points (11 call sites across 7 clients); it forwards
|
||
`tool_use` blocks to `ToolsManager::executeToolCall`.
|
||
- `BaseClient::tools()` wires `ToolsManager::toolExecutionComplete(id,
|
||
results)` → `handleToolContinuation`: round-limit check → continuation
|
||
body via the protocol-virtual `buildContinuationPayload(originalPayload,
|
||
message, toolResults)` → `finalizeTurn` → `sendRequest(id, storedUrl,
|
||
payload, storedMode)`.
|
||
|
||
## 2. Target design
|
||
|
||
### 2.1 ToolLoopRunner (new, llmqore)
|
||
|
||
```cpp
|
||
class LLMQORE_EXPORT ToolLoopRunner : public QObject
|
||
{
|
||
Q_OBJECT
|
||
public:
|
||
explicit ToolLoopRunner(BaseClient *client);
|
||
|
||
int maxRounds() const noexcept;
|
||
void setMaxRounds(int limit) noexcept;
|
||
|
||
private:
|
||
void onToolsCompleted(const RequestID &id,
|
||
const QHash<QString, ToolResult> &results);
|
||
void onRequestClosed(const RequestID &id);
|
||
|
||
struct LoopState
|
||
{
|
||
int rounds = 0;
|
||
};
|
||
|
||
BaseClient *m_client = nullptr;
|
||
QHash<RequestID, LoopState> m_loops;
|
||
int m_maxRounds = 10;
|
||
};
|
||
```
|
||
|
||
The whole loop policy on one screen:
|
||
|
||
```cpp
|
||
void ToolLoopRunner::onToolsCompleted(const RequestID &id,
|
||
const QHash<QString, ToolResult> &results)
|
||
{
|
||
auto &loop = m_loops[id];
|
||
if (++loop.rounds > m_maxRounds) {
|
||
m_client->abortRequest(id, "Tool continuation limit reached");
|
||
m_loops.remove(id);
|
||
return;
|
||
}
|
||
|
||
const QJsonObject payload = m_client->buildReplayContinuation(id, results);
|
||
if (payload.isEmpty()) {
|
||
m_client->abortRequest(id, "Failed to build continuation payload");
|
||
m_loops.remove(id);
|
||
return;
|
||
}
|
||
|
||
m_client->continueRequest(id, payload);
|
||
}
|
||
```
|
||
|
||
- `LoopState` is keyed by request id — several concurrent requests on one
|
||
client (two chat panels on one provider) never collide.
|
||
- Cleanup: `onRequestClosed` (connected to `requestFailed` +
|
||
`requestFinalized`) drops the state.
|
||
|
||
### 2.2 BaseClient becomes transport + tool dispatch
|
||
|
||
Gains (transport primitives; `continueRequest` public — it is also the seam
|
||
any future host-driven mode would use; failure path via runner friendship):
|
||
|
||
```cpp
|
||
ToolLoopRunner *toolLoop(); // owned, created with tools()
|
||
void continueRequest(const RequestID &id, const QJsonObject &payload);
|
||
// finalizeTurn + resend stored url/mode
|
||
QJsonObject buildReplayContinuation(const RequestID &id,
|
||
const QHash<QString, ToolResult> &results);
|
||
// originalPayload + protocol virtual
|
||
```
|
||
|
||
Loses (moves to the runner): `handleToolContinuation`,
|
||
`checkContinuationLimit`, `m_maxToolContinuations`,
|
||
`ActiveRequest::continuationCount`. The `toolExecutionComplete` connection
|
||
in `tools()` retargets to the runner.
|
||
|
||
Keeps: `executeToolsFromMessage` (the 11 protocol call sites stay
|
||
untouched), the protocol-virtual `buildContinuationPayload` (it IS the
|
||
replay serialization), `originalPayload` storage,
|
||
`setMaxToolContinuations`/`maxToolContinuations` as thin forwarders to
|
||
`toolLoop()` — existing consumers (QodeAssist `ClientInterface`,
|
||
`QuickRefactorHandler`, third parties) compile unchanged.
|
||
|
||
## 3. Track 1 — structural refactoring (the plan)
|
||
|
||
Bit-identical behavior throughout; QodeAssist only needs a submodule bump.
|
||
|
||
**Phase 1 — transport primitives.** Add `continueRequest` +
|
||
`buildReplayContinuation` + public `abortRequest` (now also the body of
|
||
`cancelRequest`). — DONE 2026-06-13.
|
||
|
||
**Phase 2 — extract the runner.** New `ToolLoopRunner` class; move round
|
||
state + limit; retarget the `toolExecutionComplete` connection; delete
|
||
`handleToolContinuation` / `checkContinuationLimit` /
|
||
`ActiveRequest::continuationCount`; forwarders for
|
||
`setMaxToolContinuations`. — DONE 2026-06-13
|
||
(`include/LLMQore/ToolLoopRunner.hpp`, `source/core/ToolLoopRunner.cpp`,
|
||
`tests/tst_ToolLoopRunner.cpp` — 7 cases: replay flow, round limit, missing
|
||
replay data, two interleaved ids, cleanup on finalize/cancel, forwarders;
|
||
`continueRequest` is virtual as the test seam; llmqore architecture docs
|
||
updated: overview, request-lifecycle diagram, tools).
|
||
|
||
Deliberate behavior delta (an improvement, worth knowing while testing): an
|
||
empty payload from the protocol's `buildContinuationPayload` now aborts the
|
||
request with "Missing data for tool continuation" instead of silently
|
||
sending an empty body.
|
||
|
||
**Phase 3 — submodule bump (after the user runs llmqore tests).**
|
||
QodeAssist: bump the submodule pointer, verify live in the plugin (Ollama +
|
||
tools, Claude + tools); update `context-architecture.md`
|
||
§4.3/§6.5 to point here; update project memory.
|
||
|
||
## 4. Track 2 — host payload source (PARKED)
|
||
|
||
Only if the 2026-06-12 "проблема надумана" verdict is explicitly reversed.
|
||
Variant C makes it a ~40-line addition, so nothing is lost by parking:
|
||
|
||
- `ToolLoopRunner::setPayloadSource(id, std::function<QJsonObject(const
|
||
RequestID &)>)`; registered source is authoritative for its id (empty
|
||
result → abort, never silent fallback to replay).
|
||
- Host prerequisite: restore the context work from `stash@{0}`
|
||
(ContextAssembler + `Session::makePayload`); expect conflicts in
|
||
`Session.cpp` with the newer `dev-release-1-0` refactor commits
|
||
("Remove override tools in Session send" etc.).
|
||
- Session registers the source after `provider->sendRequest` (same-thread,
|
||
race-free; `QPointer` guard).
|
||
- Assembler continuation rules: pinned blocks anchor to the turn's TYPED
|
||
user message (recorded 2026-06-12), manifest per round.
|
||
|
||
## 5. Risks
|
||
|
||
| Risk | Mitigation |
|
||
|---|---|
|
||
| Behavior drift while moving the loop | phases are mechanical; same `buildContinuationPayload` virtuals; llmqore tests + plugin smoke before/after |
|
||
| Two sessions, one client | `LoopState` keyed by request id |
|
||
| Qt 5 compatibility (0348ac8) | runner uses only signals/`QHash`/`std::function` — no Qt 6-only API |
|
||
| Cancel mid-tool-execution | unchanged: `cancelRequest` → `failRequest` → `onRequestClosed` clears state; `ToolsManager::cleanupRequest` handles in-flight tools |
|
||
| Google (model in URL) | `continueRequest` reuses stored per-request url/mode — same as today |
|
||
|
||
## 6. Deliberately not doing
|
||
|
||
- Not moving the loop or tool execution out of llmqore
|
||
(`feedback_llmqore_boundary`).
|
||
- Not touching the 11 `executeToolsFromMessage` call sites or the protocol
|
||
`buildContinuationPayload` implementations.
|
||
- No Auto/Manual mode flags.
|
||
- Track 2 is not started without an explicit decision.
|