Skip to content

Commit 27bf734

Browse files
committed
fix(di,tests,docs): honor host-registered stores over portable Null defaults (Exists includeInterfaceTypes); align 3 stale tests with intended wave-2..5 behavior; refresh docs for 5.7.0
1 parent 2fc8cb9 commit 27bf734

14 files changed

Lines changed: 85 additions & 19 deletions

Assets/CoreAI/Docs/LLM_ROUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@ Production games such as RedoSchool should put provider keys and quota enforceme
4343

4444
- **Orchestrator / chat window:** `ICoreAISettings.LlmRequestTimeoutSeconds` is enforced by `CoreAiChatService` (`CancelAfterSlim`, WebGL-safe) for both streaming and non-streaming chat calls.
4545
- **HTTP per request:** `IOpenAiHttpSettings.RequestTimeoutSeconds` caps a single `MeaiOpenAiChatClient` round-trip. On Unity, `CoreAISettingsAsset.EffectiveHttpRequestTimeoutSeconds` applies `min(RequestTimeoutSeconds, ceil(LlmRequestTimeoutSeconds))` so the transport does not outlive the orchestrator cancel window (see [`MEAI_TOKENS_FACT_VS_ESTIMATE.md`](MEAI_TOKENS_FACT_VS_ESTIMATE.md), §3).
46-
- **Typed timeout vs cancel:** When only the library timeout fires, callers may receive `LlmOperationTimeoutException`. `RoutingLlmClient` publishes `LlmRequestCompleted` with `LlmErrorCode.Timeout` vs `Cancelled` for non-streaming failures; streaming may still surface a terminal `LlmStreamChunk` with `Error = "cancelled"` when lower layers normalize cancellation (see [`MEAI_TOKENS_FACT_VS_ESTIMATE.md`](MEAI_TOKENS_FACT_VS_ESTIMATE.md), §4).
46+
- **Typed timeout vs cancel:** When only the library timeout fires, callers may receive `LlmOperationTimeoutException`. `RoutingLlmClient` publishes `LlmRequestCompleted` with `LlmErrorCode.Timeout` vs `Cancelled`. Transport/internal timeouts — including a header phase that never completes and the timeout decorator's own linked token — surface as typed `Timeout` on both streaming and non-streaming paths; `Cancelled` is reported only when the caller actually cancelled, so timeouts stay retry/fallback-eligible (see [`MEAI_TOKENS_FACT_VS_ESTIMATE.md`](MEAI_TOKENS_FACT_VS_ESTIMATE.md), §4).
47+
- **Retry/fallback replay guard:** a failed completion that carries evidence of an **executed** tool call is not replayed by the HTTP retry loop or the fallback chain — the failure propagates instead of re-mutating the world. Rejected tool calls (duplicate-suppressed, parse errors, unknown tool names, schema/argument-conversion failures) are treated as never-invoked and do not block retries or fallback.
4748
- **Usage accounting:** `LlmUsageRecord` / `ILlmUsageSink` and `LlmUsageReported` (MessagePipe) complement routing; token counts from HTTP usage are described in [`MEAI_TOKENS_FACT_VS_ESTIMATE.md`](MEAI_TOKENS_FACT_VS_ESTIMATE.md).

Assets/CoreAI/Docs/MEAI_TOKENS_FACT_VS_ESTIMATE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ OpenAI-style servers may emit a **final** SSE object with an empty `choices` arr
3131

3232
If the provider does not send `usage` while streaming, facts may exist only on the non-streaming path—or not at all, depending on the server.
3333

34+
**Multi-roundtrip (tool-calling) turns:** usage on the terminal result is **cumulative across the whole turn** — streaming usage is summed over every tool roundtrip, and `PromptTokens + CompletionTokens == TotalTokens` holds for cost/usage consumers. The width of the context actually sent on the **final roundtrip** is reported separately as `LastRoundtripPromptTokens` (used for prompt-size calibration; providers that emit zero usage cannot pollute it).
35+
3436
---
3537

3638
## 3. Two timeouts: orchestrator and HTTP
@@ -51,7 +53,7 @@ so a single HTTP call **cannot outlive** the orchestrator cancel (important for
5153
- If **only** the chat timeout token fires (`timeoutCts`) and the **outer** user `ct` is **not** cancelled, `CoreAiChatService` throws **`LlmOperationTimeoutException`** (subclass of `OperationCanceledException`) to distinguish library timeout from explicit user cancel.
5254
- **`RoutingLlmClient`** maps non-streaming failures to `LlmRequestCompleted` with **`LlmErrorCode.Timeout`** vs **`Cancelled`** based on exception type.
5355

54-
**Streaming:** decorators such as `LoggingLlmClientDecorator` may normalize cancel/timeout into a **terminal** `LlmStreamChunk` with an error field like **`"cancelled"`**. Then **`LlmErrorCode.Timeout` on `LlmRequestCompleted` for streaming is not guaranteed** end-to-end unless `LlmOperationTimeoutException` propagates through every layer. For UI, see patterns like `ResolveTimeoutMessage` on `CoreAiChatPanel` (empty message = do not duplicate a system line).
56+
**Transport/internal timeouts are typed `Timeout`, never `Cancelled`:** a transport-internal timeout (e.g. a backend that never sends response headers) or the timeout decorator's own linked token firing surfaces as a typed timeout on **both** the streaming and non-streaming paths — an inner `Cancelled` result/terminal chunk caused by the decorator's timeout is reclassified to `Timeout`, and `TimeoutException` maps to `LlmErrorCode.Timeout`. `Cancelled` is reported **only when the caller's own token was cancelled**, so timeouts stay retry/fallback-eligible while explicit user cancels are never retried. For UI, see patterns like `ResolveTimeoutMessage` on `CoreAiChatPanel` (empty message = do not duplicate a system line).
5557

5658
---
5759

Assets/CoreAI/Docs/TOOL_CALLING_BEST_PRACTICES.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ transport retries:
7272
Example: `set_wave_modifier(id, value)` is easier to retry safely than
7373
`increase_wave_modifier(delta)`.
7474

75+
CoreAI's resilience layer also guarantees retries never double-execute tools: a
76+
failed completion whose turn already **executed** a tool call is not replayed by
77+
the HTTP retry loop or the fallback provider chain. Rejected calls
78+
(duplicate-suppressed, parse errors, unknown tool names, argument-conversion
79+
failures) are treated as never-invoked and do not block retries.
80+
7581
## Result Envelope
7682

7783
Use the same result vocabulary across tools where practical:
@@ -159,6 +165,14 @@ If you add a new state-mutating built-in, add its name to
159165
- Do not return stack traces, secrets, local file paths, or provider responses.
160166
- Do not hide domain failures as successful prose.
161167

168+
**Exceptions in `DelegateLlmTool` bodies never escape the pipeline.** If your
169+
delegate body throws, `DelegateLlmTool` converts the exception into an
170+
`"Error: ..."` tool result (matching first-party tools), so the model sees a
171+
repairable error message and the turn continues instead of the request faulting.
172+
Cancellation is the exception: `OperationCanceledException` from the caller's
173+
token propagates. Treat this as a safety net, not the primary error channel —
174+
still return structured `error` codes for expected domain failures.
175+
162176
## Roundtrip Limits
163177

164178
One **roundtrip** = one LLM call + one tool-execution batch. The cap (`MaxToolCallRoundtrips`,

Assets/CoreAIBenchmark/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ The window has four tabs, plus toolbar **Open folder** / **Open report** shortcu
8888

8989
| Tab | Purpose |
9090
|---|---|
91-
| Run | Choose model/base URL overrides, scenario groups (G1-G8), repetitions, retries, timeout override, and start a run. |
91+
| Run | Choose model/base URL overrides, scenario groups (G1-G8), repetitions, retries, timeout override, and start a run. While a run is active a **Stop (save partial)** button appears: it takes effect at retry boundaries (no waiting out every timeout attempt), saves a partial report, and stop-aborted attempts are never scored as model failures. |
9292
| History | Browse past runs grouped by model, inspect dimension/role scores, open reports, and view captured scene thumbnails. |
9393
| Models | A sortable leaderboard of the newest run per model, ranked by suite score, speed, pass-rate, or game-fit. |
9494
| Compare | Select the newest JSON reports per model, optionally pin one model first, and build `COMPARISON.md` plus `COMPARISON.svg`. |

Assets/CoreAiUnity/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ Unity host: **CoreAI.Source** build, EditMode / PlayMode tests, Editor menus, do
88

99
### Fixed (2026-07-12 audit wave 5 — adversarial review of wave 4, host)
1010

11+
- **Portable Null defaults no longer shadow host-registered stores in DI.** `RegisterCorePortable`'s
12+
`Exists` guards passed the default `includeInterfaceTypes: false`, so they never matched a store the
13+
host registered via `.As<IInterface>()` — the Null default was registered unconditionally and won the
14+
single resolve. `IGameConfigStore` (the wave-4 guard was a silent no-op), `ILuaScriptVersionStore`,
15+
and `IDataOverlayVersionStore` are now guarded with `includeInterfaceTypes: true`, so a host's real
16+
config/version stores are honored (verified by the full EditMode suite, 1532 tests green).
1117
- **World state: pending-parent ownership is claimed only on a successful load** — a failed/parse-error
1218
`TryLoad` (or one on a disposed manager) no longer steals the static owner slot from the live
1319
manager, so explicit detaches keep routing to the right pending map.

Assets/CoreAiUnity/Docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ If the game adds a **child** `LifetimeScope` (VContainer parent = `CoreAILifetim
7474

7575
`IAiPromptContextProvider` lets a game append per-request context such as current quest, lesson slot, learner profile, or world objective without mutating the static role prompt. `AiPromptComposer` appends these sections under `## Runtime Context`.
7676

77-
`ScopedAgentMemoryStoreDecorator` and `IAgentMemoryScopeProvider` let projects isolate memory by tenant, user, session, topic, and role while preserving the old role-only key when no scope provider is registered.
77+
`ScopedAgentMemoryStoreDecorator` and `IAgentMemoryScopeProvider` let projects isolate memory by tenant, user, session, topic, and role while preserving the old role-only key when no scope provider is registered. Lossless scope ids (including GUIDs) keep their legacy on-disk keys; a lossy id (one that sanitization would alter, or that mimics the hashed shape) gets a raw-length prefix plus a `-<12 hex>` hash suffix of the trimmed id, so distinct raw scopes can never collide into the same memory bucket.
7878

7979
`IConversationContextManager` prepares long chat history before each LLM call. The default `DeterministicConversationContextManager` keeps recent messages in `ChatHistory` and compacts older turns into a `## Conversation Summary` system section using `IConversationSummaryStore`. **`RegisterCorePortable`** registers **`InMemoryConversationSummaryStore`** by default so summaries accumulate across turns for each role for the process lifetime. **`IContextBudgetPolicy`** (`DefaultContextBudgetPolicy`) plus **`ITokenEstimator`** (`HeuristicTokenEstimator`) allocate a **`HistoryTokenBudget`** from the role/context window minus reserved completion headroom and an estimate of system + user + tool-contract text — this replaces the legacy fixed `ContextTokens/2` split.
8080

Assets/CoreAiUnity/Docs/AUDIT_LOG.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,13 @@ that (modulo the `hash` field) end up on disk — there is nothing hidden from t
9090
To verify: use `AuditLogVerifier.Verify(filePath)`. It re-chains from genesis (`prevHash = ""`) by,
9191
for each line: parsing it, checking the stored `prevHash` equals the running chain head, blanking
9292
the `hash` field, recomputing `SHA256(runningPrevHash + preimage)`, and comparing against the
93-
stored `hash`. It returns `{ Ok, FirstBrokenSeq, LineCount, Error }``Ok` is false at the first
94-
line whose `prevHash` or `hash` doesn't match, or that fails to parse at all (e.g. a truncated
95-
tail line), and `FirstBrokenSeq` identifies it. `AuditLogVerifier.ReadAll(filePath)` returns the
93+
stored `hash`. It returns `{ Ok, FirstBrokenSeq, LineCount, ChainResetCount, Error }``Ok` is
94+
false at the first line whose `prevHash` or `hash` doesn't match, or that fails to parse at all
95+
(e.g. a truncated tail line), and `FirstBrokenSeq` identifies it. A legitimate `ChainReset` entry
96+
is accepted as the start of a new chain segment instead of failing verification, but every
97+
mid-file reset is counted in `ChainResetCount` and reported with a warning — a self-hashed
98+
`ChainReset` line could otherwise hide tail truncation, so resets are operator-visible, never
99+
silent. `AuditLogVerifier.ReadAll(filePath)` returns the
96100
parsed `AuditEntry` list for inspection without verifying the chain.
97101

98102
Any modification to any entry (including its `ts`) breaks the chain for that entry and all
@@ -142,6 +146,9 @@ detects the existing trailing `RotationMarker` and reuses its hash instead of ap
142146
so the chain can never reference a record that isn't actually on disk.
143147
- `Dispose()` drains the entire queue (not just one batch), bounded by a ~2s deadline so a stuck
144148
disk can't hang shutdown forever.
149+
- The writer's lifetime is **scope-owned**: it is created and disposed with its DI lifetime scope,
150+
so scene reloads cannot leak background flush loops writing the same file concurrently, and the
151+
tail is flushed on scope teardown.
145152
- All flush entry points (the 500ms timer, `Dispose()`, and the test-only `FlushForTesting()`) share
146153
one lock so they can never interleave and corrupt the chain head.
147154
- Rotation at 50 MB → `audit_0001.jsonl`, `audit_0002.jsonl`, etc. (see above).

Assets/CoreAiUnity/Docs/COREAI_SETTINGS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ These fields live in the **Advanced** inspector under **Chat history summarizati
257257
|------|-------------|----------|
258258
| **Enable history summarization** || When off, the full loaded chat transcript is kept in the MEAI tail without rolling older turns into `## Conversation Summary` (may exceed the model context). |
259259
| **Recent history token budget override** | `0` | `0` = automatic from context window minus system/tools/user (via `DefaultContextBudgetPolicy`). When set to a positive value, caps the verbatim tail to that many **estimated** tokens; older lines fold into the rolling summary when summarization is on (minimum applied: 32). |
260-
| **Max rolled summary (tokens)** | `0` | `0` = no extra cap. When set, truncates the persisted rolling summary to roughly that many estimated tokens after each rollup (deterministic bullet path and LLM-assisted path). |
260+
| **Max rolled summary (tokens)** | `2048` | `0` = unlimited (no cap). When set, truncates the persisted rolling summary to roughly that many estimated tokens after each rollup (deterministic bullet path and LLM-assisted path), keeping the newest content and evicting the oldest. `2048` is the default only for fresh installs. |
261261
| **Compaction trigger ratio** | `0.8` | Roadmap §2. Compaction checks estimated history tokens against `historyBudget * ratio`; below the trigger, all turns stay verbatim and the stored summary is not rewritten. Invalid values fall back to the CoreAI default threshold. |
262262
| **Enable context pruning** || Roadmap §7 context editing. Before summarization, prunes only the in-memory prompt history copy: exact consecutive duplicates are collapsed and stale `tool` / `## Tool Results` observations are omitted. Durable stored chat history is unchanged. |
263263
| **Max retained tool results** | `3` | Newest durable `tool` / `## Tool Results` messages retained in the prompt history copy before compaction. Older tool observations are treated as superseded by newer turns. |
@@ -422,6 +422,8 @@ settings.ConfigureHttpApi("http://localhost:1234/v1", "", "qwen3.5-4b");
422422

423423
Without extra setup, **`RegisterCorePortable()`** wires **`InMemoryConversationSummaryStore`**: older turns that no longer fit the token budget become a deterministic **`## Conversation Summary`** block, and summaries **accumulate in memory per role** for the app process.
424424

425+
The **stored** summary's final line is a `[fold:v1:<hashes>]` fold marker (content hashes of the last folded messages) that records how far history has already been folded. It is internal bookkeeping: it is stamped after the token cap (so the limiter can never trim it) and stripped from every LLM- and UI-facing view of the summary.
426+
425427
Unity scenes using **`CoreAILifetimeScope`** switch to **`FileConversationSummaryStore`** under **`%persistentDataPath%/CoreAI/ConversationSummaries`** so compaction survives restarts, then **`RegisterCorePortable(suppressDefaultConversationSummaryStore: true, suppressDefaultAgentMemoryStore: true)`** (**v1.5.22** adds agent-memory suppression so **`IAgentMemoryStore`** is not double-registered).
426428

427429
This is separate from **`FileAgentMemoryStore`** transcript JSON; orchestration details are in [ARCHITECTURE.md](ARCHITECTURE.md) and [DEVELOPER_GUIDE.md](DEVELOPER_GUIDE.md).

Assets/CoreAiUnity/Docs/MemorySystem.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ Granular edits operate on the canonical `AgentMemoryState.Memory` document that
4444

4545
Every successful memory mutation records a bounded audit snapshot on `AgentMemoryState.Versions`: version number, UTC timestamp, action, full `contentAfter`, and a short size-delta note. Stores that preserve the full `AgentMemoryState` retain those snapshots; custom stores can persist them alongside `Memory`. Use `IAgentMemoryStore.ListVersions(roleId)` to inspect retained snapshots and `IAgentMemoryStore.Revert(roleId, version)` to restore one; revert itself creates a new version.
4646

47+
Two levels of "clear": the model-facing `memory(action=clear)` wipes only the memory **document** — the wipe is versioned and revertible, and chat history/transcripts survive, so the model cannot erase the user's conversation record. The store-level `IAgentMemoryStore.Clear(roleId)` is a full reset: it also wipes the version history and the system-prompt snapshot, so cleared memory can never be re-injected into system prompts.
48+
4749
**When to use:**
4850
- ✅ CoreMechanicAI — craft history
4951
- ✅ Creator — design decisions

Assets/CoreAiUnity/Docs/WORLD_COMMANDS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ Net effect: a mod that re-spawns "its" objects on load never races the snapshot
207207
| `px, py, pz` | World position |
208208
| `rx, ry, rz` | Euler rotation |
209209
| `sx, sy, sz` | Local scale |
210-
| `parent` | Parent object name (empty if root) |
210+
| `parent` | Parent object name (empty if root). A child whose parent's prefab is currently unresolved keeps its remembered parent id across autosaves until the prefab returns (it is not re-saved as a root orphan); an explicit `parent: none` detach or reparent clears the remembered link, so the old parent is not resurrected. |
211211
| `active` | Whether the object is enabled |
212212
| `cr, cg, cb, ca` | **Optional** — only present if `set_color` was called on the object; otherwise `-1` (sentinel). Prefabs without explicit `set_color` keep their original material. |
213213

0 commit comments

Comments
 (0)