From a0c9d5ba5c3edc429f9d2ba1745f53eafc47b5bf Mon Sep 17 00:00:00 2001 From: zerob13 Date: Mon, 15 Jun 2026 12:29:18 +0800 Subject: [PATCH 1/6] docs: add tape systems design --- .../deepchat-tape-view-manifest/plan.md | 179 ++++++++++++++ .../deepchat-tape-view-manifest/spec.md | 229 ++++++++++++++++++ .../deepchat-tape-view-manifest/tasks.md | 28 +++ docs/architecture/deepchat_tape_spec_v1.md | 115 +++++++++ 4 files changed, 551 insertions(+) create mode 100644 docs/architecture/deepchat-tape-view-manifest/plan.md create mode 100644 docs/architecture/deepchat-tape-view-manifest/spec.md create mode 100644 docs/architecture/deepchat-tape-view-manifest/tasks.md create mode 100644 docs/architecture/deepchat_tape_spec_v1.md diff --git a/docs/architecture/deepchat-tape-view-manifest/plan.md b/docs/architecture/deepchat-tape-view-manifest/plan.md new file mode 100644 index 000000000..632b36d0f --- /dev/null +++ b/docs/architecture/deepchat-tape-view-manifest/plan.md @@ -0,0 +1,179 @@ +# DeepChat Tape ViewManifest Shadow Mode - Plan + +## Architecture Decision + +Use the existing `DeepChatTapeService` and `deepchat_tape_entries` table. The first increment adds a +shadow manifest layer that observes context construction and request preflight results. It records +metadata as tape events and leaves production message assembly unchanged. + +`DeepChatTapeService` remains the single Tape service boundary. + +## Event Flow + +```text +AgentRuntimePresenter.processMessage() + -> tapeService.ensureSessionTapeReady() + -> messageStore.createUserMessage() + -> buildContext() + -> tapeViewManifestService.assembleInitialManifest() + -> tapeService.appendViewManifest() + -> messageStore.createAssistantMessage() + -> runStreamForMessage() + -> processStream() + -> coreStream(requestMessages, requestTools) + -> preflightRequestContext() + -> optional recoverRequestContextPressure() + -> tapeViewManifestService.assembleRequestManifest() + -> tapeService.appendViewManifest() + -> provider.coreStream() + -> optional request trace persists with matching requestSeq +``` + +Resume flow uses the same service with `taskType = "resume"` and `policy = "resume_shadow"`. + +## Module Changes + +| Module | Change | +| --- | --- | +| `src/shared/types/agent-interface.d.ts` or a new shared type file | Add public manifest result types for route/UI use. | +| `src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts` | New pure assembler for manifest metadata and hashes. | +| `src/main/presenter/agentRuntimePresenter/tapeService.ts` | Append and list `view/assembled` events. | +| `src/main/presenter/agentRuntimePresenter/index.ts` | Call manifest assembly at initial context and request-level preflight points. | +| `src/shared/contracts/routes/sessions.routes.ts` | Add route for manifests by message ID. | +| `src/renderer/api/SessionClient.ts` | Add `listMessageViewManifests(messageId)`. | +| `src/renderer/src/components/trace/TraceDialog.vue` | Add tabs for Request, View Manifest, Tape Entries, and Budget. | + +## Data Model + +Phase 1 stores manifests as tape events. This avoids a schema migration and uses the existing source +index: + +```text +deepchat_tape_entries + session_id = session id + kind = event + name = view/assembled + source_type = runtime_event + source_id = assistant message id + source_seq = request sequence + payload_json.data.manifest = DeepChatTapeViewManifest +``` + +The route resolves `messageId -> sessionId`, then reads matching tape events. + +## Request Sequence + +`runStreamForMessage()` owns an in-memory request sequence counter for the assistant message: + +```text +assistant message m1 + requestSeq 1 -> initial provider request + requestSeq 2 -> provider request after a tool result + requestSeq 3 -> provider request after another tool result +``` + +The same sequence is used for the manifest and the request trace. If trace debug is disabled, the +manifest still records the sequence. + +## Hashing + +- `promptHash`: stable hash of provider-bound `ChatMessage[]` after preflight and recovery. +- `toolDefinitionsHash`: stable hash of provider-bound tool definitions. +- `manifestHash`: stable hash of the manifest with `manifestHash` omitted. + +Use deterministic JSON stringification for hash inputs. + +## Exclusion Reason Rules + +| Reason | Source | +| --- | --- | +| `before_summary_cursor` | Message order is below `summaryCursorOrderSeq`. | +| `compaction_indicator` | Message metadata marks a compaction indicator. | +| `pending_not_context_history` | Message status is pending outside the resume target. | +| `out_of_budget` | Turn was eligible but removed by token-budget selection. | +| `empty_after_formatting` | Record formatting produced zero provider messages. | +| `superseded` | Effective tape view replaced an older fact revision. | +| `retracted` | Effective tape view removed a message through a retraction event. | + +## UI Layout + +```text +TraceDialog ++-------------------------------------------------------------------+ +| Header | +| Title | ++-------------------------------------------------------------------+ +| Trace selector | ++-------------------------------------------------------------------+ +| Tabs: Request | View Manifest | Tape Entries | Budget | ++-------------------------------------------------------------------+ +| Request tab | +| Endpoint / provider / model | +| JSON editor | ++-------------------------------------------------------------------+ +| View Manifest tab | +| View id / policy / request seq | +| Included and excluded entry list | ++-------------------------------------------------------------------+ +| Tape Entries tab | +| Matching event and anchor refs | ++-------------------------------------------------------------------+ +| Budget tab | +| Context length / max tokens / estimated prompt tokens | ++-------------------------------------------------------------------+ +| Footer | ++-------------------------------------------------------------------+ +``` + +The dialog remains compact and developer-focused, with explanatory copy kept to empty and error +states. + +## Test Strategy + +| Test area | Required checks | +| --- | --- | +| Manifest assembler | Includes selected records, excludes cursor/budget drops, hashes deterministically. | +| Context parity | Existing `buildContext()` messages match manifest included refs for normal chat. | +| Resume parity | `buildResumeContext()` target and protected tail are represented. | +| Request sequence | Tool-loop provider calls create monotonically increasing request manifests. | +| Tape service | Append/list manifest events by message ID and request sequence. | +| Route/client | Legacy messages with an absent manifest return an empty list. | +| Renderer | Trace dialog renders Request tab and View Manifest empty/data states. | + +## Rollout + +1. Land shadow manifest assembly and tape persistence as headless runtime metadata. +2. Add route/client and tests. +3. Add trace-dialog tabs. +4. Add context parity coverage. +5. Use collected manifest data to design the later `TapeViewAssembler` production path. + +## Risks and Mitigations + +| Risk | Mitigation | +| --- | --- | +| Manifest diverges from actual provider request after preflight recovery. | Append request-level manifest after preflight and recovery for provider-bound context. | +| Storage growth from per-request events. | Store metadata and hashes only; avoid raw content duplication. | +| UI fails on old traces. | Treat missing manifest as an empty state. | +| Request sequence drifts from trace sequence. | Runtime owns the sequence and passes it to both manifest and trace persistence. | +| Context mapping misses synthetic system/new-user messages. | Represent them with `source = "synthetic"` and `entryId = null`. | + +## Verification Commands + +Run focused tests during implementation: + +```bash +pnpm vitest run test/main/presenter/agentRuntimePresenter/contextBuilder.test.ts +pnpm vitest run test/main/presenter/agentRuntimePresenter/tapeService.test.ts +pnpm vitest run test/main/presenter/sqlitePresenter/deepchatTapeEntriesTable.test.ts +pnpm vitest run test/renderer/components/trace/TraceDialog.test.ts +``` + +Before completing the feature, run: + +```bash +pnpm run format +pnpm run i18n +pnpm run lint +pnpm run typecheck +``` diff --git a/docs/architecture/deepchat-tape-view-manifest/spec.md b/docs/architecture/deepchat-tape-view-manifest/spec.md new file mode 100644 index 000000000..148d9b954 --- /dev/null +++ b/docs/architecture/deepchat-tape-view-manifest/spec.md @@ -0,0 +1,229 @@ +# DeepChat Tape ViewManifest Shadow Mode - Spec + +Status: active SDD. This goal prepares implementation. Code changes start only after this spec, +plan, and task list are accepted. + +## Problem + +DeepChat already persists append-only tape facts and request traces. The runtime lacks a persisted +context-selection decision for each LLM request. Debugging a long session currently requires +correlating message rows, compaction anchors, context-budget behavior, and provider request JSON by +hand. + +`ViewManifest` shadow mode records that decision while keeping the existing context builder as the +production path. + +## Current Code Baseline + +| Area | Current file | Role in this goal | +| --- | --- | --- | +| Tape table | `src/main/presenter/sqlitePresenter/tables/deepchatTapeEntries.ts` | Stores append-only events and anchors. | +| Tape service | `src/main/presenter/agentRuntimePresenter/tapeService.ts` | Existing Tape boundary; new manifest methods belong here or next to it. | +| Effective tape view | `src/main/presenter/agentRuntimePresenter/tapeEffectiveView.ts` | Reconstructs current message facts from tape entries. | +| Context builder | `src/main/presenter/agentRuntimePresenter/contextBuilder.ts` | Production path that shadow manifests must describe. | +| Runtime send path | `src/main/presenter/agentRuntimePresenter/index.ts` | Calls `ensureSessionTapeReady()`, `buildContext()`, and `runStreamForMessage()`. | +| Message trace | `src/main/presenter/sqlitePresenter/tables/deepchatMessageTraces.ts` | Existing request trace storage shown in the renderer. | +| Trace dialog | `src/renderer/src/components/trace/TraceDialog.vue` | First UI surface for ViewManifest inspection. | + +## Goals + +1. Persist one `ViewManifest` for every DeepChat LLM request attempt. +2. Keep `buildContext()` and request preflight behavior unchanged. +3. Explain included and excluded conversation facts with stable IDs and reasons. +4. Link each manifest to the assistant message and request sequence. +5. Support trace-dialog inspection while keeping raw prompt text out of the manifest. +6. Create parity tests that compare existing context output to manifest metadata. + +## Deferred Scope + +- Replacing `buildContext()` with a new assembler. +- Introducing a second TapeStore abstraction. +- Migrating old sessions eagerly. +- Adding embedding memory, topic clustering, or cross-session recall. +- Running live LLM replay in CI. +- Storing raw provider request bodies in tape events. + +## User Stories + +- As a developer, I can open a traced assistant message and see why each context entry was included + or excluded. +- As a maintainer, I can change context selection code and run tests that detect manifest/context + divergence. +- As an agent-runtime debugger, I can inspect the latest anchor, summary cursor, and token budget + used for a request. +- As a privacy-conscious user, I get manifest metadata while raw prompt content stays in its current + storage path. + +## Acceptance Criteria + +1. A normal chat turn appends a `view/assembled` tape event before the provider request is sent. +2. A resume turn appends a `view/assembled` tape event with `taskType = "resume"`. +3. A tool-loop provider request appends a request-level manifest with a monotonic `requestSeq` for + the assistant message. +4. If context-pressure recovery changes the provider request messages, a new manifest revision is + appended with `policy = "context_pressure_recovery_shadow"`. +5. The manifest records selected message IDs, source tape entry IDs when available, excluded message + IDs, exclusion reasons, token-budget inputs, prompt hash, and tool-definition hash. +6. The manifest stores IDs, hashes, token estimates, policies, and reasons. Raw user text, raw + assistant text, raw tool output, image data, audio data, file content, API headers, and API keys + stay in existing storage paths. +7. Trace UI can show Request and View Manifest tabs for a traced assistant message. +8. Existing trace behavior remains compatible when a manifest is absent. +9. Tests prove that the selected history represented by the manifest matches `buildContext()` for + normal chat and resume paths. +10. Tests prove request-level manifest ordering across tool-loop provider calls. + +## ViewManifest Contract + +The first version is a shadow contract. It describes what the current runtime did. + +```ts +export type DeepChatTapeViewManifest = { + schemaVersion: 1 + viewId: string + sessionId: string + messageId: string + requestSeq: number + + taskType: 'chat' | 'resume' | 'tool_loop' + policy: + | 'legacy_context_shadow' + | 'resume_shadow' + | 'tool_loop_shadow' + | 'context_pressure_recovery_shadow' + + contextBuilderVersion: 'legacy-v1' + latestEntryId: number + anchorEntryIds: number[] + + included: DeepChatTapeViewEntryRef[] + excluded: DeepChatTapeViewExcludedRef[] + + tokenBudget: { + contextLength: number + requestedMaxTokens: number + effectiveMaxTokens: number + reserveTokens: number + toolReserveTokens: number + estimatedPromptTokens: number + } + + hashes: { + promptHash: string + toolDefinitionsHash: string + manifestHash: string + } + + meta: { + providerId: string + modelId: string + summaryCursorOrderSeq: number + supportsVision: boolean + supportsAudioInput: boolean + traceDebugEnabled: boolean + } + + assembledAt: number +} + +export type DeepChatTapeViewEntryRef = { + entryId: number | null + messageId: string | null + orderSeq: number | null + role: 'system' | 'user' | 'assistant' | 'tool' | null + source: 'tape' | 'synthetic' + reason: + | 'system_prompt' + | 'selected_history' + | 'new_user_input' + | 'resume_target' + | 'tool_loop_message' +} + +export type DeepChatTapeViewExcludedRef = { + entryId: number | null + messageId: string | null + orderSeq: number | null + reason: + | 'before_summary_cursor' + | 'compaction_indicator' + | 'pending_not_context_history' + | 'out_of_budget' + | 'empty_after_formatting' + | 'superseded' + | 'retracted' +} +``` + +## Persistence Contract + +Manifests are append-only tape events: + +```json +{ + "kind": "event", + "name": "view/assembled", + "source_type": "runtime_event", + "source_id": "", + "source_seq": 1, + "payload_json": { + "name": "view/assembled", + "data": { + "manifest": "" + } + } +} +``` + +The manifest lookup key is `(sessionId, messageId, requestSeq)`. Existing tape indexes support this +through `source_type`, `source_id`, and `source_seq`. + +## UI Contract + +The first UI increment extends the existing trace dialog. + +```text ++-------------------------------------------------------------------+ +| Trace #1 Request | View Manifest | Tape Entries | Budget | ++-------------------------------------------------------------------+ +| View view_01 Policy legacy_context_shadow | +| Anchor #42 Summary cursor 17 | ++------------------+------------------------------------------------+ +| Included | #43 user order=17 selected_history | +| | #44 assistant order=18 selected_history | +| Excluded | #1 user order=1 before_summary_cursor | +| | #29 assistant order=12 out_of_budget | ++------------------+------------------------------------------------+ +``` + +States: + +- Loading: dialog shows the existing spinner. +- Empty: Request tab stays available, View Manifest tab shows an empty state for this trace. +- Error: Request tab stays available during View Manifest loading failures. +- Legacy trace: the manifest tab explains that older traces have empty manifest state. + +## Privacy and Security + +- Store hashes and IDs in the manifest. +- Store raw provider request previews only in `deepchat_message_traces`. +- Reuse existing redaction for request traces. +- Keep file contents, image data, audio data, tool output, headers, and prompts in their existing + storage paths. + +## Compatibility + +- Old sessions keep lazy backfill through `DeepChatTapeService.ensureSessionTapeReady()`. +- Old traces render with an empty manifest state. +- Missing tape table remains a supported fallback for existing runtime code. +- The manifest feature preserves chat generation behavior, request ordering, and token-budget + decisions. + +## Success Criteria + +- Shadow manifest generation is covered by unit tests for normal chat, resume, and tool-loop + request sequencing. +- Trace UI can display manifest data when present and degrade cleanly when absent. +- `buildContext()` output remains unchanged in existing tests. +- The SDD can support a later replacement phase where a real `TapeViewAssembler` becomes the + production path. diff --git a/docs/architecture/deepchat-tape-view-manifest/tasks.md b/docs/architecture/deepchat-tape-view-manifest/tasks.md new file mode 100644 index 000000000..51851ea44 --- /dev/null +++ b/docs/architecture/deepchat-tape-view-manifest/tasks.md @@ -0,0 +1,28 @@ +# DeepChat Tape ViewManifest Shadow Mode - Tasks + +## Documentation + +- [x] T0: Create SDD folder and convert the broad Tape vision into the current implementation + direction. + +## Implementation Tasks + +- [ ] T1: Add `DeepChatTapeViewManifest` shared types and a pure manifest hashing helper. +- [ ] T2: Add `tapeViewManifest.ts` with pure assembly helpers for normal chat, resume, and + request-level provider calls. +- [ ] T3: Extend `DeepChatTapeService` to append and list `view/assembled` events. +- [ ] T4: Emit initial shadow manifests after `buildContext()` and `buildResumeContext()`. +- [ ] T5: Emit request-level manifests inside `runStreamForMessage()` after preflight and + context-pressure recovery. +- [ ] T6: Add typed route and renderer client method for manifests by message ID. +- [ ] T7: Extend `TraceDialog.vue` with Request, View Manifest, Tape Entries, and Budget tabs. +- [ ] T8: Add unit tests for manifest assembly, tape service list/append behavior, and route/client + compatibility. +- [ ] T9: Add renderer tests for manifest tab loading, empty, error, and data states. +- [ ] T10: Run format, i18n, lint, typecheck, and focused test suites. + +## Follow-up Tasks + +- [ ] F1: Evaluate whether manifest storage should get a dedicated table after real usage data. +- [ ] F2: Design production `TapeViewAssembler` replacement only after shadow parity is stable. +- [ ] F3: Define eval/replay export format from manifest slices. diff --git a/docs/architecture/deepchat_tape_spec_v1.md b/docs/architecture/deepchat_tape_spec_v1.md new file mode 100644 index 000000000..c07add812 --- /dev/null +++ b/docs/architecture/deepchat_tape_spec_v1.md @@ -0,0 +1,115 @@ +# DeepChat Tape System - Implementation Baseline + +Status: current implementation direction. The active SDD goal is +[deepchat-tape-view-manifest](deepchat-tape-view-manifest/spec.md). + +This document keeps the Tape vision aligned with the current DeepChat codebase. The implementation +path is: + +```text +Existing DeepChat runtime + -> existing DeepChatTapeService + -> ViewManifest shadow mode + -> Inspector and replay contracts + -> later policy replacement +``` + +## Current Baseline + +DeepChat already has the main Tape primitives. + +| Tape concept | Current owner | Notes | +| --- | --- | --- | +| Tape store | `DeepChatTapeEntriesTable` | Append-only `deepchat_tape_entries` with per-session monotonic `entry_id`. | +| Tape service | `DeepChatTapeService` | Backfills message facts, exposes info/search/anchors/handoff/fork metadata. | +| Message facts | `DeepChatMessageStore` + `tapeFacts.ts` | User, assistant, tool call, tool result, replacement, and retraction facts. | +| Anchor | `kind = "anchor"` entries | `session/start`, `compaction/*`, `handoff/*`, `auto_handoff/*`, `fork/start`. | +| Effective view | `tapeEffectiveView.ts` | Reconstructs current message records from append-only facts. | +| Context build | `contextBuilder.ts` | Current production context assembler and token-budget selector. | +| Request trace | `deepchat_message_traces` | Stores redacted provider request previews for the trace dialog. | +| Agent tools | `agentTapeTools.ts` | Exposes `tape_info`, `tape_search`, `tape_anchors`, `tape_handoff`. | + +The first implementation step uses this baseline as the single runtime path. `DeepChatTapeService` +remains the Tape service boundary. + +## Active SDD + +The active SDD folder is: + +```text +docs/architecture/deepchat-tape-view-manifest/ +├── spec.md +├── plan.md +└── tasks.md +``` + +The SDD scope is `Existing TapeService + ViewManifest shadow mode`. + +## Scope Boundary + +### In scope + +- Generate a `ViewManifest` for each DeepChat LLM request while `buildContext` remains the source + of model messages. +- Persist manifests as `view/assembled` tape events. +- Link manifests to request traces by `messageId` and request sequence. +- Add Inspector support that explains included/excluded context entries. +- Add parity tests proving shadow manifests describe the same context that the existing runtime + sends. + +### Deferred scope for the first increment + +- Replacing `buildContext` as the production context builder. +- Creating a separate TapeStore abstraction. +- Memory graph retrieval, embedding-backed topic clustering, and cross-session recall. +- Live LLM replay in CI. +- Full eval pipeline and training exports. + +## Implementation Rules + +1. Keep `DeepChatTapeService` as the Tape service boundary. +2. Store manifest data as append-only tape events. +3. Keep raw prompt and provider request bodies in existing trace storage only. +4. Store IDs, hashes, token estimates, policy names, and exclusion reasons in the manifest. +5. Keep old sessions compatible through existing lazy backfill and bootstrap anchors. +6. Treat `ViewManifest` as an explanation and regression artifact until parity is proven. + +## Target Flow + +```text +sendMessage / resume + -> ensureSessionTapeReady() + -> buildContext() + -> assemble ViewManifest shadow event + -> runStreamForMessage() + -> preflight provider request + -> assemble request-level ViewManifest revision if context changed + -> provider.coreStream() + -> optional request trace linked to ViewManifest + -> message/tool facts appended +``` + +## Inspector Shape + +```text ++-------------------------------------------------------------------+ +| Trace #1 Request | View Manifest | Tape Entries | Budget | ++-------------------------------------------------------------------+ +| Provider openai Model gpt-4.1 | +| View view_01 Policy legacy_context_shadow | ++-----------------------------+-------------------------------------+ +| Included | message/user #12 | +| | message/assistant #13 | +| Excluded | #1-#8 compressed by anchor #42 | +| Budget | 23k estimated / 64k context | ++-----------------------------+-------------------------------------+ +``` + +## Expected Benefits + +- Every LLM request can explain which conversation facts were included. +- Context compaction and handoff behavior becomes auditable through anchor and manifest metadata. +- Trace debugging gains policy-level context instead of only raw request JSON. +- Future context-policy changes get a parity baseline. +- Evaluation and replay can be derived from existing runtime facts after the manifest contract is + stable. From a32c02922949f9050d4add45ba0cc083c4581468 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Mon, 15 Jun 2026 14:02:05 +0800 Subject: [PATCH 2/6] feat(tape): implement view manifest flow --- .../deepchat-tape-policy-provenance/plan.md | 52 +++ .../deepchat-tape-policy-provenance/spec.md | 56 +++ .../deepchat-tape-policy-provenance/tasks.md | 11 + .../deepchat-tape-policy-selector/plan.md | 50 +++ .../deepchat-tape-policy-selector/spec.md | 54 +++ .../deepchat-tape-policy-selector/tasks.md | 10 + .../deepchat-tape-replay-contract/plan.md | 58 +++ .../deepchat-tape-replay-contract/spec.md | 86 ++++ .../deepchat-tape-replay-contract/tasks.md | 10 + .../deepchat-tape-view-assembler/plan.md | 59 +++ .../deepchat-tape-view-assembler/spec.md | 56 +++ .../deepchat-tape-view-assembler/tasks.md | 9 + .../deepchat-tape-view-manifest/plan.md | 18 +- .../deepchat-tape-view-manifest/spec.md | 16 +- .../deepchat-tape-view-manifest/tasks.md | 20 +- .../deepchat-tape-view-policy/plan.md | 49 +++ .../deepchat-tape-view-policy/spec.md | 49 +++ .../deepchat-tape-view-policy/tasks.md | 9 + docs/architecture/deepchat_tape_spec_v1.md | 65 ++- .../agentRuntimePresenter/contextBuilder.ts | 197 ++++++++- .../presenter/agentRuntimePresenter/index.ts | 224 +++++++++- .../agentRuntimePresenter/tapeService.ts | 302 ++++++++++++++ .../tapeViewAssembler.ts | 85 ++++ .../agentRuntimePresenter/tapeViewManifest.ts | 270 ++++++++++++ .../agentRuntimePresenter/tapeViewPolicy.ts | 148 +++++++ .../presenter/agentSessionPresenter/index.ts | 44 ++ src/main/routes/index.ts | 15 +- src/renderer/api/SessionClient.ts | 33 ++ .../components/ProviderConfigImportDialog.vue | 2 +- .../mcp-config/AgentMcpSelector.vue | 4 +- .../src/components/trace/TraceDialog.vue | 386 +++++++++++++++--- src/renderer/src/i18n/da-DK/traceDialog.json | 37 +- src/renderer/src/i18n/de-DE/traceDialog.json | 37 +- src/renderer/src/i18n/en-US/traceDialog.json | 37 +- src/renderer/src/i18n/es-ES/traceDialog.json | 37 +- src/renderer/src/i18n/fa-IR/traceDialog.json | 37 +- src/renderer/src/i18n/fr-FR/traceDialog.json | 37 +- src/renderer/src/i18n/he-IL/traceDialog.json | 37 +- src/renderer/src/i18n/id-ID/traceDialog.json | 37 +- src/renderer/src/i18n/it-IT/traceDialog.json | 37 +- src/renderer/src/i18n/ja-JP/traceDialog.json | 37 +- src/renderer/src/i18n/ko-KR/traceDialog.json | 37 +- src/renderer/src/i18n/ms-MY/traceDialog.json | 37 +- src/renderer/src/i18n/pl-PL/traceDialog.json | 37 +- src/renderer/src/i18n/pt-BR/traceDialog.json | 37 +- src/renderer/src/i18n/ru-RU/traceDialog.json | 37 +- src/renderer/src/i18n/tr-TR/traceDialog.json | 37 +- src/renderer/src/i18n/vi-VN/traceDialog.json | 37 +- src/renderer/src/i18n/zh-CN/traceDialog.json | 37 +- src/renderer/src/i18n/zh-HK/traceDialog.json | 37 +- src/renderer/src/i18n/zh-TW/traceDialog.json | 37 +- src/shared/contracts/routes.ts | 6 +- .../contracts/routes/sessions.routes.ts | 31 +- src/shared/types/agent-interface.d.ts | 15 + .../presenters/agent-session.presenter.d.ts | 7 + src/shared/types/tape-replay.ts | 63 +++ src/shared/types/tape-view-manifest.ts | 97 +++++ .../agentRuntimePresenter.test.ts | 74 ++++ .../agentRuntimePresenter/tapeService.test.ts | 272 ++++++++++++ .../tapeViewAssembler.test.ts | 226 ++++++++++ .../tapeViewManifest.test.ts | 159 ++++++++ .../tapeViewPolicy.test.ts | 180 ++++++++ .../presenterCallErrorHandler.test.ts | 13 +- .../components/trace/TraceDialog.test.ts | 170 ++++++-- 64 files changed, 4309 insertions(+), 191 deletions(-) create mode 100644 docs/architecture/deepchat-tape-policy-provenance/plan.md create mode 100644 docs/architecture/deepchat-tape-policy-provenance/spec.md create mode 100644 docs/architecture/deepchat-tape-policy-provenance/tasks.md create mode 100644 docs/architecture/deepchat-tape-policy-selector/plan.md create mode 100644 docs/architecture/deepchat-tape-policy-selector/spec.md create mode 100644 docs/architecture/deepchat-tape-policy-selector/tasks.md create mode 100644 docs/architecture/deepchat-tape-replay-contract/plan.md create mode 100644 docs/architecture/deepchat-tape-replay-contract/spec.md create mode 100644 docs/architecture/deepchat-tape-replay-contract/tasks.md create mode 100644 docs/architecture/deepchat-tape-view-assembler/plan.md create mode 100644 docs/architecture/deepchat-tape-view-assembler/spec.md create mode 100644 docs/architecture/deepchat-tape-view-assembler/tasks.md create mode 100644 docs/architecture/deepchat-tape-view-policy/plan.md create mode 100644 docs/architecture/deepchat-tape-view-policy/spec.md create mode 100644 docs/architecture/deepchat-tape-view-policy/tasks.md create mode 100644 src/main/presenter/agentRuntimePresenter/tapeViewAssembler.ts create mode 100644 src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts create mode 100644 src/main/presenter/agentRuntimePresenter/tapeViewPolicy.ts create mode 100644 src/shared/types/tape-replay.ts create mode 100644 src/shared/types/tape-view-manifest.ts create mode 100644 test/main/presenter/agentRuntimePresenter/tapeViewAssembler.test.ts create mode 100644 test/main/presenter/agentRuntimePresenter/tapeViewManifest.test.ts create mode 100644 test/main/presenter/agentRuntimePresenter/tapeViewPolicy.test.ts diff --git a/docs/architecture/deepchat-tape-policy-provenance/plan.md b/docs/architecture/deepchat-tape-policy-provenance/plan.md new file mode 100644 index 000000000..865c05c64 --- /dev/null +++ b/docs/architecture/deepchat-tape-policy-provenance/plan.md @@ -0,0 +1,52 @@ +# DeepChat Tape Policy Provenance - Plan + +## Architecture Decision + +Use the existing `TapeViewAssemblerResult.policyId` and `policyVersion` as the source of truth for +initial chat and resume manifests. Keep request-level tool-loop and context-pressure recovery +manifests as shadow policies because they are post-assembly request transformations. + +## Flow + +```text +TapeViewAssembler.buildTapeChatView() + -> result.policyId = legacy_context_v1 + -> runStreamForMessage(viewContext.policy = result.policyId) + -> ViewManifest.policy = legacy_context_v1 + -> ViewManifest.policyVersion = 1 + +Tool loop / context pressure recovery + -> request-level ViewManifest + -> shadow policy label + -> policyVersion = null +``` + +## Module Changes + +| Module | Change | +| --- | --- | +| `src/shared/types/tape-view-manifest.ts` | Add `legacy_context_v1` and `policyVersion`. | +| `src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts` | Persist policy version in manifest. | +| `src/main/presenter/agentRuntimePresenter/index.ts` | Pass assembler policy id/version into view context. | +| `src/main/presenter/agentRuntimePresenter/tapeService.ts` | Store policy version in event metadata. | +| `src/renderer/src/components/trace/TraceDialog.vue` | Show policy version when present. | +| Tests | Update manifest, service, and trace expectations. | + +## Compatibility + +- Existing shadow policy strings remain accepted by shared types and UI. +- Old manifests without `policyVersion` continue to render; the field is treated as absent/null. +- Replay slice export keeps its current lookup behavior. + +## Verification + +```bash +pnpm vitest run test/main/presenter/agentRuntimePresenter/tapeViewManifest.test.ts +pnpm vitest run test/main/presenter/agentRuntimePresenter/tapeService.test.ts +pnpm vitest run test/renderer/components/trace/TraceDialog.test.ts +pnpm run format +pnpm run i18n +pnpm run lint +pnpm run typecheck:node +pnpm run typecheck:web +``` diff --git a/docs/architecture/deepchat-tape-policy-provenance/spec.md b/docs/architecture/deepchat-tape-policy-provenance/spec.md new file mode 100644 index 000000000..9b514f33b --- /dev/null +++ b/docs/architecture/deepchat-tape-policy-provenance/spec.md @@ -0,0 +1,56 @@ +# DeepChat Tape Policy Provenance - Spec + +Status: implemented SDD. This goal records the active Tape view policy in every ViewManifest. + +## Problem + +`TapeViewAssembler` returns `policyId` and `policyVersion`, but `ViewManifest.policy` still stores +the older shadow labels such as `legacy_context_shadow` and `resume_shadow`. This weakens the Tape +audit trail because the persisted manifest does not identify the actual `TapeViewPolicy` that +selected the initial chat or resume context. + +## Goals + +1. Store the active `TapeViewPolicy` id in `ViewManifest.policy` for initial chat and resume + requests. +2. Store the active policy version in `ViewManifest.policyVersion`. +3. Preserve existing `tool_loop_shadow`, `context_pressure_recovery_shadow`, and legacy shadow + policy labels for compatibility. +4. Show policy version in TraceDialog when present. +5. Keep replay export and manifest lookup compatible with old manifest records. + +## Non-Goals + +- Changing the default policy away from `legacy_context_v1`. +- Adding user-facing policy selection. +- Changing token-budget selection. +- Rewriting tool-loop or context-pressure recovery selection. + +## Acceptance Criteria + +1. Normal chat manifests use `policy = "legacy_context_v1"` and `policyVersion = 1`. +2. Resume manifests use `policy = "legacy_context_v1"` and `policyVersion = 1`. +3. Tool-loop manifests keep `policy = "tool_loop_shadow"` and `policyVersion = null`. +4. Context-pressure recovery manifests keep `policy = "context_pressure_recovery_shadow"` and + `policyVersion = null`. +5. Existing shadow policy values remain valid manifest values. +6. TraceDialog displays policy version when the manifest includes one. +7. Manifest, runtime, trace UI, replay, lint, typecheck, and targeted tests pass. + +## Contract + +```ts +export type DeepChatTapeViewPolicy = + | 'legacy_context_v1' + | 'legacy_context_shadow' + | 'resume_shadow' + | 'tool_loop_shadow' + | 'context_pressure_recovery_shadow' + +export interface DeepChatTapeViewManifest { + policy: DeepChatTapeViewPolicy + policyVersion: number | null +} +``` + +`legacy_context_shadow` and `resume_shadow` are accepted for older persisted manifests only. diff --git a/docs/architecture/deepchat-tape-policy-provenance/tasks.md b/docs/architecture/deepchat-tape-policy-provenance/tasks.md new file mode 100644 index 000000000..d44286542 --- /dev/null +++ b/docs/architecture/deepchat-tape-policy-provenance/tasks.md @@ -0,0 +1,11 @@ +# DeepChat Tape Policy Provenance - Tasks + +## Implementation Tasks + +- [x] T1: Add `legacy_context_v1` and `policyVersion` to the ViewManifest contract. +- [x] T2: Pass assembler policy id/version into chat and resume ViewManifest creation. +- [x] T3: Keep tool-loop and context-pressure recovery manifests on shadow policy labels with null + policy version. +- [x] T4: Show policy version in TraceDialog when available. +- [x] T5: Update baseline docs and tests. +- [x] T6: Run focused tests, format, i18n, lint, and typecheck. diff --git a/docs/architecture/deepchat-tape-policy-selector/plan.md b/docs/architecture/deepchat-tape-policy-selector/plan.md new file mode 100644 index 000000000..0097805c8 --- /dev/null +++ b/docs/architecture/deepchat-tape-policy-selector/plan.md @@ -0,0 +1,50 @@ +# DeepChat Tape Policy Selector - Plan + +## Architecture Decision + +Keep selector logic inside `src/main/presenter/agentRuntimePresenter/tapeViewPolicy.ts`. This avoids +adding a service and keeps the policy boundary local to the assembler. + +## Flow + +```text +TapeViewAssembler.buildTapeChatView() + -> resolveTapeViewPolicy() + -> policy.buildChat() + -> return messages + policy id/version + selection reason + +TapeViewAssembler.buildTapeResumeView() + -> resolveTapeViewPolicy() + -> policy.buildResume() + -> return messages + policy id/version + selection reason +``` + +## Module Changes + +| Module | Change | +| --- | --- | +| `src/main/presenter/agentRuntimePresenter/tapeViewPolicy.ts` | Add registry, lookup, list, and resolver helpers. | +| `src/main/presenter/agentRuntimePresenter/tapeViewAssembler.ts` | Resolve default policy through selector. | +| `test/main/presenter/agentRuntimePresenter/tapeViewPolicy.test.ts` | Cover registry and selector behavior. | +| `test/main/presenter/agentRuntimePresenter/tapeViewAssembler.test.ts` | Assert selection reason and injected policy behavior. | +| `docs/architecture/deepchat_tape_spec_v1.md` | Record the selector boundary. | + +## Compatibility + +- The default resolved policy remains `legacy_context_v1`. +- Existing injected policy tests continue to work. +- ViewManifest policy provenance remains `legacy_context_v1@1` for chat and resume. + +## Verification + +```bash +pnpm vitest run test/main/presenter/agentRuntimePresenter/tapeViewPolicy.test.ts +pnpm vitest run test/main/presenter/agentRuntimePresenter/tapeViewAssembler.test.ts +pnpm vitest run test/main/presenter/agentRuntimePresenter/tapeViewManifest.test.ts +pnpm run format +pnpm run i18n +pnpm run lint +pnpm run typecheck:node +pnpm run typecheck:web +pnpm vitest run --reporter=dot +``` diff --git a/docs/architecture/deepchat-tape-policy-selector/spec.md b/docs/architecture/deepchat-tape-policy-selector/spec.md new file mode 100644 index 000000000..132cec19b --- /dev/null +++ b/docs/architecture/deepchat-tape-policy-selector/spec.md @@ -0,0 +1,54 @@ +# DeepChat Tape Policy Selector - Spec + +Status: implemented SDD. This goal adds a policy registry and default selector for Tape view +assembly. + +## Problem + +`TapeViewAssembler` can accept an injected policy, but production assembly still selects the legacy +policy inline. The architecture needs a small selector boundary so future policy expansion can add +new policies without changing chat or resume assembly call sites. + +## Goals + +1. Add a `TapeViewPolicy` registry in the existing policy module. +2. Resolve the active policy through a selector for chat and resume assembly. +3. Keep `legacy_context_v1` as the default policy for all sessions. +4. Preserve current provider-bound message output. +5. Return policy selection reason in assembler metadata for audit/debugging. + +## Non-Goals + +- Introducing a new context-selection algorithm. +- Adding user-facing policy settings. +- Changing compaction, preflight, or context-pressure recovery. +- Adding a separate policy service or persistence table. + +## Acceptance Criteria + +1. `TapeViewAssembler` uses `resolveTapeViewPolicy()` for default policy selection. +2. `resolveTapeViewPolicy()` returns `legacy_context_v1` with reason `default` when no policy is + requested. +3. Unknown requested policy ids fall back to `legacy_context_v1` with reason `fallback_default`. +4. Injected test policies remain supported and report reason `injected`. +5. Assembler output remains provider-message equivalent with the previous implementation. +6. Policy registry tests cover list, lookup, default selection, and fallback selection. +7. Focused Tape tests, format, i18n, lint, typecheck, and full Vitest pass. + +## Contract + +```ts +export type TapeViewPolicySelectionReason = 'default' | 'requested' | 'fallback_default' | 'injected' + +export interface TapeViewPolicySelection { + policy: TapeViewPolicy + requestedPolicyId: string | null + reason: TapeViewPolicySelectionReason +} + +export function resolveTapeViewPolicy(input?: { + requestedPolicyId?: string | null +}): TapeViewPolicySelection +``` + +The first registry contains only `legacy_context_v1`. diff --git a/docs/architecture/deepchat-tape-policy-selector/tasks.md b/docs/architecture/deepchat-tape-policy-selector/tasks.md new file mode 100644 index 000000000..88e49aabc --- /dev/null +++ b/docs/architecture/deepchat-tape-policy-selector/tasks.md @@ -0,0 +1,10 @@ +# DeepChat Tape Policy Selector - Tasks + +## Implementation Tasks + +- [x] T1: Add registry, lookup, list, and resolver helpers to `tapeViewPolicy.ts`. +- [x] T2: Route `TapeViewAssembler` default selection through the resolver. +- [x] T3: Add selection reason to `TapeViewAssemblerResult`. +- [x] T4: Update policy and assembler tests. +- [x] T5: Update baseline Tape architecture docs. +- [x] T6: Run focused tests, format, i18n, lint, typecheck, and full tests. diff --git a/docs/architecture/deepchat-tape-replay-contract/plan.md b/docs/architecture/deepchat-tape-replay-contract/plan.md new file mode 100644 index 000000000..7fd6a9a7d --- /dev/null +++ b/docs/architecture/deepchat-tape-replay-contract/plan.md @@ -0,0 +1,58 @@ +# DeepChat Tape Replay Contract - Plan + +## Architecture Decision + +Add a replay-slice export on top of `DeepChatTapeService`. The service already owns manifest lookup +and has access to the SQLite presenter, so it remains the single Tape boundary. + +## Flow + +```text +renderer SessionClient.exportMessageTapeReplaySlice(messageId, options) + -> sessions.exportMessageTapeReplaySlice route + -> AgentSessionPresenter.exportMessageTapeReplaySlice(messageId, options) + -> AgentRuntimePresenter.exportMessageTapeReplaySlice(sessionId, messageId, options) + -> DeepChatTapeService.exportReplaySlice(sessionId, messageId, options) + -> select manifest by requestSeq or latest + -> find matching message trace by requestSeq + -> collect manifest/included/excluded/anchor tape entries + -> return deterministic replay slice +``` + +## Module Changes + +| Module | Change | +| --- | --- | +| `src/shared/types/tape-replay.ts` | Add replay slice, trace snapshot, entry snapshot, and options types. | +| `src/main/presenter/agentRuntimePresenter/tapeService.ts` | Add `exportReplaySlice()`. | +| `src/main/presenter/agentRuntimePresenter/index.ts` | Expose agent-level replay export method. | +| `src/main/presenter/agentSessionPresenter/index.ts` | Resolve `messageId -> sessionId -> agent`. | +| `src/shared/contracts/routes/sessions.routes.ts` | Add typed replay export route. | +| `src/main/routes/index.ts` | Wire the route. | +| `src/renderer/api/SessionClient.ts` | Add replay export client method. | +| `test/main/presenter/agentRuntimePresenter/tapeService.test.ts` | Cover replay export behavior. | + +## Hashing + +- `entry.payloadHash`: hash of raw `payload_json`. +- `entry.metaHash`: hash of raw `meta_json`. +- `trace.headersHash`: hash of raw `headers_json`. +- `trace.bodyHash`: hash of raw `body_json`. +- `sliceHash`: deterministic hash of the returned slice with `sliceHash` empty. + +## Compatibility + +- Missing tape table returns `null`. +- Missing manifest returns `null`. +- Missing matching trace returns a manifest-only slice. +- Old traces keep working through existing trace APIs. + +## Verification + +```bash +pnpm vitest run test/main/presenter/agentRuntimePresenter/tapeService.test.ts +pnpm run format +pnpm run i18n +pnpm run lint +pnpm run typecheck +``` diff --git a/docs/architecture/deepchat-tape-replay-contract/spec.md b/docs/architecture/deepchat-tape-replay-contract/spec.md new file mode 100644 index 000000000..74c824756 --- /dev/null +++ b/docs/architecture/deepchat-tape-replay-contract/spec.md @@ -0,0 +1,86 @@ +# DeepChat Tape Replay Contract - Spec + +Status: implemented SDD. This goal extends the completed ViewManifest shadow-mode increment with a +stable replay/export contract. + +## Problem + +ViewManifest records which context facts participated in each provider request. The next Tape +architecture layer needs a typed export shape that joins a manifest, matching trace metadata, and +referenced tape entries into one deterministic slice. Without this slice, Inspector debugging and +future eval/replay tools still need to reimplement lookup rules. + +## Goals + +1. Export a `DeepChatTapeReplaySlice` for a message request sequence. +2. Reuse `DeepChatTapeService` and existing `deepchat_tape_entries` / `deepchat_message_traces` + storage. +3. Keep metadata-only export as the default. +4. Allow explicit inclusion of existing tape payloads and trace payloads for developer replay. +5. Produce stable hashes for slice, tape entry payloads, tape entry metadata, and trace payloads. +6. Return `null` when no manifest exists for the requested message or request sequence. + +## Non-Goals + +- Running live LLM replay. +- Replacing `buildContext()` or request preflight behavior. +- Adding a dedicated replay table. +- Adding cross-session memory retrieval. + +## User Stories + +- As a runtime debugger, I can export one request's manifest, trace metadata, and referenced tape + entries with one typed call. +- As an eval author, I can identify the exact manifest hash and request hash for a historical + request. +- As a privacy-conscious maintainer, I can inspect replay structure without duplicating raw prompt + or message content by default. + +## Acceptance Criteria + +1. `DeepChatTapeService` can export the latest replay slice for a message. +2. `DeepChatTapeService` can export a replay slice for an explicit `requestSeq`. +3. The slice includes the manifest record, matching trace metadata when present, referenced tape + entry snapshots, anchor refs, and stable hashes. +4. The default export omits tape `payload` / `meta` and trace `headersJson` / `bodyJson`. +5. Explicit options can include tape payloads and trace payloads from their existing storage paths. +6. A typed route and renderer client method expose the export by `messageId`. +7. Tests cover default privacy behavior, explicit payload inclusion, missing manifests, and + request-sequence selection. + +## Contract + +```ts +export interface DeepChatTapeReplaySlice { + schemaVersion: 1 + sliceId: string + sessionId: string + messageId: string + requestSeq: number + mode: 'manifest_only' | 'trace_bound' + manifestRecord: DeepChatTapeViewManifestRecord + trace: DeepChatTapeReplayTraceSnapshot | null + entries: DeepChatTapeReplayEntrySnapshot[] + refs: { + manifestEntryId: number + includedEntryIds: number[] + excludedEntryIds: number[] + anchorEntryIds: number[] + } + hashes: { + manifestHash: string + sliceHash: string + } + createdAt: number +} +``` + +## Privacy + +Default export is metadata-only. It contains IDs, timestamps, names, source refs, and hashes. +Payload inclusion is opt-in and reads from existing storage: + +- tape entry payload/meta from `deepchat_tape_entries` +- trace headers/body from `deepchat_message_traces` + +No new raw-content storage path is introduced. diff --git a/docs/architecture/deepchat-tape-replay-contract/tasks.md b/docs/architecture/deepchat-tape-replay-contract/tasks.md new file mode 100644 index 000000000..dc27e75d4 --- /dev/null +++ b/docs/architecture/deepchat-tape-replay-contract/tasks.md @@ -0,0 +1,10 @@ +# DeepChat Tape Replay Contract - Tasks + +## Implementation Tasks + +- [x] T1: Add shared `DeepChatTapeReplaySlice` contract types. +- [x] T2: Add `DeepChatTapeService.exportReplaySlice()` with metadata-only defaults. +- [x] T3: Add route, presenter, and renderer client method. +- [x] T4: Add tests for latest selection, explicit `requestSeq`, missing manifest, default privacy, + and explicit payload inclusion. +- [x] T5: Run format, i18n, lint, typecheck, and focused tests. diff --git a/docs/architecture/deepchat-tape-view-assembler/plan.md b/docs/architecture/deepchat-tape-view-assembler/plan.md new file mode 100644 index 000000000..ced829cd6 --- /dev/null +++ b/docs/architecture/deepchat-tape-view-assembler/plan.md @@ -0,0 +1,59 @@ +# DeepChat Tape View Assembler - Plan + +## Architecture Decision + +Add `src/main/presenter/agentRuntimePresenter/tapeViewAssembler.ts` as the production context +assembly boundary. The assembler resolves the active `TapeViewPolicy` through the selector and +constrains input history to tape-effective records. + +## Flow + +```text +processMessage() + -> tapeService.ensureSessionTapeReady() + -> compaction uses tape-ready context history + -> TapeViewAssembler.buildChatView() + -> TapeViewPolicy selector + -> legacy_context_v1 policy + -> return messages + metadata + assembler provenance + -> runStreamForMessage() + -> ViewManifest append + +resumeAssistantMessage() + -> tapeService.ensureSessionTapeReady() + -> TapeViewAssembler.buildResumeView() + -> TapeViewPolicy selector + -> legacy_context_v1 policy + -> return messages + metadata + assembler provenance + -> runStreamForMessage() +``` + +## Module Changes + +| Module | Change | +| --- | --- | +| `src/main/presenter/agentRuntimePresenter/tapeViewAssembler.ts` | New assembler boundary. | +| `src/main/presenter/agentRuntimePresenter/tapeViewPolicy.ts` | Selection policy boundary used by the assembler. | +| `src/main/presenter/agentRuntimePresenter/index.ts` | Replace direct metadata builder calls with assembler calls. | +| `test/main/presenter/agentRuntimePresenter/tapeViewAssembler.test.ts` | Add chat/resume parity tests. | +| `docs/architecture/deepchat_tape_spec_v1.md` | Update current implementation path and owner table. | + +## Compatibility + +- Provider-bound messages stay byte-for-byte equivalent for existing tests. +- `contextBuilder.ts` remains available for unit tests and compatibility helpers. +- ViewManifest `contextBuilderVersion` remains `legacy-v1`. + +## Verification + +```bash +pnpm vitest run test/main/presenter/agentRuntimePresenter/tapeViewAssembler.test.ts +pnpm vitest run test/main/presenter/agentRuntimePresenter/tapeService.test.ts +pnpm vitest run test/renderer/components/trace/TraceDialog.test.ts +pnpm run format +pnpm run i18n +pnpm run lint +pnpm run typecheck:node +pnpm run typecheck:web +pnpm vitest run --reporter=dot +``` diff --git a/docs/architecture/deepchat-tape-view-assembler/spec.md b/docs/architecture/deepchat-tape-view-assembler/spec.md new file mode 100644 index 000000000..ccd06005c --- /dev/null +++ b/docs/architecture/deepchat-tape-view-assembler/spec.md @@ -0,0 +1,56 @@ +# DeepChat Tape View Assembler - Spec + +Status: implemented SDD. This goal replaces direct runtime context-builder calls with a Tape-owned +assembler boundary while preserving provider-bound message parity. + +## Problem + +ViewManifest and replay slices now make context decisions auditable. The runtime still calls +`buildContextWithMetadata()` and `buildResumeContextWithMetadata()` directly, so Tape remains an +observer around the production context path. The next architecture step needs a production +`TapeViewAssembler` boundary that owns context assembly inputs from the effective tape view. + +## Goals + +1. Route normal chat and resume context assembly through `TapeViewAssembler`. +2. Keep provider-bound `ChatMessage[]` identical to the existing context-builder output. +3. Keep `DeepChatTapeService` as the Tape boundary and use `ensureSessionTapeReady()` history + records as the assembly source. +4. Preserve ViewManifest shadow event behavior and replay export behavior. +5. Add parity tests proving assembler output matches the legacy context builder for chat and resume. + +## Non-Goals + +- Changing context selection policy. +- Changing compaction policy. +- Changing provider preflight or context-pressure recovery. +- Adding embedding memory or cross-session recall. +- Removing the legacy `contextBuilder.ts` implementation. + +## Acceptance Criteria + +1. Runtime normal chat no longer calls `buildContextWithMetadata()` directly. +2. Runtime resume no longer calls `buildResumeContextWithMetadata()` directly. +3. `TapeViewAssembler` exposes chat and resume assembly methods with typed metadata. +4. The assembler uses tape-ready history records supplied by `DeepChatTapeService`. +5. Tests prove chat parity against `buildContextWithMetadata()`. +6. Tests prove resume parity against `buildResumeContextWithMetadata()`. +7. Existing ViewManifest, replay slice, trace, lint, typecheck, and full test suites pass. + +## Contract + +```ts +export interface TapeViewAssemblerResult { + messages: ChatMessage[] + metadata: ContextBuildMetadata + assemblerVersion: 'tape-view-assembler-v1' + historySource: 'tape_effective_view' + historyRecords: ChatMessageRecord[] + policyId: 'legacy_context_v1' + policyVersion: 1 + policySelectionReason: 'default' | 'requested' | 'fallback_default' | 'injected' +} +``` + +`contextBuilderVersion` in ViewManifest remains `legacy-v1` for this increment because the +underlying selection algorithm stays unchanged. diff --git a/docs/architecture/deepchat-tape-view-assembler/tasks.md b/docs/architecture/deepchat-tape-view-assembler/tasks.md new file mode 100644 index 000000000..19116d50e --- /dev/null +++ b/docs/architecture/deepchat-tape-view-assembler/tasks.md @@ -0,0 +1,9 @@ +# DeepChat Tape View Assembler - Tasks + +## Implementation Tasks + +- [x] T1: Add `TapeViewAssembler` types and chat/resume assembly functions. +- [x] T2: Replace runtime direct calls to metadata context builders. +- [x] T3: Add chat and resume parity tests. +- [x] T4: Update baseline Tape architecture doc. +- [x] T5: Run focused tests, format, i18n, lint, typecheck, and full tests. diff --git a/docs/architecture/deepchat-tape-view-manifest/plan.md b/docs/architecture/deepchat-tape-view-manifest/plan.md index 632b36d0f..73df6c229 100644 --- a/docs/architecture/deepchat-tape-view-manifest/plan.md +++ b/docs/architecture/deepchat-tape-view-manifest/plan.md @@ -14,22 +14,21 @@ metadata as tape events and leaves production message assembly unchanged. AgentRuntimePresenter.processMessage() -> tapeService.ensureSessionTapeReady() -> messageStore.createUserMessage() - -> buildContext() - -> tapeViewManifestService.assembleInitialManifest() - -> tapeService.appendViewManifest() + -> buildContextWithMetadata() -> messageStore.createAssistantMessage() -> runStreamForMessage() -> processStream() -> coreStream(requestMessages, requestTools) -> preflightRequestContext() -> optional recoverRequestContextPressure() - -> tapeViewManifestService.assembleRequestManifest() + -> tapeViewManifest.assembleRequestManifest() -> tapeService.appendViewManifest() -> provider.coreStream() -> optional request trace persists with matching requestSeq ``` -Resume flow uses the same service with `taskType = "resume"` and `policy = "resume_shadow"`. +Resume flow uses the same service with `taskType = "resume"`, `policy = "legacy_context_v1"`, and +`policyVersion = 1`. ## Module Changes @@ -39,8 +38,8 @@ Resume flow uses the same service with `taskType = "resume"` and `policy = "resu | `src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts` | New pure assembler for manifest metadata and hashes. | | `src/main/presenter/agentRuntimePresenter/tapeService.ts` | Append and list `view/assembled` events. | | `src/main/presenter/agentRuntimePresenter/index.ts` | Call manifest assembly at initial context and request-level preflight points. | -| `src/shared/contracts/routes/sessions.routes.ts` | Add route for manifests by message ID. | -| `src/renderer/api/SessionClient.ts` | Add `listMessageViewManifests(messageId)`. | +| `src/shared/contracts/routes/sessions.routes.ts` | Extend the existing trace route output with manifest records. | +| `src/renderer/api/SessionClient.ts` | Add diagnostics and manifest client methods for message ID lookups. | | `src/renderer/src/components/trace/TraceDialog.vue` | Add tabs for Request, View Manifest, Tape Entries, and Budget. | ## Data Model @@ -59,7 +58,8 @@ deepchat_tape_entries payload_json.data.manifest = DeepChatTapeViewManifest ``` -The route resolves `messageId -> sessionId`, then reads matching tape events. +The existing trace route resolves `messageId -> sessionId`, then returns both request traces and +matching tape manifest events. ## Request Sequence @@ -112,7 +112,7 @@ TraceDialog | JSON editor | +-------------------------------------------------------------------+ | View Manifest tab | -| View id / policy / request seq | +| View id / policy / policy version / request seq | | Included and excluded entry list | +-------------------------------------------------------------------+ | Tape Entries tab | diff --git a/docs/architecture/deepchat-tape-view-manifest/spec.md b/docs/architecture/deepchat-tape-view-manifest/spec.md index 148d9b954..763a49904 100644 --- a/docs/architecture/deepchat-tape-view-manifest/spec.md +++ b/docs/architecture/deepchat-tape-view-manifest/spec.md @@ -1,7 +1,6 @@ # DeepChat Tape ViewManifest Shadow Mode - Spec -Status: active SDD. This goal prepares implementation. Code changes start only after this spec, -plan, and task list are accepted. +Status: implemented SDD. This goal records the shadow-mode architecture and implementation tasks. ## Problem @@ -61,12 +60,12 @@ production path. 3. A tool-loop provider request appends a request-level manifest with a monotonic `requestSeq` for the assistant message. 4. If context-pressure recovery changes the provider request messages, a new manifest revision is - appended with `policy = "context_pressure_recovery_shadow"`. + appended with `policy = "context_pressure_recovery_shadow"` and `policyVersion = null`. 5. The manifest records selected message IDs, source tape entry IDs when available, excluded message IDs, exclusion reasons, token-budget inputs, prompt hash, and tool-definition hash. -6. The manifest stores IDs, hashes, token estimates, policies, and reasons. Raw user text, raw - assistant text, raw tool output, image data, audio data, file content, API headers, and API keys - stay in existing storage paths. +6. The manifest stores IDs, hashes, token estimates, policies, policy versions, and reasons. Raw + user text, raw assistant text, raw tool output, image data, audio data, file content, API + headers, and API keys stay in existing storage paths. 7. Trace UI can show Request and View Manifest tabs for a traced assistant message. 8. Existing trace behavior remains compatible when a manifest is absent. 9. Tests prove that the selected history represented by the manifest matches `buildContext()` for @@ -87,10 +86,12 @@ export type DeepChatTapeViewManifest = { taskType: 'chat' | 'resume' | 'tool_loop' policy: + | 'legacy_context_v1' | 'legacy_context_shadow' | 'resume_shadow' | 'tool_loop_shadow' | 'context_pressure_recovery_shadow' + policyVersion: number | null contextBuilderVersion: 'legacy-v1' latestEntryId: number @@ -186,7 +187,7 @@ The first UI increment extends the existing trace dialog. +-------------------------------------------------------------------+ | Trace #1 Request | View Manifest | Tape Entries | Budget | +-------------------------------------------------------------------+ -| View view_01 Policy legacy_context_shadow | +| View view_01 Policy legacy_context_v1 Version 1 | | Anchor #42 Summary cursor 17 | +------------------+------------------------------------------------+ | Included | #43 user order=17 selected_history | @@ -218,6 +219,7 @@ States: - Missing tape table remains a supported fallback for existing runtime code. - The manifest feature preserves chat generation behavior, request ordering, and token-budget decisions. +- Manifest append failures are logged and request execution continues. ## Success Criteria diff --git a/docs/architecture/deepchat-tape-view-manifest/tasks.md b/docs/architecture/deepchat-tape-view-manifest/tasks.md index 51851ea44..7fd9301ef 100644 --- a/docs/architecture/deepchat-tape-view-manifest/tasks.md +++ b/docs/architecture/deepchat-tape-view-manifest/tasks.md @@ -7,19 +7,19 @@ ## Implementation Tasks -- [ ] T1: Add `DeepChatTapeViewManifest` shared types and a pure manifest hashing helper. -- [ ] T2: Add `tapeViewManifest.ts` with pure assembly helpers for normal chat, resume, and +- [x] T1: Add `DeepChatTapeViewManifest` shared types and a pure manifest hashing helper. +- [x] T2: Add `tapeViewManifest.ts` with pure assembly helpers for normal chat, resume, and request-level provider calls. -- [ ] T3: Extend `DeepChatTapeService` to append and list `view/assembled` events. -- [ ] T4: Emit initial shadow manifests after `buildContext()` and `buildResumeContext()`. -- [ ] T5: Emit request-level manifests inside `runStreamForMessage()` after preflight and +- [x] T3: Extend `DeepChatTapeService` to append and list `view/assembled` events. +- [x] T4: Emit initial shadow manifests after `buildContext()` and `buildResumeContext()`. +- [x] T5: Emit request-level manifests inside `runStreamForMessage()` after preflight and context-pressure recovery. -- [ ] T6: Add typed route and renderer client method for manifests by message ID. -- [ ] T7: Extend `TraceDialog.vue` with Request, View Manifest, Tape Entries, and Budget tabs. -- [ ] T8: Add unit tests for manifest assembly, tape service list/append behavior, and route/client +- [x] T6: Add typed route and renderer client method for manifests by message ID. +- [x] T7: Extend `TraceDialog.vue` with Request, View Manifest, Tape Entries, and Budget tabs. +- [x] T8: Add unit tests for manifest assembly, tape service list/append behavior, and route/client compatibility. -- [ ] T9: Add renderer tests for manifest tab loading, empty, error, and data states. -- [ ] T10: Run format, i18n, lint, typecheck, and focused test suites. +- [x] T9: Add renderer tests for manifest tab loading, empty, error, and data states. +- [x] T10: Run format, i18n, lint, typecheck, and focused test suites. ## Follow-up Tasks diff --git a/docs/architecture/deepchat-tape-view-policy/plan.md b/docs/architecture/deepchat-tape-view-policy/plan.md new file mode 100644 index 000000000..c81fcfcb4 --- /dev/null +++ b/docs/architecture/deepchat-tape-view-policy/plan.md @@ -0,0 +1,49 @@ +# DeepChat Tape View Policy - Plan + +## Architecture Decision + +Add `src/main/presenter/agentRuntimePresenter/tapeViewPolicy.ts`. The policy owns calls into +`contextBuilder.ts`; `TapeViewAssembler` owns production orchestration and provenance. + +## Flow + +```text +TapeViewAssembler.buildTapeChatView() + -> select default legacy_context_v1 policy + -> policy.buildChat() + -> return messages + metadata + policy id/version + +TapeViewAssembler.buildTapeResumeView() + -> select default legacy_context_v1 policy + -> policy.buildResume() + -> return messages + metadata + policy id/version +``` + +## Module Changes + +| Module | Change | +| --- | --- | +| `src/main/presenter/agentRuntimePresenter/tapeViewPolicy.ts` | New policy interface and legacy policy implementation. | +| `src/main/presenter/agentRuntimePresenter/tapeViewAssembler.ts` | Delegate selection to `TapeViewPolicy`. | +| `test/main/presenter/agentRuntimePresenter/tapeViewPolicy.test.ts` | Prove legacy policy parity. | +| `test/main/presenter/agentRuntimePresenter/tapeViewAssembler.test.ts` | Assert policy metadata and policy delegation. | +| `docs/architecture/deepchat_tape_spec_v1.md` | Record the new policy boundary. | + +## Compatibility + +- No runtime behavior change. +- `contextBuilder.ts` remains the legacy algorithm implementation. +- The default policy is fixed to `legacy_context_v1`. + +## Verification + +```bash +pnpm vitest run test/main/presenter/agentRuntimePresenter/tapeViewPolicy.test.ts +pnpm vitest run test/main/presenter/agentRuntimePresenter/tapeViewAssembler.test.ts +pnpm run format +pnpm run i18n +pnpm run lint +pnpm run typecheck:node +pnpm run typecheck:web +pnpm vitest run --reporter=dot +``` diff --git a/docs/architecture/deepchat-tape-view-policy/spec.md b/docs/architecture/deepchat-tape-view-policy/spec.md new file mode 100644 index 000000000..ce5623a77 --- /dev/null +++ b/docs/architecture/deepchat-tape-view-policy/spec.md @@ -0,0 +1,49 @@ +# DeepChat Tape View Policy - Spec + +Status: implemented SDD. This goal extracts the current legacy context-selection algorithm behind a +Tape view policy interface. + +## Problem + +`TapeViewAssembler` is now the production context assembly entry. Tape context policy work needs a +typed policy boundary so new selection strategies can be introduced without changing runtime call +sites. + +## Goals + +1. Introduce a `TapeViewPolicy` interface for chat and resume assembly. +2. Implement `legacy_context_v1` as the first policy. +3. Keep provider-bound `ChatMessage[]` and metadata identical to the current assembler output. +4. Record policy id/version in `TapeViewAssemblerResult`. +5. Keep ViewManifest and replay slice behavior compatible. +6. Support registry-backed default policy resolution. + +## Non-Goals + +- Introducing a new context-selection algorithm. +- Changing compaction, preflight, or context-pressure recovery. +- Adding user-facing policy selection. +- Adding memory graph or embedding retrieval. + +## Acceptance Criteria + +1. `TapeViewAssembler` no longer imports `buildContextWithMetadata()` directly. +2. `TapeViewAssembler` no longer imports `buildResumeContextWithMetadata()` directly. +3. `legacy_context_v1` policy delegates to the current context builder. +4. Assembler results include `policyId = "legacy_context_v1"` and `policyVersion = 1`. +5. Tests prove policy output matches the legacy builder for chat and resume. +6. Tests prove assembler output still matches policy output. +7. Existing ViewManifest, replay, trace, lint, typecheck, and full test suites pass. + +## Contract + +```ts +export interface TapeViewPolicy { + id: 'legacy_context_v1' + version: 1 + buildChat(input: TapeChatViewPolicyInput): ContextBuildResult + buildResume(input: TapeResumeViewPolicyInput): ContextBuildResult +} +``` + +`legacy_context_v1` is the default policy for all DeepChat sessions in this increment. diff --git a/docs/architecture/deepchat-tape-view-policy/tasks.md b/docs/architecture/deepchat-tape-view-policy/tasks.md new file mode 100644 index 000000000..4b75d8e4f --- /dev/null +++ b/docs/architecture/deepchat-tape-view-policy/tasks.md @@ -0,0 +1,9 @@ +# DeepChat Tape View Policy - Tasks + +## Implementation Tasks + +- [x] T1: Add `TapeViewPolicy` interface and `legacy_context_v1` implementation. +- [x] T2: Update `TapeViewAssembler` to call policy interface and return policy metadata. +- [x] T3: Add policy parity tests and update assembler tests. +- [x] T4: Update baseline Tape architecture doc. +- [x] T5: Run focused tests, format, i18n, lint, typecheck, and full tests. diff --git a/docs/architecture/deepchat_tape_spec_v1.md b/docs/architecture/deepchat_tape_spec_v1.md index c07add812..2613b17a7 100644 --- a/docs/architecture/deepchat_tape_spec_v1.md +++ b/docs/architecture/deepchat_tape_spec_v1.md @@ -1,7 +1,12 @@ # DeepChat Tape System - Implementation Baseline -Status: current implementation direction. The active SDD goal is -[deepchat-tape-view-manifest](deepchat-tape-view-manifest/spec.md). +Status: current implementation direction. Active SDD goals are +[deepchat-tape-view-manifest](deepchat-tape-view-manifest/spec.md), +[deepchat-tape-replay-contract](deepchat-tape-replay-contract/spec.md), and +[deepchat-tape-view-assembler](deepchat-tape-view-assembler/spec.md), and +[deepchat-tape-view-policy](deepchat-tape-view-policy/spec.md), and +[deepchat-tape-policy-provenance](deepchat-tape-policy-provenance/spec.md), and +[deepchat-tape-policy-selector](deepchat-tape-policy-selector/spec.md). This document keeps the Tape vision aligned with the current DeepChat codebase. The implementation path is: @@ -11,7 +16,10 @@ Existing DeepChat runtime -> existing DeepChatTapeService -> ViewManifest shadow mode -> Inspector and replay contracts - -> later policy replacement + -> TapeViewAssembler production entry + -> TapeViewPolicy boundary + -> ViewManifest policy provenance + -> TapeViewPolicy registry and selector ``` ## Current Baseline @@ -25,7 +33,9 @@ DeepChat already has the main Tape primitives. | Message facts | `DeepChatMessageStore` + `tapeFacts.ts` | User, assistant, tool call, tool result, replacement, and retraction facts. | | Anchor | `kind = "anchor"` entries | `session/start`, `compaction/*`, `handoff/*`, `auto_handoff/*`, `fork/start`. | | Effective view | `tapeEffectiveView.ts` | Reconstructs current message records from append-only facts. | -| Context build | `contextBuilder.ts` | Current production context assembler and token-budget selector. | +| Context assembly | `tapeViewAssembler.ts` | Production entry that assembles provider context from tape-effective records. | +| View policy | `tapeViewPolicy.ts` | Registry and selector boundary; `legacy_context_v1` delegates to the current selector. | +| Context selection | `contextBuilder.ts` | Legacy token-budget selector used by `legacy_context_v1`. | | Request trace | `deepchat_message_traces` | Stores redacted provider request previews for the trace dialog. | | Agent tools | `agentTapeTools.ts` | Exposes `tape_info`, `tape_search`, `tape_anchors`, `tape_handoff`. | @@ -34,32 +44,58 @@ remains the Tape service boundary. ## Active SDD -The active SDD folder is: +The active SDD folders are: ```text docs/architecture/deepchat-tape-view-manifest/ ├── spec.md ├── plan.md └── tasks.md +docs/architecture/deepchat-tape-replay-contract/ +├── spec.md +├── plan.md +└── tasks.md +docs/architecture/deepchat-tape-view-assembler/ +├── spec.md +├── plan.md +└── tasks.md +docs/architecture/deepchat-tape-view-policy/ +├── spec.md +├── plan.md +└── tasks.md +docs/architecture/deepchat-tape-policy-provenance/ +├── spec.md +├── plan.md +└── tasks.md +docs/architecture/deepchat-tape-policy-selector/ +├── spec.md +├── plan.md +└── tasks.md ``` -The SDD scope is `Existing TapeService + ViewManifest shadow mode`. +The current SDD scopes are `Existing TapeService + ViewManifest shadow mode`, replay/export +contracts, `TapeViewAssembler` as the production context assembly entry, and `TapeViewPolicy` as +the policy replacement boundary, `ViewManifest` policy provenance, and policy selector registry. ## Scope Boundary ### In scope -- Generate a `ViewManifest` for each DeepChat LLM request while `buildContext` remains the source - of model messages. +- Generate a `ViewManifest` for each DeepChat LLM request while `TapeViewAssembler` remains + provider-message equivalent with the legacy context selector. - Persist manifests as `view/assembled` tape events. - Link manifests to request traces by `messageId` and request sequence. - Add Inspector support that explains included/excluded context entries. - Add parity tests proving shadow manifests describe the same context that the existing runtime sends. +- Export replay slices from manifest, trace metadata, and referenced tape entries. +- Route chat and resume production context assembly through `TapeViewAssembler`. +- Route context selection through `TapeViewPolicy` with `legacy_context_v1` as the default policy. +- Persist the active Tape view policy id and version in initial chat and resume manifests. +- Resolve active Tape view policies through a registry-backed selector. ### Deferred scope for the first increment -- Replacing `buildContext` as the production context builder. - Creating a separate TapeStore abstraction. - Memory graph retrieval, embedding-backed topic clustering, and cross-session recall. - Live LLM replay in CI. @@ -70,7 +106,8 @@ The SDD scope is `Existing TapeService + ViewManifest shadow mode`. 1. Keep `DeepChatTapeService` as the Tape service boundary. 2. Store manifest data as append-only tape events. 3. Keep raw prompt and provider request bodies in existing trace storage only. -4. Store IDs, hashes, token estimates, policy names, and exclusion reasons in the manifest. +4. Store IDs, hashes, token estimates, policy names, policy versions, and exclusion reasons in the + manifest. 5. Keep old sessions compatible through existing lazy backfill and bootstrap anchors. 6. Treat `ViewManifest` as an explanation and regression artifact until parity is proven. @@ -79,8 +116,10 @@ The SDD scope is `Existing TapeService + ViewManifest shadow mode`. ```text sendMessage / resume -> ensureSessionTapeReady() - -> buildContext() - -> assemble ViewManifest shadow event + -> TapeViewAssembler.buildChatView() / buildResumeView() + -> TapeViewPolicy selector + -> legacy_context_v1 TapeViewPolicy + -> assemble ViewManifest shadow event with legacy_context_v1@1 provenance -> runStreamForMessage() -> preflight provider request -> assemble request-level ViewManifest revision if context changed @@ -96,7 +135,7 @@ sendMessage / resume | Trace #1 Request | View Manifest | Tape Entries | Budget | +-------------------------------------------------------------------+ | Provider openai Model gpt-4.1 | -| View view_01 Policy legacy_context_shadow | +| View view_01 Policy legacy_context_v1@1 | +-----------------------------+-------------------------------------+ | Included | message/user #12 | | | message/assistant #13 | diff --git a/src/main/presenter/agentRuntimePresenter/contextBuilder.ts b/src/main/presenter/agentRuntimePresenter/contextBuilder.ts index 93711a51a..3b970ba6f 100644 --- a/src/main/presenter/agentRuntimePresenter/contextBuilder.ts +++ b/src/main/presenter/agentRuntimePresenter/contextBuilder.ts @@ -10,6 +10,10 @@ import type { MessageMetadata, SendMessageInput } from '@shared/types/agent-interface' +import type { + DeepChatTapeViewEntryReason, + DeepChatTapeViewExcludedReason +} from '@shared/types/tape-view-manifest' import type { DeepChatMessageStore } from './messageStore' const IMAGE_TOKEN_ESTIMATE = 512 @@ -43,6 +47,27 @@ export type HistoryTurn = { tokens: number } +export type ContextIncludedRecord = { + record: ChatMessageRecord + reason: DeepChatTapeViewEntryReason +} + +export type ContextExcludedRecord = { + record: ChatMessageRecord + reason: DeepChatTapeViewExcludedReason +} + +export type ContextBuildMetadata = { + includedRecords: ContextIncludedRecord[] + excludedRecords: ContextExcludedRecord[] + includesSystemPrompt: boolean +} + +export type ContextBuildResult = { + messages: ChatMessage[] + metadata: ContextBuildMetadata +} + function parseProviderOptionsJson( value: string | undefined ): ChatMessageProviderOptions | undefined { @@ -860,18 +885,26 @@ function selectTurnHistory( availableTokens: number, fallbackProtectedTurnCount: number ): ChatMessage[] { + return flattenTurns(selectTurnHistoryTurns(turns, availableTokens, fallbackProtectedTurnCount)) +} + +function selectTurnHistoryTurns( + turns: T[], + availableTokens: number, + fallbackProtectedTurnCount: number +): T[] { if (turns.length === 0) { return [] } const protectedCount = Math.max(0, Math.min(fallbackProtectedTurnCount, turns.length)) if (availableTokens <= 0) { - return protectedCount > 0 ? flattenTurns(turns.slice(-protectedCount)) : [] + return protectedCount > 0 ? turns.slice(-protectedCount) : [] } let total = turns.reduce((sum, turn) => sum + turn.tokens, 0) if (total <= availableTokens) { - return flattenTurns(turns) + return turns } const remainingTurns = [...turns] @@ -886,10 +919,17 @@ function selectTurnHistory( estimateMessagesTokens(flattened) <= availableTokens || remainingTurns.length <= protectedCount ) { - return flattened + return remainingTurns } - return truncateContext(flattened, availableTokens) + const truncatedMessages = truncateContext(flattened, availableTokens) + return [ + { + ...remainingTurns[0], + messages: truncatedMessages, + tokens: estimateMessagesTokens(truncatedMessages) + } + ] } function filterRecordsFromCursor( @@ -910,12 +950,33 @@ export function buildContext( supportsVision: boolean = false, options: ContextBuildOptions = {} ): ChatMessage[] { + return buildContextWithMetadata( + sessionId, + newUserContent, + systemPrompt, + contextLength, + reserveTokens, + messageStore, + supportsVision, + options + ).messages +} + +export function buildContextWithMetadata( + sessionId: string, + newUserContent: string | SendMessageInput, + systemPrompt: string, + contextLength: number, + reserveTokens: number, + messageStore: DeepChatMessageStore, + supportsVision: boolean = false, + options: ContextBuildOptions = {} +): ContextBuildResult { const supportsAudioInput = options.supportsAudioInput === true const candidateRecords = options.historyRecords ?? messageStore.getMessages(sessionId) - const historyRecords = filterRecordsFromCursor( - candidateRecords.filter(isContextHistoryRecord), - options.summaryCursorOrderSeq ?? 1 - ) + const contextCandidateRecords = candidateRecords.filter(isContextHistoryRecord) + const cursor = Math.max(1, options.summaryCursorOrderSeq ?? 1) + const historyRecords = filterRecordsFromCursor(contextCandidateRecords, cursor) const historyTurns = buildHistoryTurns( historyRecords, supportsVision, @@ -933,11 +994,18 @@ export function buildContext( newUserTokens - reserveTokens - (options.extraReserveTokens ?? 0) - const selectedHistory = selectTurnHistory( + const selectedTurns = selectTurnHistoryTurns( historyTurns, available, options.fallbackProtectedTurnCount ?? 0 ) + const selectedHistory = flattenTurns(selectedTurns) + const selectedRecordIds = new Set( + selectedTurns.flatMap((turn) => turn.records.map((record) => record.id)) + ) + const emittedRecordIds = new Set( + historyTurns.flatMap((turn) => turn.records.map((record) => record.id)) + ) const messages: ChatMessage[] = [] if (systemPrompt) { @@ -947,7 +1015,40 @@ export function buildContext( if (hasPromptMessageContent(newUserMessage)) { messages.push(newUserMessage) } - return messages + const excludedRecords: ContextExcludedRecord[] = [ + ...contextCandidateRecords + .filter((record) => record.orderSeq < cursor) + .map((record) => ({ + record, + reason: 'before_summary_cursor' as const + })), + ...historyRecords + .filter((record) => !emittedRecordIds.has(record.id)) + .map((record) => ({ + record, + reason: 'empty_after_formatting' as const + })), + ...historyRecords + .filter((record) => emittedRecordIds.has(record.id) && !selectedRecordIds.has(record.id)) + .map((record) => ({ + record, + reason: 'out_of_budget' as const + })) + ] + + return { + messages, + metadata: { + includedRecords: selectedTurns.flatMap((turn) => + turn.records.map((record) => ({ + record, + reason: 'selected_history' as const + })) + ), + excludedRecords, + includesSystemPrompt: Boolean(systemPrompt) + } + } } export function fitMessagesToContextWindow( @@ -1001,6 +1102,28 @@ export function buildResumeContext( supportsVision: boolean = false, options: ContextBuildOptions = {} ): ChatMessage[] { + return buildResumeContextWithMetadata( + sessionId, + assistantMessageId, + systemPrompt, + contextLength, + reserveTokens, + messageStore, + supportsVision, + options + ).messages +} + +export function buildResumeContextWithMetadata( + sessionId: string, + assistantMessageId: string, + systemPrompt: string, + contextLength: number, + reserveTokens: number, + messageStore: DeepChatMessageStore, + supportsVision: boolean = false, + options: ContextBuildOptions = {} +): ContextBuildResult { const supportsAudioInput = options.supportsAudioInput === true const allMessages = options.historyRecords ?? messageStore.getMessages(sessionId) const targetMessage = allMessages.find((message) => message.id === assistantMessageId) @@ -1030,16 +1153,66 @@ export function buildResumeContext( const systemPromptTokens = systemPrompt ? approximateTokenSize(systemPrompt) : 0 const available = contextLength - systemPromptTokens - reserveTokens - (options.extraReserveTokens ?? 0) - const selectedHistory = selectTurnHistory( + const selectedTurns = selectTurnHistoryTurns( historyTurns, available, options.fallbackProtectedTurnCount ?? 1 ) + const selectedHistory = flattenTurns(selectedTurns) + const selectedRecordIds = new Set( + selectedTurns.flatMap((turn) => turn.records.map((record) => record.id)) + ) + const emittedRecordIds = new Set( + historyTurns.flatMap((turn) => turn.records.map((record) => record.id)) + ) const messages: ChatMessage[] = [] if (systemPrompt) { messages.push({ role: 'system', content: systemPrompt }) } messages.push(...selectedHistory) - return messages + const eligibleIds = new Set(historyRecords.map((record) => record.id)) + const excludedRecords: ContextExcludedRecord[] = [ + ...allMessages + .filter( + (record) => + record.id !== assistantMessageId && + isContextHistoryRecord(record) && + record.orderSeq < cursor && + (targetOrderSeq === undefined || record.orderSeq <= targetOrderSeq) + ) + .map((record) => ({ + record, + reason: 'before_summary_cursor' as const + })), + ...historyRecords + .filter((record) => !emittedRecordIds.has(record.id)) + .map((record) => ({ + record, + reason: 'empty_after_formatting' as const + })), + ...historyRecords + .filter((record) => eligibleIds.has(record.id) && !selectedRecordIds.has(record.id)) + .map((record) => ({ + record, + reason: 'out_of_budget' as const + })) + ] + + return { + messages, + metadata: { + includedRecords: selectedTurns.flatMap((turn) => + turn.records.map((record) => ({ + record, + reason: + record.id === assistantMessageId + ? ('resume_target' as const) + : ('selected_history' as const) + })) + ), + excludedRecords, + includesSystemPrompt: Boolean(systemPrompt) + } + } } diff --git a/src/main/presenter/agentRuntimePresenter/index.ts b/src/main/presenter/agentRuntimePresenter/index.ts index d5ebbcba4..21675bd3c 100644 --- a/src/main/presenter/agentRuntimePresenter/index.ts +++ b/src/main/presenter/agentRuntimePresenter/index.ts @@ -29,6 +29,10 @@ import type { } from '@shared/types/agent-interface' import type { MCPToolCall, MCPToolResponse, ToolCallImagePreview } from '@shared/types/core/mcp' import type { ChatMessage } from '@shared/types/core/chat-message' +import type { + DeepChatTapeReplayExportOptions, + DeepChatTapeReplaySlice +} from '@shared/types/tape-replay' import type { IConfigPresenter, ILlmProviderPresenter, @@ -80,7 +84,12 @@ import { buildRuntimeCapabilitiesPrompt, buildSystemEnvPrompt } from '@/lib/agentRuntime/systemEnvPromptBuilder' -import { buildContext, buildResumeContext, isContextHistoryRecord } from './contextBuilder' +import type { ContextBuildMetadata } from './contextBuilder' +import { + buildTapeChatView, + buildTapeResumeView, + getTapeContextHistoryRecords +} from './tapeViewAssembler' import { capAgentDefaultMaxTokens, capAgentRequestMaxTokens, @@ -98,6 +107,14 @@ import { import { buildPersistableMessageTracePayload } from './messageTracePayload' import { buildTerminalErrorBlocks, DeepChatMessageStore } from './messageStore' import { DeepChatTapeService } from './tapeService' +import { + buildExcludedRefs, + buildIncludedRefs, + buildSyntheticRequestRefs, + createTapeViewManifest, + resolveTapeViewManifestPolicy, + type TapeViewContextSelection +} from './tapeViewManifest' import { PendingInputCoordinator } from './pendingInputCoordinator' import { DeepChatPendingInputStore } from './pendingInputStore' import { processStream } from './process' @@ -106,6 +123,12 @@ import { DeepChatSessionStore, type SessionSummaryState } from './sessionStore' import type { InterleavedReasoningConfig, PendingToolInteraction, ProcessResult } from './types' import { ToolOutputGuard } from './toolOutputGuard' import type { ProviderRequestTracePayload } from '../llmProviderPresenter/requestTrace' +import type { + DeepChatTapeViewPolicy, + DeepChatTapeViewManifestRecord, + DeepChatTapeViewTaskType, + DeepChatTapeViewTokenBudget +} from '@shared/types/tape-view-manifest' import type { NewSessionHooksBridge } from '../hooksNotifications/newSessionBridge' import { providerDbLoader } from '../configPresenter/providerDbLoader' import { resolveSessionVisionTarget } from '../vision/sessionVisionResolver' @@ -130,6 +153,17 @@ type PendingInteractionEntry = { type ProcessPendingInputSource = PendingInputEnqueueSource | 'steer' +type PendingTapeViewContext = { + taskType: DeepChatTapeViewTaskType + policy: DeepChatTapeViewPolicy + policyVersion?: number | null + selection: TapeViewContextSelection + summaryCursorOrderSeq: number + supportsVision: boolean + supportsAudioInput: boolean + traceDebugEnabled: boolean +} + type DeferredToolExecutionResult = { responseText: string isError: boolean @@ -275,6 +309,18 @@ const createAbortError = (): Error => { return error } +function buildTapeViewSelection( + metadata: ContextBuildMetadata, + newUserMessageId?: string | null +): TapeViewContextSelection { + return { + includedRecords: metadata.includedRecords, + excludedRecords: metadata.excludedRecords, + includesSystemPrompt: metadata.includesSystemPrompt, + newUserMessageId + } +} + export class AgentRuntimePresenter implements IAgentImplementation { private readonly llmProviderPresenter: ILlmProviderPresenter private readonly configPresenter: IConfigPresenter @@ -704,7 +750,7 @@ export class AgentRuntimePresenter implements IAgentImplementation { ) this.throwIfAbortRequested(preStreamAbortSignal) const tapeReady = this.tapeService.ensureSessionTapeReady(sessionId, this.messageStore) - const historyRecords = tapeReady.historyRecords.filter(isContextHistoryRecord) + const historyRecords = getTapeContextHistoryRecords(tapeReady.historyRecords) const userContent: UserMessageContent = { text: normalizedInput.text, files: normalizedInput.files || [], @@ -783,24 +829,25 @@ export class AgentRuntimePresenter implements IAgentImplementation { appendSummarySection(baseSystemPrompt, summaryState.summaryText), this.sessionStore.getReconstructionAnchorPromptState(sessionId) ) - const messages = buildContext( + const contextBuild = buildTapeChatView({ sessionId, - normalizedInput, + newUserContent: normalizedInput, systemPrompt, - contextBudgetLength, - maxTokens, - this.messageStore, + contextLength: contextBudgetLength, + reserveTokens: maxTokens, + messageStore: this.messageStore, supportsVision, - { + historyRecords, + options: { summaryCursorOrderSeq: summaryState.summaryCursorOrderSeq, - historyRecords, supportsAudioInput, extraReserveTokens: toolReserveTokens, preserveInterleavedReasoning: interleavedReasoning.preserveReasoningContent, preserveEmptyInterleavedReasoning: interleavedReasoning.preserveEmptyReasoningContent === true } - ) + }) + const messages = contextBuild.messages const assistantOrderSeq = this.messageStore.getNextOrderSeq(sessionId) assistantMessageId = this.messageStore.createAssistantMessage(sessionId, assistantOrderSeq) @@ -824,7 +871,17 @@ export class AgentRuntimePresenter implements IAgentImplementation { promptPreview: normalizedInput.text, tools, baseSystemPrompt, - interleavedReasoning + interleavedReasoning, + viewContext: { + taskType: 'chat', + policy: contextBuild.policyId, + policyVersion: contextBuild.policyVersion, + selection: buildTapeViewSelection(contextBuild.metadata, userMessageId), + summaryCursorOrderSeq: summaryState.summaryCursorOrderSeq, + supportsVision, + supportsAudioInput, + traceDebugEnabled: this.configPresenter.getSetting('traceDebugEnabled') === true + } }) if (context?.pendingQueueItemId && !consumedPendingQueueItem) { if (pendingInputSource === 'queue' || pendingInputSource === 'steer') { @@ -1952,6 +2009,23 @@ export class AgentRuntimePresenter implements IAgentImplementation { return this.toTapeAnchorResult(row) } + async listMessageViewManifests( + sessionId: string, + messageId: string + ): Promise { + this.tapeService.ensureSessionTapeReady(sessionId, this.messageStore) + return this.tapeService.listViewManifestsByMessage(sessionId, messageId) + } + + async exportMessageTapeReplaySlice( + sessionId: string, + messageId: string, + options?: DeepChatTapeReplayExportOptions + ): Promise { + this.tapeService.ensureSessionTapeReady(sessionId, this.messageStore) + return this.tapeService.exportReplaySlice(sessionId, messageId, options) + } + async mergeSubagentTape( parentSessionId: string, childSessionId: string, @@ -2235,6 +2309,7 @@ export class AgentRuntimePresenter implements IAgentImplementation { initialBlocks?: AssistantMessageBlock[] promptPreview?: string interleavedReasoning?: InterleavedReasoningConfig + viewContext?: PendingTapeViewContext }): Promise<{ runId: string; result: ProcessResult }> { const { sessionId, @@ -2245,7 +2320,8 @@ export class AgentRuntimePresenter implements IAgentImplementation { baseSystemPrompt, initialBlocks, promptPreview, - interleavedReasoning: providedInterleavedReasoning + interleavedReasoning: providedInterleavedReasoning, + viewContext } = args const state = this.runtimeState.get(sessionId) if (!state) { @@ -2309,6 +2385,7 @@ export class AgentRuntimePresenter implements IAgentImplementation { const recoverContextPressure = this.recoverRequestContextPressure.bind(this) const replaceLeadingSystemPromptInPlace = this.replaceLeadingSystemPromptInPlace.bind(this) const persistMessageTrace = this.persistMessageTrace.bind(this) + const appendTapeViewManifest = this.appendTapeViewManifest.bind(this) if (traceEnabled) { const traceAwareConfig = modelConfig as ModelConfig & { requestTraceContext?: { @@ -2342,6 +2419,7 @@ export class AgentRuntimePresenter implements IAgentImplementation { const rateLimitMessageId = this.buildRateLimitStreamMessageId(activeGeneration.runId) const emitRateLimitWaitingMessage = this.emitRateLimitWaitingMessage.bind(this) const clearRateLimitWaitingMessage = this.clearRateLimitWaitingMessage.bind(this) + let requestSeq = 0 try { this.dispatchHook('SessionStart', { @@ -2375,6 +2453,7 @@ export class AgentRuntimePresenter implements IAgentImplementation { try { let providerMessages = requestMessages let providerMaxTokens = requestMaxTokens + let recoveredFromContextPressure = false const isTtsRequest = isTtsModelConfig(requestModelConfig) || isTtsModelId(requestModelId) const effectiveRequestTools: MCPToolDefinition[] = isTtsRequest ? [] : requestTools @@ -2405,6 +2484,7 @@ export class AgentRuntimePresenter implements IAgentImplementation { minimumProtectedTailCount: 0, signal: abortController.signal }) + recoveredFromContextPressure = true requestMessages.splice(0, requestMessages.length, ...recovered.messages) if (recovered.systemPrompt) { replaceLeadingSystemPromptInPlace(requestMessages, recovered.systemPrompt) @@ -2427,6 +2507,42 @@ export class AgentRuntimePresenter implements IAgentImplementation { throw new Error('Request was not sent because the prompt became empty.') } + requestSeq += 1 + const isInitialViewRequest = requestSeq === 1 && Boolean(viewContext) + const manifestPolicy = resolveTapeViewManifestPolicy({ + recoveredFromContextPressure, + isInitialViewRequest, + viewPolicy: viewContext?.policy, + viewPolicyVersion: viewContext?.policyVersion + }) + appendTapeViewManifest({ + sessionId, + messageId, + requestSeq, + taskType: isInitialViewRequest ? viewContext!.taskType : 'tool_loop', + policy: manifestPolicy.policy, + policyVersion: manifestPolicy.policyVersion, + messages: providerMessages, + tools: effectiveRequestTools, + tokenBudget: { + contextLength: requestModelConfig.contextLength ?? contextBudgetLength, + requestedMaxTokens: requestMaxTokens, + effectiveMaxTokens: providerMaxTokens, + reserveTokens: requestMaxTokens, + toolReserveTokens: estimateToolReserveTokens(effectiveRequestTools) + }, + providerId: state.providerId, + modelId: requestModelId, + selection: + isInitialViewRequest && !recoveredFromContextPressure + ? viewContext!.selection + : undefined, + summaryCursorOrderSeq: viewContext?.summaryCursorOrderSeq ?? 1, + supportsVision: viewContext?.supportsVision ?? supportsVision, + supportsAudioInput: viewContext?.supportsAudioInput ?? supportsAudioInput, + traceDebugEnabled: viewContext?.traceDebugEnabled ?? traceEnabled + }) + await llmProviderPresenter.executeWithRateLimit(state.providerId, { signal: abortController.signal, onQueued: (snapshot) => { @@ -2578,6 +2694,59 @@ export class AgentRuntimePresenter implements IAgentImplementation { } } + private appendTapeViewManifest(params: { + sessionId: string + messageId: string + requestSeq: number + taskType: DeepChatTapeViewTaskType + policy: DeepChatTapeViewPolicy + policyVersion?: number | null + messages: ChatMessage[] + tools: MCPToolDefinition[] + tokenBudget: Omit + providerId: string + modelId: string + selection?: TapeViewContextSelection + summaryCursorOrderSeq: number + supportsVision: boolean + supportsAudioInput: boolean + traceDebugEnabled: boolean + }): void { + try { + const sourceMaps = this.tapeService.getViewManifestSourceMaps(params.sessionId) + const manifest = createTapeViewManifest({ + sessionId: params.sessionId, + messageId: params.messageId, + requestSeq: params.requestSeq, + taskType: params.taskType, + policy: params.policy, + policyVersion: params.policyVersion ?? null, + messages: params.messages, + tools: params.tools, + latestEntryId: sourceMaps.latestEntryId, + anchorEntryIds: sourceMaps.anchorEntryIds, + included: params.selection + ? buildIncludedRefs(params.selection, sourceMaps) + : buildSyntheticRequestRefs(params.messages), + excluded: params.selection ? buildExcludedRefs(params.selection, sourceMaps) : [], + tokenBudget: params.tokenBudget, + providerId: params.providerId, + modelId: params.modelId, + summaryCursorOrderSeq: params.summaryCursorOrderSeq, + supportsVision: params.supportsVision, + supportsAudioInput: params.supportsAudioInput, + traceDebugEnabled: params.traceDebugEnabled + }) + this.tapeService.appendViewManifest(manifest) + } catch (error) { + logger.warn( + `[DeepChatAgent] Failed to persist tape view manifest: ${ + error instanceof Error ? error.message : String(error) + }` + ) + } + } + private async recoverRequestContextPressure(params: { sessionId: string providerId: string @@ -3031,17 +3200,17 @@ export class AgentRuntimePresenter implements IAgentImplementation { appendSummarySection(baseSystemPrompt, summaryState.summaryText), this.sessionStore.getReconstructionAnchorPromptState(sessionId) ) - let resumeContext = buildResumeContext( + const resumeContextBuild = buildTapeResumeView({ sessionId, - messageId, + assistantMessageId: messageId, systemPrompt, - contextBudgetLength, - maxTokens, - this.messageStore, - this.supportsVision(state.providerId, state.modelId), - { + contextLength: contextBudgetLength, + reserveTokens: maxTokens, + messageStore: this.messageStore, + supportsVision: this.supportsVision(state.providerId, state.modelId), + historyRecords: tapeReady.historyRecords, + options: { summaryCursorOrderSeq: summaryState.summaryCursorOrderSeq, - historyRecords: tapeReady.historyRecords, fallbackProtectedTurnCount: 1, supportsAudioInput: this.supportsAudioInput(state.providerId, state.modelId), extraReserveTokens: toolReserveTokens, @@ -3049,7 +3218,8 @@ export class AgentRuntimePresenter implements IAgentImplementation { preserveEmptyInterleavedReasoning: interleavedReasoning.preserveEmptyReasoningContent === true } - ) + }) + let resumeContext = resumeContextBuild.messages if (budgetToolCall?.id && budgetToolCall.name && useContextBudget) { const resumeBudget = this.fitResumeBudgetForToolCall({ resumeContext, @@ -3096,7 +3266,17 @@ export class AgentRuntimePresenter implements IAgentImplementation { tools, baseSystemPrompt, initialBlocks, - interleavedReasoning + interleavedReasoning, + viewContext: { + taskType: 'resume', + policy: resumeContextBuild.policyId, + policyVersion: resumeContextBuild.policyVersion, + selection: buildTapeViewSelection(resumeContextBuild.metadata), + summaryCursorOrderSeq: summaryState.summaryCursorOrderSeq, + supportsVision: this.supportsVision(state.providerId, state.modelId), + supportsAudioInput: this.supportsAudioInput(state.providerId, state.modelId), + traceDebugEnabled: this.configPresenter.getSetting('traceDebugEnabled') === true + } }) try { this.applyProcessResultStatus(sessionId, result, runId) diff --git a/src/main/presenter/agentRuntimePresenter/tapeService.ts b/src/main/presenter/agentRuntimePresenter/tapeService.ts index c0d60ebcc..b7c157b3e 100644 --- a/src/main/presenter/agentRuntimePresenter/tapeService.ts +++ b/src/main/presenter/agentRuntimePresenter/tapeService.ts @@ -1,22 +1,35 @@ import { SQLitePresenter } from '../sqlitePresenter' import { nanoid } from 'nanoid' +import { createHash } from 'crypto' import type { AgentTapeAnchorResult, AgentTapeAnchorsOptions, AgentTapeSearchOptions, ChatMessageRecord } from '@shared/types/agent-interface' +import type { + DeepChatTapeViewManifest, + DeepChatTapeViewManifestRecord +} from '@shared/types/tape-view-manifest' +import type { + DeepChatTapeReplayEntrySnapshot, + DeepChatTapeReplayExportOptions, + DeepChatTapeReplaySlice, + DeepChatTapeReplayTraceSnapshot +} from '@shared/types/tape-replay' import type { DeepChatMessageStore } from './messageStore' import type { DeepChatTapeEntryRow, DeepChatTapeSearchInput } from '../sqlitePresenter/tables/deepchatTapeEntries' +import type { DeepChatMessageTraceRow } from '../sqlitePresenter/tables/deepchatMessageTraces' import { appendMessageRecordToTape } from './tapeFacts' import { buildEffectiveTapeView, getLastEffectiveTokenUsage, searchEffectiveTapeRows } from './tapeEffectiveView' +import { hashJson, TAPE_VIEW_MANIFEST_EVENT_NAME } from './tapeViewManifest' export type TapeMigrationState = 'none' | 'ready' @@ -57,6 +70,12 @@ export type TapeForkHandle = { forkSessionId: string } +export type TapeViewManifestSourceMaps = { + latestEntryId: number + anchorEntryIds: number[] + entryIdByMessageId: Map +} + function parseJsonObject(raw: string): Record { try { const parsed = JSON.parse(raw) as unknown @@ -168,6 +187,34 @@ function forkSessionId(parentSessionId: string, forkId: string): string { return `${parentSessionId}::fork::${forkId}` } +function hashString(value: string): string { + return createHash('sha256').update(value).digest('hex') +} + +function isPositiveInteger(value: number): boolean { + return Number.isInteger(value) && value > 0 +} + +function collectEntryIds(values: Array): number[] { + return [...new Set(values.filter((value): value is number => typeof value === 'number'))].sort( + (left, right) => left - right + ) +} + +function withReplaySliceHash( + slice: Omit & { + hashes: Omit & { sliceHash: '' } + } +): DeepChatTapeReplaySlice { + return { + ...slice, + hashes: { + ...slice.hashes, + sliceHash: hashJson(slice) + } + } +} + export class DeepChatTapeService { constructor(private readonly sqlitePresenter: SQLitePresenter) {} @@ -298,6 +345,165 @@ export class DeepChatTapeService { : [] } + getViewManifestSourceMaps(sessionId: string): TapeViewManifestSourceMaps { + const table = this.table + if (!table) { + return { + latestEntryId: 0, + anchorEntryIds: [], + entryIdByMessageId: new Map() + } + } + + const rows = table.getBySession(sessionId) + const entryIdByMessageId = new Map() + let latestEntryId = 0 + const anchorEntryIds: number[] = [] + + for (const row of rows) { + latestEntryId = Math.max(latestEntryId, row.entry_id) + if (row.kind === 'anchor') { + anchorEntryIds.push(row.entry_id) + } + if (row.kind === 'message' && row.source_type === 'message' && row.source_id) { + entryIdByMessageId.set(row.source_id, row.entry_id) + } + } + + return { + latestEntryId, + anchorEntryIds, + entryIdByMessageId + } + } + + appendViewManifest(manifest: DeepChatTapeViewManifest): DeepChatTapeEntryRow { + const table = this.table + if (!table) { + throw new Error('Tape table is not available.') + } + + table.ensureBootstrapAnchor(manifest.sessionId) + return table.appendEvent({ + sessionId: manifest.sessionId, + name: TAPE_VIEW_MANIFEST_EVENT_NAME, + source: { + type: 'runtime_event', + id: manifest.messageId, + seq: manifest.requestSeq + }, + provenanceKey: `view:${manifest.sessionId}:${manifest.messageId}:${manifest.requestSeq}:${manifest.hashes.manifestHash}`, + data: { + manifest + }, + meta: { + viewId: manifest.viewId, + requestSeq: manifest.requestSeq, + taskType: manifest.taskType, + policy: manifest.policy, + policyVersion: manifest.policyVersion + }, + createdAt: manifest.assembledAt, + idempotent: true + }) + } + + listViewManifestsByMessage( + sessionId: string, + messageId: string + ): DeepChatTapeViewManifestRecord[] { + const table = this.table + if (!table) { + return [] + } + + return table + .getBySession(sessionId) + .filter( + (row) => + row.kind === 'event' && + row.name === TAPE_VIEW_MANIFEST_EVENT_NAME && + row.source_type === 'runtime_event' && + row.source_id === messageId + ) + .map((row) => this.toViewManifestRecord(row)) + .filter((record): record is DeepChatTapeViewManifestRecord => Boolean(record)) + .sort((left, right) => right.requestSeq - left.requestSeq || right.entryId - left.entryId) + } + + exportReplaySlice( + sessionId: string, + messageId: string, + options: DeepChatTapeReplayExportOptions = {} + ): DeepChatTapeReplaySlice | null { + if (options.requestSeq !== undefined && !isPositiveInteger(options.requestSeq)) { + throw new Error('requestSeq must be a positive integer.') + } + + const table = this.table + if (!table) { + return null + } + + const manifests = this.listViewManifestsByMessage(sessionId, messageId) + const manifestRecord = + options.requestSeq === undefined + ? manifests[0] + : manifests.find((record) => record.requestSeq === options.requestSeq) + if (!manifestRecord) { + return null + } + + const manifest = manifestRecord.manifest + const includedEntryIds = collectEntryIds(manifest.included.map((ref) => ref.entryId)) + const excludedEntryIds = collectEntryIds(manifest.excluded.map((ref) => ref.entryId)) + const anchorEntryIds = collectEntryIds(manifest.anchorEntryIds) + const selectedEntryIds = new Set([ + manifestRecord.entryId, + ...includedEntryIds, + ...excludedEntryIds, + ...anchorEntryIds + ]) + const entries = table + .getBySession(sessionId) + .filter((row) => selectedEntryIds.has(row.entry_id)) + .map((row) => this.toReplayEntrySnapshot(row, options.includeTapePayloads === true)) + + const trace = this.findReplayTrace(sessionId, messageId, manifestRecord.requestSeq) + const createdAt = Date.now() + const sliceBase: Omit & { + hashes: Omit & { sliceHash: '' } + } = { + schemaVersion: 1 as const, + sliceId: `replay_${hashJson({ + sessionId, + messageId, + requestSeq: manifestRecord.requestSeq, + manifestHash: manifest.hashes.manifestHash + }).slice(0, 16)}`, + sessionId, + messageId, + requestSeq: manifestRecord.requestSeq, + mode: trace ? 'trace_bound' : 'manifest_only', + manifestRecord, + trace: trace ? this.toReplayTraceSnapshot(trace, options.includeTracePayload === true) : null, + entries, + refs: { + manifestEntryId: manifestRecord.entryId, + includedEntryIds, + excludedEntryIds, + anchorEntryIds + }, + hashes: { + manifestHash: manifest.hashes.manifestHash, + sliceHash: '' + }, + createdAt + } + + return withReplaySliceHash(sliceBase) + } + handoff( sessionId: string, name: string, @@ -586,4 +792,100 @@ export class DeepChatTapeService { createdAt: row.created_at } } + + private toViewManifestRecord(row: DeepChatTapeEntryRow): DeepChatTapeViewManifestRecord | null { + const payload = parseJsonObject(row.payload_json) + const data = payload.data + const manifest = + data && typeof data === 'object' && !Array.isArray(data) + ? (data as Record).manifest + : undefined + if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { + return null + } + const candidate = manifest as DeepChatTapeViewManifest + if ( + candidate.schemaVersion !== 1 || + candidate.sessionId !== row.session_id || + typeof candidate.messageId !== 'string' || + typeof candidate.requestSeq !== 'number' + ) { + return null + } + + return { + sessionId: row.session_id, + messageId: candidate.messageId, + requestSeq: candidate.requestSeq, + entryId: row.entry_id, + createdAt: row.created_at, + manifest: candidate + } + } + + private findReplayTrace( + sessionId: string, + messageId: string, + requestSeq: number + ): DeepChatMessageTraceRow | null { + const traceTable = this.sqlitePresenter.deepchatMessageTracesTable + if (!traceTable) { + return null + } + + return ( + traceTable + .listByMessageId(messageId) + .find((row) => row.session_id === sessionId && row.request_seq === requestSeq) ?? null + ) + } + + private toReplayEntrySnapshot( + row: DeepChatTapeEntryRow, + includePayloads: boolean + ): DeepChatTapeReplayEntrySnapshot { + const snapshot: DeepChatTapeReplayEntrySnapshot = { + entryId: row.entry_id, + kind: row.kind, + name: row.name, + sourceType: row.source_type, + sourceId: row.source_id, + sourceSeq: row.source_seq, + provenanceKey: row.provenance_key, + payloadHash: hashString(row.payload_json), + metaHash: hashString(row.meta_json), + createdAt: row.created_at + } + + if (includePayloads) { + snapshot.payload = parseJsonObject(row.payload_json) + snapshot.meta = parseJsonObject(row.meta_json) + } + + return snapshot + } + + private toReplayTraceSnapshot( + row: DeepChatMessageTraceRow, + includePayload: boolean + ): DeepChatTapeReplayTraceSnapshot { + const snapshot: DeepChatTapeReplayTraceSnapshot = { + id: row.id, + requestSeq: row.request_seq, + providerId: row.provider_id, + modelId: row.model_id, + endpoint: row.endpoint, + headersHash: hashString(row.headers_json), + bodyHash: hashString(row.body_json), + truncated: row.truncated === 1, + createdAt: row.created_at + } + + if (includePayload) { + snapshot.headersJson = row.headers_json + snapshot.bodyJson = row.body_json + } + + return snapshot + } } diff --git a/src/main/presenter/agentRuntimePresenter/tapeViewAssembler.ts b/src/main/presenter/agentRuntimePresenter/tapeViewAssembler.ts new file mode 100644 index 000000000..7da4fe492 --- /dev/null +++ b/src/main/presenter/agentRuntimePresenter/tapeViewAssembler.ts @@ -0,0 +1,85 @@ +import type { ChatMessage } from '@shared/types/core/chat-message' +import type { ChatMessageRecord } from '@shared/types/agent-interface' +import { isContextHistoryRecord, type ContextBuildMetadata } from './contextBuilder' +import { + resolveTapeViewPolicy, + type TapeChatViewPolicyInput, + type TapeResumeViewPolicyInput, + type TapeViewPolicy, + type TapeViewPolicyId, + type TapeViewPolicySelection, + type TapeViewPolicySelectionReason +} from './tapeViewPolicy' + +export const TAPE_VIEW_ASSEMBLER_VERSION = 'tape-view-assembler-v1' as const +export const TAPE_VIEW_HISTORY_SOURCE = 'tape_effective_view' as const + +export interface TapeViewAssemblerResult { + messages: ChatMessage[] + metadata: ContextBuildMetadata + assemblerVersion: typeof TAPE_VIEW_ASSEMBLER_VERSION + historySource: typeof TAPE_VIEW_HISTORY_SOURCE + historyRecords: ChatMessageRecord[] + policyId: TapeViewPolicyId + policyVersion: TapeViewPolicy['version'] + policySelectionReason: TapeViewPolicySelectionReason +} + +export interface TapeChatViewAssemblerInput extends TapeChatViewPolicyInput { + policy?: TapeViewPolicy + requestedPolicyId?: string | null +} + +export interface TapeResumeViewAssemblerInput extends TapeResumeViewPolicyInput { + policy?: TapeViewPolicy + requestedPolicyId?: string | null +} + +export function getTapeContextHistoryRecords(records: ChatMessageRecord[]): ChatMessageRecord[] { + return records.filter(isContextHistoryRecord) +} + +function withAssemblerMetadata( + result: { messages: ChatMessage[]; metadata: ContextBuildMetadata }, + historyRecords: ChatMessageRecord[], + selection: TapeViewPolicySelection +): TapeViewAssemblerResult { + return { + ...result, + assemblerVersion: TAPE_VIEW_ASSEMBLER_VERSION, + historySource: TAPE_VIEW_HISTORY_SOURCE, + historyRecords, + policyId: selection.policy.id, + policyVersion: selection.policy.version, + policySelectionReason: selection.reason + } +} + +function resolveAssemblerPolicy(input: { + policy?: TapeViewPolicy + requestedPolicyId?: string | null +}): TapeViewPolicySelection { + if (input.policy) { + return { + policy: input.policy, + requestedPolicyId: input.requestedPolicyId ?? null, + reason: 'injected' + } + } + + return resolveTapeViewPolicy({ requestedPolicyId: input.requestedPolicyId }) +} + +export function buildTapeChatView(input: TapeChatViewAssemblerInput): TapeViewAssemblerResult { + const selection = resolveAssemblerPolicy(input) + const result = selection.policy.buildChat(input) + + return withAssemblerMetadata(result, input.historyRecords, selection) +} + +export function buildTapeResumeView(input: TapeResumeViewAssemblerInput): TapeViewAssemblerResult { + const selection = resolveAssemblerPolicy(input) + const result = selection.policy.buildResume(input) + + return withAssemblerMetadata(result, input.historyRecords, selection) +} diff --git a/src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts b/src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts new file mode 100644 index 000000000..e4d6145d8 --- /dev/null +++ b/src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts @@ -0,0 +1,270 @@ +import { createHash } from 'crypto' +import type { ChatMessage } from '@shared/types/core/chat-message' +import type { MCPToolDefinition } from '@shared/types/core/mcp' +import type { ChatMessageRecord, MessageMetadata } from '@shared/types/agent-interface' +import type { + DeepChatTapeViewEntryRef, + DeepChatTapeViewExcludedRef, + DeepChatTapeViewManifest, + DeepChatTapeViewPolicy, + DeepChatTapeViewTaskType, + DeepChatTapeViewTokenBudget +} from '@shared/types/tape-view-manifest' +import { estimateMessagesTokens } from './contextBuilder' + +export const TAPE_VIEW_MANIFEST_EVENT_NAME = 'view/assembled' +export const TAPE_VIEW_CONTEXT_BUILDER_VERSION = 'legacy-v1' as const + +export type TapeViewManifestSourceMaps = { + entryIdByMessageId?: Map +} + +export type TapeViewManifestBuildInput = { + viewId?: string + sessionId: string + messageId: string + requestSeq: number + taskType: DeepChatTapeViewTaskType + policy: DeepChatTapeViewPolicy + policyVersion?: number | null + messages: ChatMessage[] + tools: MCPToolDefinition[] + latestEntryId: number + anchorEntryIds: number[] + included: DeepChatTapeViewEntryRef[] + excluded: DeepChatTapeViewExcludedRef[] + tokenBudget: Omit + providerId: string + modelId: string + summaryCursorOrderSeq: number + supportsVision: boolean + supportsAudioInput: boolean + traceDebugEnabled: boolean + assembledAt?: number +} + +export type TapeViewManifestPolicyInput = { + recoveredFromContextPressure: boolean + isInitialViewRequest: boolean + viewPolicy?: DeepChatTapeViewPolicy + viewPolicyVersion?: number | null +} + +export type TapeViewManifestPolicyResult = { + policy: DeepChatTapeViewPolicy + policyVersion: number | null +} + +export type TapeViewContextSelection = { + includedRecords: Array<{ + record: ChatMessageRecord + reason: DeepChatTapeViewEntryRef['reason'] + }> + excludedRecords: Array<{ + record: ChatMessageRecord + reason: DeepChatTapeViewExcludedRef['reason'] + }> + includesSystemPrompt: boolean + newUserMessageId?: string | null +} + +export function resolveTapeViewManifestPolicy( + input: TapeViewManifestPolicyInput +): TapeViewManifestPolicyResult { + if (input.recoveredFromContextPressure) { + return { + policy: 'context_pressure_recovery_shadow', + policyVersion: null + } + } + + if (input.isInitialViewRequest && input.viewPolicy) { + return { + policy: input.viewPolicy, + policyVersion: input.viewPolicyVersion ?? null + } + } + + return { + policy: 'tool_loop_shadow', + policyVersion: null + } +} + +function normalizeForStableJson(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(normalizeForStableJson) + } + + if (!value || typeof value !== 'object') { + return value + } + + const record = value as Record + return Object.keys(record) + .sort() + .reduce>((result, key) => { + const nested = record[key] + if (nested !== undefined) { + result[key] = normalizeForStableJson(nested) + } + return result + }, {}) +} + +export function stableJsonStringify(value: unknown): string { + return JSON.stringify(normalizeForStableJson(value)) +} + +export function hashJson(value: unknown): string { + return createHash('sha256').update(stableJsonStringify(value)).digest('hex') +} + +function buildViewId(input: TapeViewManifestBuildInput, assembledAt: number): string { + return `view_${hashJson({ + sessionId: input.sessionId, + messageId: input.messageId, + requestSeq: input.requestSeq, + policy: input.policy, + assembledAt + }).slice(0, 16)}` +} + +function attachManifestHash( + manifest: Omit & { + hashes: Omit & { manifestHash: '' } + } +): DeepChatTapeViewManifest { + const manifestForHash = { + ...manifest, + hashes: { + ...manifest.hashes, + manifestHash: '' + } + } + return { + ...manifest, + hashes: { + ...manifest.hashes, + manifestHash: hashJson(manifestForHash) + } + } +} + +export function createTapeViewManifest( + input: TapeViewManifestBuildInput +): DeepChatTapeViewManifest { + const assembledAt = input.assembledAt ?? Date.now() + const viewId = input.viewId ?? buildViewId(input, assembledAt) + const manifest: Omit & { + hashes: Omit & { manifestHash: '' } + } = { + schemaVersion: 1 as const, + viewId, + sessionId: input.sessionId, + messageId: input.messageId, + requestSeq: input.requestSeq, + taskType: input.taskType, + policy: input.policy, + policyVersion: input.policyVersion ?? null, + contextBuilderVersion: TAPE_VIEW_CONTEXT_BUILDER_VERSION, + latestEntryId: input.latestEntryId, + anchorEntryIds: [...input.anchorEntryIds], + included: input.included, + excluded: input.excluded, + tokenBudget: { + ...input.tokenBudget, + estimatedPromptTokens: estimateMessagesTokens(input.messages) + }, + hashes: { + promptHash: hashJson(input.messages), + toolDefinitionsHash: hashJson(input.tools), + manifestHash: '' + }, + meta: { + providerId: input.providerId, + modelId: input.modelId, + summaryCursorOrderSeq: input.summaryCursorOrderSeq, + supportsVision: input.supportsVision, + supportsAudioInput: input.supportsAudioInput, + traceDebugEnabled: input.traceDebugEnabled + }, + assembledAt + } + + return attachManifestHash(manifest) +} + +export function buildIncludedRefs( + selection: TapeViewContextSelection, + sourceMaps: TapeViewManifestSourceMaps = {} +): DeepChatTapeViewEntryRef[] { + const refs: DeepChatTapeViewEntryRef[] = [] + + if (selection.includesSystemPrompt) { + refs.push({ + entryId: null, + messageId: null, + orderSeq: null, + role: 'system', + source: 'synthetic', + reason: 'system_prompt' + }) + } + + for (const item of selection.includedRecords) { + refs.push({ + entryId: sourceMaps.entryIdByMessageId?.get(item.record.id) ?? null, + messageId: item.record.id, + orderSeq: item.record.orderSeq, + role: item.record.role, + source: sourceMaps.entryIdByMessageId?.has(item.record.id) ? 'tape' : 'synthetic', + reason: item.reason + }) + } + + if (selection.newUserMessageId) { + refs.push({ + entryId: sourceMaps.entryIdByMessageId?.get(selection.newUserMessageId) ?? null, + messageId: selection.newUserMessageId, + orderSeq: null, + role: 'user', + source: sourceMaps.entryIdByMessageId?.has(selection.newUserMessageId) ? 'tape' : 'synthetic', + reason: 'new_user_input' + }) + } + + return refs +} + +export function buildExcludedRefs( + selection: TapeViewContextSelection, + sourceMaps: TapeViewManifestSourceMaps = {} +): DeepChatTapeViewExcludedRef[] { + return selection.excludedRecords.map((item) => ({ + entryId: sourceMaps.entryIdByMessageId?.get(item.record.id) ?? null, + messageId: item.record.id, + orderSeq: item.record.orderSeq, + reason: item.reason + })) +} + +export function buildSyntheticRequestRefs(messages: ChatMessage[]): DeepChatTapeViewEntryRef[] { + return messages.map((message) => ({ + entryId: null, + messageId: null, + orderSeq: null, + role: message.role, + source: 'synthetic', + reason: message.role === 'tool' ? 'tool_loop_message' : 'selected_history' + })) +} + +export function isCompactionRecord(record: ChatMessageRecord): boolean { + try { + const metadata = JSON.parse(record.metadata) as MessageMetadata + return metadata.messageType === 'compaction' + } catch { + return false + } +} diff --git a/src/main/presenter/agentRuntimePresenter/tapeViewPolicy.ts b/src/main/presenter/agentRuntimePresenter/tapeViewPolicy.ts new file mode 100644 index 000000000..038d73d27 --- /dev/null +++ b/src/main/presenter/agentRuntimePresenter/tapeViewPolicy.ts @@ -0,0 +1,148 @@ +import type { ChatMessageRecord, SendMessageInput } from '@shared/types/agent-interface' +import type { DeepChatMessageStore } from './messageStore' +import { + buildContextWithMetadata, + buildResumeContextWithMetadata, + type ContextBuildOptions, + type ContextBuildResult +} from './contextBuilder' + +export const LEGACY_TAPE_VIEW_POLICY_ID = 'legacy_context_v1' as const +export const LEGACY_TAPE_VIEW_POLICY_VERSION = 1 as const +export const DEFAULT_TAPE_VIEW_POLICY_ID = LEGACY_TAPE_VIEW_POLICY_ID + +export type TapeViewPolicyId = typeof LEGACY_TAPE_VIEW_POLICY_ID +export type TapeViewPolicySelectionReason = + | 'default' + | 'requested' + | 'fallback_default' + | 'injected' + +export interface TapeChatViewPolicyInput { + sessionId: string + newUserContent: string | SendMessageInput + systemPrompt: string + contextLength: number + reserveTokens: number + messageStore: DeepChatMessageStore + supportsVision: boolean + historyRecords: ChatMessageRecord[] + options?: Omit +} + +export interface TapeResumeViewPolicyInput { + sessionId: string + assistantMessageId: string + systemPrompt: string + contextLength: number + reserveTokens: number + messageStore: DeepChatMessageStore + supportsVision: boolean + historyRecords: ChatMessageRecord[] + options?: Omit +} + +export interface TapeViewPolicy { + id: TapeViewPolicyId + version: typeof LEGACY_TAPE_VIEW_POLICY_VERSION + buildChat(input: TapeChatViewPolicyInput): ContextBuildResult + buildResume(input: TapeResumeViewPolicyInput): ContextBuildResult +} + +export interface TapeViewPolicySelectionInput { + requestedPolicyId?: string | null +} + +export interface TapeViewPolicySelection { + policy: TapeViewPolicy + requestedPolicyId: string | null + reason: TapeViewPolicySelectionReason +} + +export const legacyTapeViewPolicy: TapeViewPolicy = { + id: LEGACY_TAPE_VIEW_POLICY_ID, + version: LEGACY_TAPE_VIEW_POLICY_VERSION, + buildChat(input) { + return buildContextWithMetadata( + input.sessionId, + input.newUserContent, + input.systemPrompt, + input.contextLength, + input.reserveTokens, + input.messageStore, + input.supportsVision, + { + ...input.options, + historyRecords: input.historyRecords + } + ) + }, + buildResume(input) { + return buildResumeContextWithMetadata( + input.sessionId, + input.assistantMessageId, + input.systemPrompt, + input.contextLength, + input.reserveTokens, + input.messageStore, + input.supportsVision, + { + ...input.options, + historyRecords: input.historyRecords + } + ) + } +} + +const BUILTIN_TAPE_VIEW_POLICIES: Record = { + [LEGACY_TAPE_VIEW_POLICY_ID]: legacyTapeViewPolicy +} + +export function listTapeViewPolicies(): TapeViewPolicy[] { + return Object.values(BUILTIN_TAPE_VIEW_POLICIES) +} + +export function getTapeViewPolicy(policyId: string | null | undefined): TapeViewPolicy | null { + if (!policyId) { + return null + } + + const normalizedPolicyId = policyId.trim() + if (!normalizedPolicyId) { + return null + } + + return BUILTIN_TAPE_VIEW_POLICIES[normalizedPolicyId as TapeViewPolicyId] ?? null +} + +export function resolveTapeViewPolicy( + input: TapeViewPolicySelectionInput = {} +): TapeViewPolicySelection { + const requestedPolicyId = + typeof input.requestedPolicyId === 'string' && input.requestedPolicyId.trim() + ? input.requestedPolicyId.trim() + : null + + if (requestedPolicyId) { + const requestedPolicy = getTapeViewPolicy(requestedPolicyId) + if (requestedPolicy) { + return { + policy: requestedPolicy, + requestedPolicyId, + reason: 'requested' + } + } + + return { + policy: BUILTIN_TAPE_VIEW_POLICIES[DEFAULT_TAPE_VIEW_POLICY_ID], + requestedPolicyId, + reason: 'fallback_default' + } + } + + return { + policy: BUILTIN_TAPE_VIEW_POLICIES[DEFAULT_TAPE_VIEW_POLICY_ID], + requestedPolicyId: null, + reason: 'default' + } +} diff --git a/src/main/presenter/agentSessionPresenter/index.ts b/src/main/presenter/agentSessionPresenter/index.ts index 8d6ad3a99..6fd29c4c2 100644 --- a/src/main/presenter/agentSessionPresenter/index.ts +++ b/src/main/presenter/agentSessionPresenter/index.ts @@ -39,6 +39,11 @@ import type { } from '@shared/types/agent-interface' import type { Message } from '@shared/chat' import type { SearchResult } from '@shared/types/core/search' +import type { DeepChatTapeViewManifestRecord } from '@shared/types/tape-view-manifest' +import type { + DeepChatTapeReplayExportOptions, + DeepChatTapeReplaySlice +} from '@shared/types/tape-replay' import type { AcpConfigState, IConfigPresenter, @@ -1427,6 +1432,45 @@ export class AgentSessionPresenter { return await agent.handoffTape(sessionId, name, state) } + async listMessageViewManifests(messageId: string): Promise { + const normalizedMessageId = messageId?.trim() + if (!normalizedMessageId) return [] + + const message = this.sqlitePresenter.deepchatMessagesTable.get(normalizedMessageId) + if (!message) return [] + + const session = this.sessionManager.get(message.session_id) + if (!session) return [] + + const agent = await this.resolveAgentImplementation(session.agentId) + if (!agent.listMessageViewManifests) return [] + + return await agent.listMessageViewManifests(message.session_id, normalizedMessageId) + } + + async exportMessageTapeReplaySlice( + messageId: string, + options?: DeepChatTapeReplayExportOptions + ): Promise { + const normalizedMessageId = messageId?.trim() + if (!normalizedMessageId) return null + + const message = this.sqlitePresenter.deepchatMessagesTable.get(normalizedMessageId) + if (!message) return null + + const session = this.sessionManager.get(message.session_id) + if (!session) return null + + const agent = await this.resolveAgentImplementation(session.agentId) + if (!agent.exportMessageTapeReplaySlice) return null + + return await agent.exportMessageTapeReplaySlice( + message.session_id, + normalizedMessageId, + options + ) + } + async mergeSubagentTape( parentSessionId: string, childSessionId: string, diff --git a/src/main/routes/index.ts b/src/main/routes/index.ts index 72ee38504..35f0ab6c9 100644 --- a/src/main/routes/index.ts +++ b/src/main/routes/index.ts @@ -183,6 +183,7 @@ import { sessionsDeactivateRoute, sessionsEditUserMessageRoute, sessionsEnsureAcpDraftRoute, + sessionsExportMessageTapeReplaySliceRoute, sessionsExportRoute, sessionsForkRoute, sessionsGetAcpSessionCommandsRoute, @@ -2228,7 +2229,19 @@ export async function dispatchDeepchatRoute( case sessionsListMessageTracesRoute.name: { const input = sessionsListMessageTracesRoute.input.parse(rawInput) const traces = await runtime.agentSessionPresenter.listMessageTraces(input.messageId) - return sessionsListMessageTracesRoute.output.parse({ traces }) + const manifests = await runtime.agentSessionPresenter.listMessageViewManifests( + input.messageId + ) + return sessionsListMessageTracesRoute.output.parse({ traces, manifests }) + } + + case sessionsExportMessageTapeReplaySliceRoute.name: { + const input = sessionsExportMessageTapeReplaySliceRoute.input.parse(rawInput) + const slice = await runtime.agentSessionPresenter.exportMessageTapeReplaySlice( + input.messageId, + input.options + ) + return sessionsExportMessageTapeReplaySliceRoute.output.parse({ slice }) } case sessionsTranslateTextRoute.name: { diff --git a/src/renderer/api/SessionClient.ts b/src/renderer/api/SessionClient.ts index fecc7e983..0858db16f 100644 --- a/src/renderer/api/SessionClient.ts +++ b/src/renderer/api/SessionClient.ts @@ -22,6 +22,7 @@ import { sessionsDeactivateRoute, sessionsEditUserMessageRoute, sessionsEnsureAcpDraftRoute, + sessionsExportMessageTapeReplaySliceRoute, sessionsExportRoute, sessionsForkRoute, sessionsGetAcpSessionCommandsRoute, @@ -64,6 +65,11 @@ import { sessionsUpdateQueuedInputRoute } from '@shared/contracts/routes' import type { CreateSessionInput, SendMessageInput } from '@shared/types/agent-interface' +import type { DeepChatTapeViewManifestRecord } from '@shared/types/tape-view-manifest' +import type { + DeepChatTapeReplayExportOptions, + DeepChatTapeReplaySlice +} from '@shared/types/tape-replay' import { getDeepchatBridge } from './core' export function createSessionClient(bridge: DeepchatBridge = getDeepchatBridge()) { @@ -238,6 +244,30 @@ export function createSessionClient(bridge: DeepchatBridge = getDeepchatBridge() return result.traces } + async function listMessageTraceDiagnostics(messageId: string) { + const result = await bridge.invoke(sessionsListMessageTracesRoute.name, { messageId }) + return { + traces: result.traces, + manifests: result.manifests as unknown as DeepChatTapeViewManifestRecord[] + } + } + + async function listMessageViewManifests(messageId: string) { + const result = await bridge.invoke(sessionsListMessageTracesRoute.name, { messageId }) + return result.manifests as unknown as DeepChatTapeViewManifestRecord[] + } + + async function exportMessageTapeReplaySlice( + messageId: string, + options?: DeepChatTapeReplayExportOptions + ): Promise { + const result = await bridge.invoke(sessionsExportMessageTapeReplaySliceRoute.name, { + messageId, + options + }) + return result.slice + } + async function translateText(text: string, locale?: string, agentId?: string) { const result = await bridge.invoke(sessionsTranslateTextRoute.name, { text, @@ -526,6 +556,9 @@ export function createSessionClient(bridge: DeepchatBridge = getDeepchatBridge() searchHistory, getSearchResults, listMessageTraces, + listMessageTraceDiagnostics, + listMessageViewManifests, + exportMessageTapeReplaySlice, translateText, getAgents, getUsageDashboard, diff --git a/src/renderer/settings/components/ProviderConfigImportDialog.vue b/src/renderer/settings/components/ProviderConfigImportDialog.vue index 76274d421..133f8d674 100644 --- a/src/renderer/settings/components/ProviderConfigImportDialog.vue +++ b/src/renderer/settings/components/ProviderConfigImportDialog.vue @@ -638,7 +638,7 @@ const runScan = async () => { applyError.value = '' selectedProviderApiTypes.value = {} try { - const result = await providerClient.scanProviderImports() + const result = (await providerClient.scanProviderImports()) as ProviderImportScanResult scanResult.value = result selectedSources.value = new Set( result.sources diff --git a/src/renderer/src/components/mcp-config/AgentMcpSelector.vue b/src/renderer/src/components/mcp-config/AgentMcpSelector.vue index 13e115d99..3adc5e629 100644 --- a/src/renderer/src/components/mcp-config/AgentMcpSelector.vue +++ b/src/renderer/src/components/mcp-config/AgentMcpSelector.vue @@ -36,10 +36,10 @@ const isPluginOwnedServerConfig = (config: AgentMcpServerConfig): boolean => const load = async () => { loading.value = true try { - const [servers, currentSelections] = await Promise.all([ + const [servers, currentSelections] = (await Promise.all([ configClient.getMcpServers(), configClient.getAcpSharedMcpSelections() - ]) + ])) as [Record, string[]] availableServers.value = Object.entries(servers ?? {}) .filter(([, config]) => !isPluginOwnedServerConfig(config)) diff --git a/src/renderer/src/components/trace/TraceDialog.vue b/src/renderer/src/components/trace/TraceDialog.vue index 18481ab18..92193af6c 100644 --- a/src/renderer/src/components/trace/TraceDialog.vue +++ b/src/renderer/src/components/trace/TraceDialog.vue @@ -16,21 +16,21 @@

{{ t('traceDialog.errorDesc') }}

-
-
+
+
-
+
{{ t('traceDialog.endpoint') }}:
{{ selectedTrace.endpoint }} @@ -39,39 +39,195 @@
{{ t('traceDialog.provider') }}: - {{ selectedTrace.providerId }} + {{ diagnosticProviderId || '-' }}
{{ t('traceDialog.model') }}: - {{ selectedTrace.modelId }} + {{ diagnosticModelId || '-' }}
-
-
- {{ t('traceDialog.body') }} -
-
-
-
-
{{ formattedJson }}
+ + +
+
+
+
{{ formattedJson }}
+
-
-
+
+ +

{{ t('traceDialog.requestUnavailable') }}

+

+ {{ t('traceDialog.requestUnavailableDesc') }} +

+
+ + + +
+
+
{{ item.label }}
+
{{ item.value }}
+
+
+
+ +

{{ t('traceDialog.manifestUnavailable') }}

+

+ {{ t('traceDialog.manifestUnavailableDesc') }} +

+
+
+ + +
+
+

{{ t('traceDialog.includedEntries') }}

+
+ + + + + + + + + + + + + + + + + + + + + +
{{ t('traceDialog.entryId') }}{{ t('traceDialog.messageId') }}{{ t('traceDialog.orderSeq') }}{{ t('traceDialog.role') }}{{ t('traceDialog.source') }}{{ t('traceDialog.reason') }}
{{ formatNullable(entry.entryId) }} + {{ formatNullable(entry.messageId) }} + {{ formatNullable(entry.orderSeq) }}{{ formatNullable(entry.role) }}{{ entry.source }}{{ entry.reason }}
+
+
+ +
+

{{ t('traceDialog.excludedEntries') }}

+
+ + + + + + + + + + + + + + + + + +
{{ t('traceDialog.entryId') }}{{ t('traceDialog.messageId') }}{{ t('traceDialog.orderSeq') }}{{ t('traceDialog.reason') }}
{{ formatNullable(entry.entryId) }} + {{ formatNullable(entry.messageId) }} + {{ formatNullable(entry.orderSeq) }}{{ entry.reason }}
+
+
+
+
+ +

{{ t('traceDialog.manifestUnavailable') }}

+

+ {{ t('traceDialog.manifestUnavailableDesc') }} +

+
+
+ + +
+
+
{{ item.label }}
+
{{ item.value }}
+
+
+
+ +

{{ t('traceDialog.manifestUnavailable') }}

+

+ {{ t('traceDialog.manifestUnavailableDesc') }} +

+
+
+ +
+ +
+ +

{{ t('traceDialog.empty') }}

+

{{ t('traceDialog.emptyDesc') }}

@@ -91,6 +247,7 @@ import { DialogFooter } from '@shadcn/components/ui/dialog' import { Button } from '@shadcn/components/ui/button' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@shadcn/components/ui/tabs' import { Spinner } from '@shadcn/components/ui/spinner' import { Icon } from '@iconify/vue' import { useI18n } from 'vue-i18n' @@ -99,6 +256,9 @@ import { createSessionClient } from '@api/SessionClient' import { useMonaco } from 'stream-monaco' import { useUiSettingsStore } from '@/stores/uiSettingsStore' import type { MessageTraceRecord } from '@shared/types/agent-interface' +import type { DeepChatTapeViewManifestRecord } from '@shared/types/tape-view-manifest' + +type DiagnosticTab = 'request' | 'view' | 'entries' | 'budget' const { t } = useI18n() const deviceClient = createDeviceClient() @@ -140,15 +300,41 @@ const error = ref(false) const copySuccess = ref(false) const requestId = ref(0) const traceList = ref([]) -const selectedTraceId = ref(null) +const manifestList = ref([]) +const selectedRequestSeq = ref(null) +const activeTab = ref('request') + +const diagnosticTabs: Array<{ id: DiagnosticTab; labelKey: string }> = [ + { id: 'request', labelKey: 'traceDialog.tabs.request' }, + { id: 'view', labelKey: 'traceDialog.tabs.view' }, + { id: 'entries', labelKey: 'traceDialog.tabs.entries' }, + { id: 'budget', labelKey: 'traceDialog.tabs.budget' } +] + +const requestOptions = computed(() => { + const seqs = new Set() + for (const trace of traceList.value) { + seqs.add(trace.requestSeq) + } + for (const manifest of manifestList.value) { + seqs.add(manifest.requestSeq) + } + return [...seqs] + .sort((left, right) => right - left) + .map((requestSeq) => ({ + requestSeq + })) +}) + +const hasDiagnostics = computed(() => traceList.value.length > 0 || manifestList.value.length > 0) const selectedTrace = computed(() => { if (!traceList.value.length) { return null } - if (selectedTraceId.value) { - const matched = traceList.value.find((item) => item.id === selectedTraceId.value) + if (selectedRequestSeq.value !== null) { + const matched = traceList.value.find((item) => item.requestSeq === selectedRequestSeq.value) if (matched) { return matched } @@ -157,6 +343,29 @@ const selectedTrace = computed(() => { return traceList.value[0] ?? null }) +const selectedManifest = computed(() => { + if (!manifestList.value.length) { + return null + } + + if (selectedRequestSeq.value !== null) { + const matched = manifestList.value.find((item) => item.requestSeq === selectedRequestSeq.value) + if (matched) { + return matched + } + } + + return manifestList.value[0] ?? null +}) + +const diagnosticProviderId = computed( + () => selectedTrace.value?.providerId ?? selectedManifest.value?.manifest.meta.providerId ?? '' +) + +const diagnosticModelId = computed( + () => selectedTrace.value?.modelId ?? selectedManifest.value?.manifest.meta.modelId ?? '' +) + const parsedHeaders = computed(() => { if (!selectedTrace.value) return {} try { @@ -187,6 +396,65 @@ const formattedJson = computed(() => { return JSON.stringify(fullData, null, 2) }) +const activeTabLabel = computed(() => { + const tab = diagnosticTabs.find((item) => item.id === activeTab.value) + return tab ? t(tab.labelKey) : t('traceDialog.body') +}) + +const activeJson = computed(() => { + if (activeTab.value === 'request') { + return formattedJson.value + } + if (!selectedManifest.value) { + return '' + } + if (activeTab.value === 'view') { + return JSON.stringify(selectedManifest.value.manifest, null, 2) + } + if (activeTab.value === 'entries') { + return JSON.stringify( + { + included: selectedManifest.value.manifest.included, + excluded: selectedManifest.value.manifest.excluded + }, + null, + 2 + ) + } + return JSON.stringify(selectedManifest.value.manifest.tokenBudget, null, 2) +}) + +const manifestOverview = computed(() => { + const manifest = selectedManifest.value?.manifest + if (!manifest) return [] + return [ + { label: t('traceDialog.viewId'), value: manifest.viewId }, + { label: t('traceDialog.policy'), value: manifest.policy }, + ...(typeof manifest.policyVersion === 'number' + ? [{ label: t('traceDialog.policyVersion'), value: String(manifest.policyVersion) }] + : []), + { label: t('traceDialog.taskType'), value: manifest.taskType }, + { label: t('traceDialog.requestSeq'), value: String(manifest.requestSeq) }, + { label: t('traceDialog.latestEntryId'), value: String(manifest.latestEntryId) }, + { label: t('traceDialog.promptHash'), value: manifest.hashes.promptHash }, + { label: t('traceDialog.toolDefinitionsHash'), value: manifest.hashes.toolDefinitionsHash }, + { label: t('traceDialog.manifestHash'), value: manifest.hashes.manifestHash } + ] +}) + +const tokenBudgetItems = computed(() => { + const budget = selectedManifest.value?.manifest.tokenBudget + if (!budget) return [] + return [ + { label: t('traceDialog.contextLength'), value: budget.contextLength }, + { label: t('traceDialog.requestedMaxTokens'), value: budget.requestedMaxTokens }, + { label: t('traceDialog.effectiveMaxTokens'), value: budget.effectiveMaxTokens }, + { label: t('traceDialog.reserveTokens'), value: budget.reserveTokens }, + { label: t('traceDialog.toolReserveTokens'), value: budget.toolReserveTokens }, + { label: t('traceDialog.estimatedPromptTokens'), value: budget.estimatedPromptTokens } + ] +}) + watch( () => props.messageId, async (newMessageId) => { @@ -215,10 +483,17 @@ const applyFontFamily = (fontFamily: string) => { } } +watch(activeTab, (tab) => { + if (tab !== 'request') { + cleanupEditor() + editorInitialized.value = false + } +}) + watch( - [isOpen, selectedTrace, formattedJson, jsonEditor], - async ([open, trace, json, editorEl]) => { - if (open && trace && json && editorEl) { + [isOpen, activeTab, selectedTrace, formattedJson, jsonEditor], + async ([open, tab, trace, json, editorEl]) => { + if (open && tab === 'request' && trace && json && editorEl) { await nextTick() await nextTick() const hasEditor = editorEl.querySelector('.monaco-editor') @@ -239,7 +514,13 @@ watch( ) onMounted(async () => { - if (isOpen.value && selectedTrace.value && formattedJson.value && jsonEditor.value) { + if ( + isOpen.value && + activeTab.value === 'request' && + selectedTrace.value && + formattedJson.value && + jsonEditor.value + ) { await nextTick() await nextTick() if (!jsonEditor.value.querySelector('.monaco-editor') && !editorInitialized.value) { @@ -273,21 +554,21 @@ const loadTraces = async (messageId: string) => { loading.value = true error.value = false traceList.value = [] - selectedTraceId.value = null + manifestList.value = [] + selectedRequestSeq.value = null + activeTab.value = 'request' try { - const result = await sessionClient.listMessageTraces(messageId) + const { traces, manifests } = await sessionClient.listMessageTraceDiagnostics(messageId) if (currentRequestId !== requestId.value) { return } - if (!Array.isArray(result) || result.length === 0) { - error.value = true - return - } - - traceList.value = result - selectedTraceId.value = result[0].id + traceList.value = Array.isArray(traces) ? traces : [] + manifestList.value = Array.isArray(manifests) ? manifests : [] + selectedRequestSeq.value = + traceList.value[0]?.requestSeq ?? manifestList.value[0]?.requestSeq ?? null + activeTab.value = traceList.value.length > 0 ? 'request' : 'view' } catch (err) { if (currentRequestId === requestId.value) { console.error('Failed to load message traces:', err) @@ -301,9 +582,9 @@ const loadTraces = async (messageId: string) => { } const copyJson = async () => { - if (!formattedJson.value) return + if (!activeJson.value) return try { - deviceClient.copyText(formattedJson.value) + deviceClient.copyText(activeJson.value) copySuccess.value = true setTimeout(() => { copySuccess.value = false @@ -318,11 +599,20 @@ const resetState = () => { error.value = false copySuccess.value = false traceList.value = [] - selectedTraceId.value = null + manifestList.value = [] + selectedRequestSeq.value = null + activeTab.value = 'request' cleanupEditor() editorInitialized.value = false } +const formatNullable = (value: string | number | null): string => { + if (value === null) { + return '-' + } + return String(value) +} + const close = () => { isOpen.value = false resetState() diff --git a/src/renderer/src/i18n/da-DK/traceDialog.json b/src/renderer/src/i18n/da-DK/traceDialog.json index 7a744a334..4ba41d6dc 100644 --- a/src/renderer/src/i18n/da-DK/traceDialog.json +++ b/src/renderer/src/i18n/da-DK/traceDialog.json @@ -13,5 +13,40 @@ "errorDesc": "Kan ikke hente anmodningsforhåndsvisning, prøv igen", "notImplemented": "Ikke understøttet", "notImplementedDesc": "Denne udbyder understøtter endnu ikke forhåndsvisning", - "mayNotMatch": "Bemærk: Denne forhåndsvisning er rekonstrueret ud fra de nuværende samtaleindstillinger og matcher muligvis ikke de faktiske anmodningsparametre helt præcist" + "mayNotMatch": "Bemærk: Denne forhåndsvisning er rekonstrueret ud fra de nuværende samtaleindstillinger og matcher muligvis ikke de faktiske anmodningsparametre helt præcist", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/de-DE/traceDialog.json b/src/renderer/src/i18n/de-DE/traceDialog.json index f863240f2..bd9662a66 100644 --- a/src/renderer/src/i18n/de-DE/traceDialog.json +++ b/src/renderer/src/i18n/de-DE/traceDialog.json @@ -13,5 +13,40 @@ "errorDesc": "Vorschauinformationen konnten nicht abgerufen werden. Bitte erneut versuchen", "notImplemented": "Noch nicht unterstützt", "notImplementedDesc": "Für diesen Anbieter ist die Vorschau noch nicht verfügbar", - "mayNotMatch": "Hinweis: Diese Vorschau wird aus den aktuellen Sitzungseinstellungen rekonstruiert und stimmt möglicherweise nicht vollständig mit den tatsächlich gesendeten Parametern überein" + "mayNotMatch": "Hinweis: Diese Vorschau wird aus den aktuellen Sitzungseinstellungen rekonstruiert und stimmt möglicherweise nicht vollständig mit den tatsächlich gesendeten Parametern überein", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/en-US/traceDialog.json b/src/renderer/src/i18n/en-US/traceDialog.json index 7884345ec..0a0539cf6 100644 --- a/src/renderer/src/i18n/en-US/traceDialog.json +++ b/src/renderer/src/i18n/en-US/traceDialog.json @@ -13,5 +13,40 @@ "errorDesc": "Unable to fetch request preview, please try again", "notImplemented": "Not supported", "notImplementedDesc": "This provider has not implemented request preview yet", - "mayNotMatch": "Note: This preview is reconstructed from current conversation settings and may not exactly match the actual request parameters" + "mayNotMatch": "Note: This preview is reconstructed from current conversation settings and may not exactly match the actual request parameters", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/es-ES/traceDialog.json b/src/renderer/src/i18n/es-ES/traceDialog.json index dc5e8211d..a311a80ae 100644 --- a/src/renderer/src/i18n/es-ES/traceDialog.json +++ b/src/renderer/src/i18n/es-ES/traceDialog.json @@ -13,5 +13,40 @@ "errorDesc": "No se puede obtener la vista previa de la solicitud. Inténtelo de nuevo.", "notImplemented": "No compatible", "notImplementedDesc": "Este provider aún no ha implementado la vista previa de la solicitud.", - "mayNotMatch": "Nota: Esta vista previa se reconstruye a partir de la configuración de conversación actual y es posible que no coincida exactamente con los parámetros de solicitud reales." + "mayNotMatch": "Nota: Esta vista previa se reconstruye a partir de la configuración de conversación actual y es posible que no coincida exactamente con los parámetros de solicitud reales.", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/fa-IR/traceDialog.json b/src/renderer/src/i18n/fa-IR/traceDialog.json index 784705695..d97e644ee 100644 --- a/src/renderer/src/i18n/fa-IR/traceDialog.json +++ b/src/renderer/src/i18n/fa-IR/traceDialog.json @@ -13,5 +13,40 @@ "notImplementedDesc": "این تامین کننده در حال حاضر قابل پیش‌نمایش نیست.", "provider": "تامین‌کننده", "model": "مدل", - "title": "پیش‌نمایش پارامتر درخواست" + "title": "پیش‌نمایش پارامتر درخواست", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/fr-FR/traceDialog.json b/src/renderer/src/i18n/fr-FR/traceDialog.json index 4764a5d8a..8e44056e7 100644 --- a/src/renderer/src/i18n/fr-FR/traceDialog.json +++ b/src/renderer/src/i18n/fr-FR/traceDialog.json @@ -13,5 +13,40 @@ "notImplementedDesc": "Ce fournisseur n'est pas encore disponible en aperçu.", "provider": "Fournisseur", "model": "Modèle", - "title": "Aperçu des paramètres de requête" + "title": "Aperçu des paramètres de requête", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/he-IL/traceDialog.json b/src/renderer/src/i18n/he-IL/traceDialog.json index 703332a23..5c5829537 100644 --- a/src/renderer/src/i18n/he-IL/traceDialog.json +++ b/src/renderer/src/i18n/he-IL/traceDialog.json @@ -13,5 +13,40 @@ "errorDesc": "לא ניתן להביא תצוגה מקדימה של הבקשה, אנא נסה שוב", "notImplemented": "לא נתמך", "notImplementedDesc": "ספק זה טרם יישם תצוגה מקדימה של הבקשה", - "mayNotMatch": "הערה: תצוגה מקדימה זו משוחזרת מהגדרות השיחה הנוכחיות ועשויה שלא להתאים במדויק לפרמטרי הבקשה בפועל" + "mayNotMatch": "הערה: תצוגה מקדימה זו משוחזרת מהגדרות השיחה הנוכחיות ועשויה שלא להתאים במדויק לפרמטרי הבקשה בפועל", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/id-ID/traceDialog.json b/src/renderer/src/i18n/id-ID/traceDialog.json index a65b2b360..521d434a5 100644 --- a/src/renderer/src/i18n/id-ID/traceDialog.json +++ b/src/renderer/src/i18n/id-ID/traceDialog.json @@ -13,5 +13,40 @@ "errorDesc": "Tidak dapat memperoleh informasi pratinjau permintaan, silakan coba lagi", "notImplemented": "Belum didukung", "notImplementedDesc": "Pemasok ini belum tersedia untuk pratinjau", - "mayNotMatch": "Catatan: Pratinjau ini dibuat ulang berdasarkan pengaturan sesi saat ini dan mungkin tidak sama persis dengan parameter yang sebenarnya dikirim." + "mayNotMatch": "Catatan: Pratinjau ini dibuat ulang berdasarkan pengaturan sesi saat ini dan mungkin tidak sama persis dengan parameter yang sebenarnya dikirim.", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/it-IT/traceDialog.json b/src/renderer/src/i18n/it-IT/traceDialog.json index a929296d4..2daf3e320 100644 --- a/src/renderer/src/i18n/it-IT/traceDialog.json +++ b/src/renderer/src/i18n/it-IT/traceDialog.json @@ -13,5 +13,40 @@ "errorDesc": "Impossibile ottenere l'anteprima della richiesta. Riprova", "notImplemented": "Non supportato", "notImplementedDesc": "Questo provider non supporta ancora l'anteprima", - "mayNotMatch": "Nota: questa anteprima è ricostruita dalle impostazioni correnti della sessione e potrebbe non corrispondere esattamente ai parametri inviati" + "mayNotMatch": "Nota: questa anteprima è ricostruita dalle impostazioni correnti della sessione e potrebbe non corrispondere esattamente ai parametri inviati", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/ja-JP/traceDialog.json b/src/renderer/src/i18n/ja-JP/traceDialog.json index 6ba0e4fe1..bd1f1ece0 100644 --- a/src/renderer/src/i18n/ja-JP/traceDialog.json +++ b/src/renderer/src/i18n/ja-JP/traceDialog.json @@ -13,5 +13,40 @@ "notImplementedDesc": "このサプライヤーはまだプレビューできません", "provider": "サプライヤー", "model": "モデル", - "title": "リクエストパラメータプレビュー" + "title": "リクエストパラメータプレビュー", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/ko-KR/traceDialog.json b/src/renderer/src/i18n/ko-KR/traceDialog.json index edc2b9bf0..5881e5c45 100644 --- a/src/renderer/src/i18n/ko-KR/traceDialog.json +++ b/src/renderer/src/i18n/ko-KR/traceDialog.json @@ -13,5 +13,40 @@ "notImplementedDesc": "이 공급업체는 아직 미리 볼 수 없습니다.", "provider": "공급업체", "model": "모델", - "title": "요청 매개변수 미리보기" + "title": "요청 매개변수 미리보기", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/ms-MY/traceDialog.json b/src/renderer/src/i18n/ms-MY/traceDialog.json index 732626fbb..9ecf3b8a3 100644 --- a/src/renderer/src/i18n/ms-MY/traceDialog.json +++ b/src/renderer/src/i18n/ms-MY/traceDialog.json @@ -13,5 +13,40 @@ "errorDesc": "Tidak dapat mendapatkan maklumat pratonton permintaan, sila cuba lagi", "notImplemented": "Belum disokong lagi", "notImplementedDesc": "Pembekal ini belum tersedia untuk pratonton", - "mayNotMatch": "Nota: Pratonton ini dibina semula berdasarkan tetapan sesi semasa dan mungkin tidak sama persis dengan parameter yang sebenarnya dihantar." + "mayNotMatch": "Nota: Pratonton ini dibina semula berdasarkan tetapan sesi semasa dan mungkin tidak sama persis dengan parameter yang sebenarnya dihantar.", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/pl-PL/traceDialog.json b/src/renderer/src/i18n/pl-PL/traceDialog.json index e94c479b0..999ed85ac 100644 --- a/src/renderer/src/i18n/pl-PL/traceDialog.json +++ b/src/renderer/src/i18n/pl-PL/traceDialog.json @@ -13,5 +13,40 @@ "errorDesc": "Nie można pobrać podglądu żądania. Spróbuj ponownie", "notImplemented": "Nieobsługiwane", "notImplementedDesc": "Ten dostawca nie wdrożył jeszcze podglądu żądań", - "mayNotMatch": "Uwaga: ten podgląd jest rekonstruowany na podstawie bieżących ustawień konwersacji i może nie odpowiadać dokładnie rzeczywistym parametrom żądania" + "mayNotMatch": "Uwaga: ten podgląd jest rekonstruowany na podstawie bieżących ustawień konwersacji i może nie odpowiadać dokładnie rzeczywistym parametrom żądania", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/pt-BR/traceDialog.json b/src/renderer/src/i18n/pt-BR/traceDialog.json index 6e63e4e8c..d32106226 100644 --- a/src/renderer/src/i18n/pt-BR/traceDialog.json +++ b/src/renderer/src/i18n/pt-BR/traceDialog.json @@ -13,5 +13,40 @@ "notImplementedDesc": "Este fornecedor ainda não está disponível para visualização.", "provider": "Fornecedor", "model": "Modelo", - "title": "Prévia dos parâmetros da solicitação" + "title": "Prévia dos parâmetros da solicitação", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/ru-RU/traceDialog.json b/src/renderer/src/i18n/ru-RU/traceDialog.json index f51c4c130..fabf3baeb 100644 --- a/src/renderer/src/i18n/ru-RU/traceDialog.json +++ b/src/renderer/src/i18n/ru-RU/traceDialog.json @@ -13,5 +13,40 @@ "notImplementedDesc": "Этот поставщик пока недоступен для предварительного просмотра.", "provider": "Поставщик", "title": "Предварительный просмотр параметров запроса", - "model": "Модель" + "model": "Модель", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/tr-TR/traceDialog.json b/src/renderer/src/i18n/tr-TR/traceDialog.json index 707795a95..d86d7e137 100644 --- a/src/renderer/src/i18n/tr-TR/traceDialog.json +++ b/src/renderer/src/i18n/tr-TR/traceDialog.json @@ -13,5 +13,40 @@ "errorDesc": "İstek önizlemesi getirilemiyor, lütfen tekrar deneyin", "notImplemented": "Desteklenmiyor", "notImplementedDesc": "Bu sağlayıcı henüz istek önizlemesini uygulamadı", - "mayNotMatch": "Not: Bu önizleme mevcut görüşme ayarlarından yeniden oluşturulmuştur ve gerçek istek parametreleriyle tam olarak eşleşmeyebilir" + "mayNotMatch": "Not: Bu önizleme mevcut görüşme ayarlarından yeniden oluşturulmuştur ve gerçek istek parametreleriyle tam olarak eşleşmeyebilir", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/vi-VN/traceDialog.json b/src/renderer/src/i18n/vi-VN/traceDialog.json index fb81ddce5..550859861 100644 --- a/src/renderer/src/i18n/vi-VN/traceDialog.json +++ b/src/renderer/src/i18n/vi-VN/traceDialog.json @@ -13,5 +13,40 @@ "errorDesc": "Không thể tìm nạp bản xem trước yêu cầu, vui lòng thử lại", "notImplemented": "Không được hỗ trợ", "notImplementedDesc": "Nhà cung cấp này chưa triển khai bản xem trước yêu cầu", - "mayNotMatch": "Lưu ý: Bản xem trước này được xây dựng lại từ cài đặt cuộc trò chuyện hiện tại và có thể không khớp chính xác với các tham số yêu cầu thực tế" + "mayNotMatch": "Lưu ý: Bản xem trước này được xây dựng lại từ cài đặt cuộc trò chuyện hiện tại và có thể không khớp chính xác với các tham số yêu cầu thực tế", + "tabs": { + "request": "Request", + "view": "View", + "entries": "Entries", + "budget": "Budget" + }, + "empty": "No diagnostics", + "emptyDesc": "No request trace or view manifest is available for this message", + "requestUnavailable": "No request trace", + "requestUnavailableDesc": "This message has no persisted provider request trace", + "manifestUnavailable": "No view manifest", + "manifestUnavailableDesc": "This message has no persisted Tape view manifest", + "viewId": "View ID", + "policy": "Policy", + "policyVersion": "Policy version", + "taskType": "Task type", + "requestSeq": "Request #", + "latestEntryId": "Latest entry ID", + "promptHash": "Prompt hash", + "toolDefinitionsHash": "Tool definitions hash", + "manifestHash": "Manifest hash", + "includedEntries": "Included entries", + "excludedEntries": "Excluded entries", + "entryId": "Entry ID", + "messageId": "Message ID", + "orderSeq": "Order seq", + "role": "Role", + "source": "Source", + "reason": "Reason", + "contextLength": "Context length", + "requestedMaxTokens": "Requested max tokens", + "effectiveMaxTokens": "Effective max tokens", + "reserveTokens": "Reserve tokens", + "toolReserveTokens": "Tool reserve tokens", + "estimatedPromptTokens": "Estimated prompt tokens" } diff --git a/src/renderer/src/i18n/zh-CN/traceDialog.json b/src/renderer/src/i18n/zh-CN/traceDialog.json index b4ec2e0a7..2a3c43ed5 100644 --- a/src/renderer/src/i18n/zh-CN/traceDialog.json +++ b/src/renderer/src/i18n/zh-CN/traceDialog.json @@ -13,5 +13,40 @@ "errorDesc": "无法获取请求预览信息,请重试", "notImplemented": "暂不支持", "notImplementedDesc": "此供应商暂时还无法预览", - "mayNotMatch": "注意:此预览基于当前会话设置重建,可能与实际发送时的参数不完全一致" + "mayNotMatch": "注意:此预览基于当前会话设置重建,可能与实际发送时的参数不完全一致", + "tabs": { + "request": "请求", + "view": "视图", + "entries": "条目", + "budget": "预算" + }, + "empty": "暂无诊断数据", + "emptyDesc": "此消息暂无请求追踪或视图清单", + "requestUnavailable": "暂无请求追踪", + "requestUnavailableDesc": "此消息暂无持久化的供应商请求追踪", + "manifestUnavailable": "暂无视图清单", + "manifestUnavailableDesc": "此消息暂无持久化的 Tape 视图清单", + "viewId": "视图 ID", + "policy": "策略", + "policyVersion": "策略版本", + "taskType": "任务类型", + "requestSeq": "请求序号", + "latestEntryId": "最新条目 ID", + "promptHash": "Prompt Hash", + "toolDefinitionsHash": "工具定义 Hash", + "manifestHash": "清单 Hash", + "includedEntries": "包含条目", + "excludedEntries": "排除条目", + "entryId": "条目 ID", + "messageId": "消息 ID", + "orderSeq": "顺序号", + "role": "角色", + "source": "来源", + "reason": "原因", + "contextLength": "上下文长度", + "requestedMaxTokens": "请求 Max Tokens", + "effectiveMaxTokens": "生效 Max Tokens", + "reserveTokens": "预留 Tokens", + "toolReserveTokens": "工具预留 Tokens", + "estimatedPromptTokens": "估算 Prompt Tokens" } diff --git a/src/renderer/src/i18n/zh-HK/traceDialog.json b/src/renderer/src/i18n/zh-HK/traceDialog.json index 0952eee7c..4be9c57be 100644 --- a/src/renderer/src/i18n/zh-HK/traceDialog.json +++ b/src/renderer/src/i18n/zh-HK/traceDialog.json @@ -13,5 +13,40 @@ "notImplementedDesc": "此供應商暫時無法預覽", "provider": "供應商", "title": "請求參數預覽", - "model": "Model" + "model": "Model", + "tabs": { + "request": "请求", + "view": "视图", + "entries": "条目", + "budget": "预算" + }, + "empty": "暂无诊断数据", + "emptyDesc": "此消息暂无请求追踪或视图清单", + "requestUnavailable": "暂无请求追踪", + "requestUnavailableDesc": "此消息暂无持久化的供应商请求追踪", + "manifestUnavailable": "暂无视图清单", + "manifestUnavailableDesc": "此消息暂无持久化的 Tape 视图清单", + "viewId": "视图 ID", + "policy": "策略", + "policyVersion": "策略版本", + "taskType": "任务类型", + "requestSeq": "请求序号", + "latestEntryId": "最新条目 ID", + "promptHash": "Prompt Hash", + "toolDefinitionsHash": "工具定义 Hash", + "manifestHash": "清单 Hash", + "includedEntries": "包含条目", + "excludedEntries": "排除条目", + "entryId": "条目 ID", + "messageId": "消息 ID", + "orderSeq": "顺序号", + "role": "角色", + "source": "来源", + "reason": "原因", + "contextLength": "上下文长度", + "requestedMaxTokens": "请求 Max Tokens", + "effectiveMaxTokens": "生效 Max Tokens", + "reserveTokens": "预留 Tokens", + "toolReserveTokens": "工具预留 Tokens", + "estimatedPromptTokens": "估算 Prompt Tokens" } diff --git a/src/renderer/src/i18n/zh-TW/traceDialog.json b/src/renderer/src/i18n/zh-TW/traceDialog.json index 84460c6fb..e21efb193 100644 --- a/src/renderer/src/i18n/zh-TW/traceDialog.json +++ b/src/renderer/src/i18n/zh-TW/traceDialog.json @@ -13,5 +13,40 @@ "notImplementedDesc": "此供應商暫時無法預覽", "provider": "供應商", "title": "請求參數預覽", - "model": "Model" + "model": "Model", + "tabs": { + "request": "请求", + "view": "视图", + "entries": "条目", + "budget": "预算" + }, + "empty": "暂无诊断数据", + "emptyDesc": "此消息暂无请求追踪或视图清单", + "requestUnavailable": "暂无请求追踪", + "requestUnavailableDesc": "此消息暂无持久化的供应商请求追踪", + "manifestUnavailable": "暂无视图清单", + "manifestUnavailableDesc": "此消息暂无持久化的 Tape 视图清单", + "viewId": "视图 ID", + "policy": "策略", + "policyVersion": "策略版本", + "taskType": "任务类型", + "requestSeq": "请求序号", + "latestEntryId": "最新条目 ID", + "promptHash": "Prompt Hash", + "toolDefinitionsHash": "工具定义 Hash", + "manifestHash": "清单 Hash", + "includedEntries": "包含条目", + "excludedEntries": "排除条目", + "entryId": "条目 ID", + "messageId": "消息 ID", + "orderSeq": "顺序号", + "role": "角色", + "source": "来源", + "reason": "原因", + "contextLength": "上下文长度", + "requestedMaxTokens": "请求 Max Tokens", + "effectiveMaxTokens": "生效 Max Tokens", + "reserveTokens": "预留 Tokens", + "toolReserveTokens": "工具预留 Tokens", + "estimatedPromptTokens": "估算 Prompt Tokens" } diff --git a/src/shared/contracts/routes.ts b/src/shared/contracts/routes.ts index 6aabdaa60..656169039 100644 --- a/src/shared/contracts/routes.ts +++ b/src/shared/contracts/routes.ts @@ -293,6 +293,7 @@ import { sessionsDeactivateRoute, sessionsEditUserMessageRoute, sessionsEnsureAcpDraftRoute, + sessionsExportMessageTapeReplaySliceRoute, sessionsExportRoute, sessionsForkRoute, sessionsGetAcpSessionCommandsRoute, @@ -455,7 +456,7 @@ export * from './routes/upgrade.routes' export * from './routes/window.routes' export * from './routes/workspace.routes' -export const DEEPCHAT_ROUTE_CATALOG = { +export const DEEPCHAT_ROUTE_CATALOG: Record = { [acpTerminalInputRoute.name]: acpTerminalInputRoute, [acpTerminalKillRoute.name]: acpTerminalKillRoute, [shortcutRegisterRoute.name]: shortcutRegisterRoute, @@ -675,6 +676,7 @@ export const DEEPCHAT_ROUTE_CATALOG = { [sessionsSearchHistoryRoute.name]: sessionsSearchHistoryRoute, [sessionsGetSearchResultsRoute.name]: sessionsGetSearchResultsRoute, [sessionsListMessageTracesRoute.name]: sessionsListMessageTracesRoute, + [sessionsExportMessageTapeReplaySliceRoute.name]: sessionsExportMessageTapeReplaySliceRoute, [sessionsTranslateTextRoute.name]: sessionsTranslateTextRoute, [sessionsGetAgentsRoute.name]: sessionsGetAgentsRoute, [sessionsGetUsageDashboardRoute.name]: sessionsGetUsageDashboardRoute, @@ -825,7 +827,7 @@ export const DEEPCHAT_ROUTE_CATALOG = { [dialogErrorRoute.name]: dialogErrorRoute, [toolsListDefinitionsRoute.name]: toolsListDefinitionsRoute, [systemOpenSettingsRoute.name]: systemOpenSettingsRoute -} satisfies Record +} export type DeepchatRouteCatalog = typeof DEEPCHAT_ROUTE_CATALOG export type DeepchatRouteName = keyof DeepchatRouteCatalog diff --git a/src/shared/contracts/routes/sessions.routes.ts b/src/shared/contracts/routes/sessions.routes.ts index d124433d0..428c7d5d2 100644 --- a/src/shared/contracts/routes/sessions.routes.ts +++ b/src/shared/contracts/routes/sessions.routes.ts @@ -8,6 +8,7 @@ import type { SendMessageInput } from '@shared/types/agent-interface' import type { HistorySearchHit } from '@shared/types/presenters/agent-session.presenter' +import type { DeepChatTapeReplaySlice } from '@shared/types/tape-replay' import { SessionListItemSchema, SessionPageCursorSchema, @@ -23,10 +24,13 @@ import { SessionWithStateSchema, defineRouteContract } from '../common' +import type { RouteContract } from '../common' import { AcpConfigStateSchema, UsageDashboardDataSchema } from '../domainSchemas' const PendingSessionInputRecordSchema = z.custom() const MessageTraceRecordSchema = z.custom() +const DeepChatTapeViewManifestRecordSchema = z.record(z.unknown()) +const DeepChatTapeReplaySliceSchema = z.custom().nullable() const HistorySearchHitSchema = z.custom() const SearchResultSchema = z.custom() const AgentSchema = z.custom() @@ -321,13 +325,32 @@ export const sessionsGetSearchResultsRoute = defineRouteContract({ }) }) -export const sessionsListMessageTracesRoute = defineRouteContract({ - name: 'sessions.listMessageTraces', +export const sessionsListMessageTracesRoute: RouteContract<'sessions.listMessageTraces'> = + defineRouteContract({ + name: 'sessions.listMessageTraces', + input: z.object({ + messageId: EntityIdSchema + }), + output: z.object({ + traces: z.array(MessageTraceRecordSchema), + manifests: z.array(DeepChatTapeViewManifestRecordSchema) + }) + }) + +export const sessionsExportMessageTapeReplaySliceRoute = defineRouteContract({ + name: 'sessions.exportMessageTapeReplaySlice', input: z.object({ - messageId: EntityIdSchema + messageId: EntityIdSchema, + options: z + .object({ + requestSeq: z.number().int().positive().optional(), + includeTapePayloads: z.boolean().optional(), + includeTracePayload: z.boolean().optional() + }) + .optional() }), output: z.object({ - traces: z.array(MessageTraceRecordSchema) + slice: DeepChatTapeReplaySliceSchema }) }) diff --git a/src/shared/types/agent-interface.d.ts b/src/shared/types/agent-interface.d.ts index 146245cba..32584589f 100644 --- a/src/shared/types/agent-interface.d.ts +++ b/src/shared/types/agent-interface.d.ts @@ -3,6 +3,8 @@ import type { ImageGenerationOptions } from '../imageGenerationSettings' import type { VideoGenerationOptions } from '../videoGenerationSettings' import type { ToolCallImagePreview } from './core/mcp' import type { AgentPlanDisplayItem } from './agent-plan' +import type { DeepChatTapeViewManifestRecord } from './tape-view-manifest' +import type { DeepChatTapeReplayExportOptions, DeepChatTapeReplaySlice } from './tape-replay' /** * Agent Interface Protocol @@ -209,6 +211,19 @@ export interface IAgentImplementation { state?: Record ): Promise + /** List prompt view manifests associated with a message */ + listMessageViewManifests?( + sessionId: string, + messageId: string + ): Promise + + /** Export a deterministic tape replay slice for a message request */ + exportMessageTapeReplaySlice?( + sessionId: string, + messageId: string, + options?: DeepChatTapeReplayExportOptions + ): Promise + /** Record a completed child session as a merged tape fork */ mergeSubagentTape?( parentSessionId: string, diff --git a/src/shared/types/presenters/agent-session.presenter.d.ts b/src/shared/types/presenters/agent-session.presenter.d.ts index 7aa8abd03..c57dc0119 100644 --- a/src/shared/types/presenters/agent-session.presenter.d.ts +++ b/src/shared/types/presenters/agent-session.presenter.d.ts @@ -27,6 +27,8 @@ import type { AgentTapeAnchorResult, AgentTransferImpact } from '../agent-interface' +import type { DeepChatTapeViewManifestRecord } from '../tape-view-manifest' +import type { DeepChatTapeReplayExportOptions, DeepChatTapeReplaySlice } from '../tape-replay' import type { AcpConfigState } from './llmprovider.presenter' import type { SearchResult } from './thread.presenter' @@ -137,6 +139,11 @@ export interface IAgentSessionPresenter { getLegacyImportStatus(): Promise retryLegacyImport(): Promise listMessageTraces(messageId: string): Promise + listMessageViewManifests(messageId: string): Promise + exportMessageTapeReplaySlice( + messageId: string, + options?: DeepChatTapeReplayExportOptions + ): Promise getMessageTraceCount(messageId: string): Promise getMessageIds(sessionId: string): Promise getMessage(messageId: string): Promise diff --git a/src/shared/types/tape-replay.ts b/src/shared/types/tape-replay.ts new file mode 100644 index 000000000..5117d4acc --- /dev/null +++ b/src/shared/types/tape-replay.ts @@ -0,0 +1,63 @@ +import type { DeepChatTapeViewManifestRecord } from './tape-view-manifest' + +export interface DeepChatTapeReplayExportOptions { + requestSeq?: number + includeTapePayloads?: boolean + includeTracePayload?: boolean +} + +export interface DeepChatTapeReplayTraceSnapshot { + id: string + requestSeq: number + providerId: string + modelId: string + endpoint: string + headersHash: string + bodyHash: string + truncated: boolean + createdAt: number + headersJson?: string + bodyJson?: string +} + +export interface DeepChatTapeReplayEntrySnapshot { + entryId: number + kind: string + name: string | null + sourceType: string | null + sourceId: string | null + sourceSeq: number | null + provenanceKey: string | null + payloadHash: string + metaHash: string + createdAt: number + payload?: Record + meta?: Record +} + +export interface DeepChatTapeReplaySliceRefs { + manifestEntryId: number + includedEntryIds: number[] + excludedEntryIds: number[] + anchorEntryIds: number[] +} + +export interface DeepChatTapeReplaySliceHashes { + manifestHash: string + sliceHash: string +} + +export interface DeepChatTapeReplaySlice { + schemaVersion: 1 + sliceId: string + sessionId: string + messageId: string + requestSeq: number + mode: 'manifest_only' | 'trace_bound' + manifestRecord: DeepChatTapeViewManifestRecord + trace: DeepChatTapeReplayTraceSnapshot | null + entries: DeepChatTapeReplayEntrySnapshot[] + refs: DeepChatTapeReplaySliceRefs + hashes: DeepChatTapeReplaySliceHashes + createdAt: number +} diff --git a/src/shared/types/tape-view-manifest.ts b/src/shared/types/tape-view-manifest.ts new file mode 100644 index 000000000..35d4fdad4 --- /dev/null +++ b/src/shared/types/tape-view-manifest.ts @@ -0,0 +1,97 @@ +export type DeepChatTapeViewTaskType = 'chat' | 'resume' | 'tool_loop' + +export type DeepChatTapeViewPolicy = + | 'legacy_context_v1' + | 'legacy_context_shadow' + | 'resume_shadow' + | 'tool_loop_shadow' + | 'context_pressure_recovery_shadow' + +export type DeepChatTapeViewEntryRole = 'system' | 'user' | 'assistant' | 'tool' | null + +export type DeepChatTapeViewEntrySource = 'tape' | 'synthetic' + +export type DeepChatTapeViewEntryReason = + | 'system_prompt' + | 'selected_history' + | 'new_user_input' + | 'resume_target' + | 'tool_loop_message' + +export type DeepChatTapeViewExcludedReason = + | 'before_summary_cursor' + | 'compaction_indicator' + | 'pending_not_context_history' + | 'out_of_budget' + | 'empty_after_formatting' + | 'superseded' + | 'retracted' + +export interface DeepChatTapeViewEntryRef { + entryId: number | null + messageId: string | null + orderSeq: number | null + role: DeepChatTapeViewEntryRole + source: DeepChatTapeViewEntrySource + reason: DeepChatTapeViewEntryReason +} + +export interface DeepChatTapeViewExcludedRef { + entryId: number | null + messageId: string | null + orderSeq: number | null + reason: DeepChatTapeViewExcludedReason +} + +export interface DeepChatTapeViewTokenBudget { + contextLength: number + requestedMaxTokens: number + effectiveMaxTokens: number + reserveTokens: number + toolReserveTokens: number + estimatedPromptTokens: number +} + +export interface DeepChatTapeViewHashes { + promptHash: string + toolDefinitionsHash: string + manifestHash: string +} + +export interface DeepChatTapeViewMeta { + providerId: string + modelId: string + summaryCursorOrderSeq: number + supportsVision: boolean + supportsAudioInput: boolean + traceDebugEnabled: boolean +} + +export interface DeepChatTapeViewManifest { + schemaVersion: 1 + viewId: string + sessionId: string + messageId: string + requestSeq: number + taskType: DeepChatTapeViewTaskType + policy: DeepChatTapeViewPolicy + policyVersion: number | null + contextBuilderVersion: 'legacy-v1' + latestEntryId: number + anchorEntryIds: number[] + included: DeepChatTapeViewEntryRef[] + excluded: DeepChatTapeViewExcludedRef[] + tokenBudget: DeepChatTapeViewTokenBudget + hashes: DeepChatTapeViewHashes + meta: DeepChatTapeViewMeta + assembledAt: number +} + +export interface DeepChatTapeViewManifestRecord { + sessionId: string + messageId: string + requestSeq: number + entryId: number + createdAt: number + manifest: DeepChatTapeViewManifest +} diff --git a/test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts b/test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts index 22d428405..212895af2 100644 --- a/test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts +++ b/test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts @@ -1492,6 +1492,70 @@ describe('AgentRuntimePresenter', () => { ) }) + it('persists view manifests before each provider request with monotonic request sequences', async () => { + configPresenter.getSetting.mockImplementation((key: string) => + key === 'traceDebugEnabled' ? true : undefined + ) + + await agent.initSession('s1', { providerId: 'openai', modelId: 'gpt-4' }) + await agent.processMessage('s1', 'Hello') + + const callArgs = (processStream as ReturnType).mock.calls[0][0] + const providerCoreStream = llmProvider.getProviderInstance.mock.results[0].value.coreStream + const appendEventCallsBeforeProviderTurn = + sqlitePresenter.deepchatTapeEntriesTable.appendEvent.mock.calls.length + + for await (const _event of callArgs.coreStream( + callArgs.messages, + callArgs.modelId, + callArgs.modelConfig, + callArgs.temperature, + callArgs.maxTokens, + callArgs.tools + )) { + } + for await (const _event of callArgs.coreStream( + callArgs.messages, + callArgs.modelId, + callArgs.modelConfig, + callArgs.temperature, + callArgs.maxTokens, + callArgs.tools + )) { + } + + const firstManifestAppendOrder = + sqlitePresenter.deepchatTapeEntriesTable.appendEvent.mock.invocationCallOrder[ + appendEventCallsBeforeProviderTurn + ] + const firstProviderCallOrder = providerCoreStream.mock.invocationCallOrder[0] + expect(firstManifestAppendOrder).toBeLessThan(firstProviderCallOrder) + + const manifestRows = sqlitePresenter.deepchatTapeEntriesTable + .getBySession('s1') + .filter((row: any) => row.kind === 'event' && row.name === 'view/assembled') + const manifests = manifestRows.map((row: any) => JSON.parse(row.payload_json).data.manifest) + + expect(manifestRows).toHaveLength(2) + expect(manifestRows.map((row: any) => row.source_seq)).toEqual([1, 2]) + expect(manifests.map((manifest: any) => manifest.requestSeq)).toEqual([1, 2]) + expect(manifests[0]).toMatchObject({ + taskType: 'chat', + policy: 'legacy_context_v1', + policyVersion: 1, + meta: { + traceDebugEnabled: true + } + }) + expect(manifests[1]).toMatchObject({ + taskType: 'tool_loop', + policy: 'tool_loop_shadow', + policyVersion: null + }) + expect(manifests[0].hashes.promptHash).toHaveLength(64) + expect(manifests[1].hashes.toolDefinitionsHash).toHaveLength(64) + }) + it('emits and clears an ephemeral rate-limit message while waiting for the provider gate', async () => { llmProvider.executeWithRateLimit.mockImplementation( async (_providerId: string, options?: { onQueued?: (snapshot: any) => void }) => { @@ -3808,8 +3872,18 @@ describe('AgentRuntimePresenter', () => { estimateMessagesTokens(providerMessages) + estimateToolReserveTokens(providerTools) + providerMaxTokens + const manifestRows = sqlitePresenter.deepchatTapeEntriesTable + .getBySession('s1') + .filter((row: any) => row.kind === 'event' && row.name === 'view/assembled') + const pressureManifest = JSON.parse(manifestRows[0].payload_json).data.manifest expect(llmProvider.generateText).toHaveBeenCalled() + expect(pressureManifest).toMatchObject({ + taskType: 'chat', + requestSeq: 1, + policy: 'context_pressure_recovery_shadow', + policyVersion: null + }) expect(providerMessages[0].content).toContain('## Conversation Summary') expect(providerMaxTokens).toBeLessThan(4096) expect(totalRequestTokens).toBeLessThanOrEqual(getUsableContextLength(8192)) diff --git a/test/main/presenter/agentRuntimePresenter/tapeService.test.ts b/test/main/presenter/agentRuntimePresenter/tapeService.test.ts index 58ae4fbb1..d66c114e4 100644 --- a/test/main/presenter/agentRuntimePresenter/tapeService.test.ts +++ b/test/main/presenter/agentRuntimePresenter/tapeService.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { buildContext } from '@/presenter/agentRuntimePresenter/contextBuilder' import { DeepChatTapeService } from '@/presenter/agentRuntimePresenter/tapeService' +import { createTapeViewManifest } from '@/presenter/agentRuntimePresenter/tapeViewManifest' import { appendMessageReplacementToTape, appendMessageRetractionToTape @@ -181,6 +182,35 @@ function createRecord(overrides: Partial): ChatMessageRecord } } +function createTraceRow(overrides: Record = {}) { + return { + id: 'trace-1', + message_id: 'a1', + session_id: 's1', + provider_id: 'openai', + model_id: 'gpt-4o', + request_seq: 1, + endpoint: 'https://api.openai.test/v1/chat/completions', + headers_json: '{"authorization":"[redacted]"}', + body_json: '{"messages":[{"role":"user","content":"hello"}]}', + truncated: 0, + created_at: 300, + ...overrides + } +} + +function createTapeService(table: unknown, traceRows: Array> = []) { + return new DeepChatTapeService({ + deepchatTapeEntriesTable: table, + deepchatMessageTracesTable: { + listByMessageId: vi.fn((messageId: string) => + traceRows.filter((row) => row.message_id === messageId) + ) + }, + deepchatSessionsTable: { getSummaryState: vi.fn().mockReturnValue(null) } + } as any) +} + describe('DeepChatTapeService', () => { it('backfills message and tool facts idempotently before returning tape records', () => { const { table, entries } = createTapeTableMock() @@ -432,6 +462,248 @@ describe('DeepChatTapeService', () => { }) }) + it('stores and lists view manifests as idempotent tape events', () => { + const { table, entries } = createTapeTableMock() + const service = new DeepChatTapeService({ + deepchatTapeEntriesTable: table, + deepchatSessionsTable: { getSummaryState: vi.fn().mockReturnValue(null) } + } as any) + const messageStore = { + getMessages: vi.fn().mockReturnValue([createRecord({ id: 'u1', orderSeq: 1 })]) + } + + service.ensureSessionTapeReady('s1', messageStore as any) + const sourceMaps = service.getViewManifestSourceMaps('s1') + const manifest = createTapeViewManifest({ + sessionId: 's1', + messageId: 'a1', + requestSeq: 1, + taskType: 'chat', + policy: 'legacy_context_v1', + policyVersion: 1, + messages: [{ role: 'user' as const, content: 'hello' }], + tools: [], + latestEntryId: sourceMaps.latestEntryId, + anchorEntryIds: sourceMaps.anchorEntryIds, + included: [ + { + entryId: sourceMaps.entryIdByMessageId.get('u1') ?? null, + messageId: 'u1', + orderSeq: 1, + role: 'user', + source: 'tape', + reason: 'selected_history' + } + ], + excluded: [], + tokenBudget: { + contextLength: 1000, + requestedMaxTokens: 100, + effectiveMaxTokens: 100, + reserveTokens: 100, + toolReserveTokens: 0 + }, + providerId: 'openai', + modelId: 'gpt-4o', + summaryCursorOrderSeq: 1, + supportsVision: true, + supportsAudioInput: false, + traceDebugEnabled: false, + assembledAt: 200 + }) + + const first = service.appendViewManifest(manifest) + const second = service.appendViewManifest(manifest) + + expect(second.entry_id).toBe(first.entry_id) + expect(entries.filter((entry) => entry.name === 'view/assembled')).toHaveLength(1) + expect(JSON.parse(first.meta_json)).toMatchObject({ + policy: 'legacy_context_v1', + policyVersion: 1 + }) + expect(service.listViewManifestsByMessage('s1', 'a1')).toMatchObject([ + { + sessionId: 's1', + messageId: 'a1', + requestSeq: 1, + entryId: first.entry_id, + manifest: { + hashes: { + manifestHash: manifest.hashes.manifestHash + }, + policy: 'legacy_context_v1', + policyVersion: 1, + included: [ + { + messageId: 'u1', + entryId: sourceMaps.entryIdByMessageId.get('u1') + } + ] + } + } + ]) + }) + + it('exports replay slices with metadata-only payloads by default', () => { + const { table } = createTapeTableMock() + const service = createTapeService(table, [createTraceRow()]) + const messageStore = { + getMessages: vi.fn().mockReturnValue([createRecord({ id: 'u1', orderSeq: 1 })]) + } + + service.ensureSessionTapeReady('s1', messageStore as any) + const sourceMaps = service.getViewManifestSourceMaps('s1') + const manifest = createTapeViewManifest({ + sessionId: 's1', + messageId: 'a1', + requestSeq: 1, + taskType: 'chat', + policy: 'legacy_context_v1', + policyVersion: 1, + messages: [{ role: 'user' as const, content: 'hello' }], + tools: [], + latestEntryId: sourceMaps.latestEntryId, + anchorEntryIds: sourceMaps.anchorEntryIds, + included: [ + { + entryId: sourceMaps.entryIdByMessageId.get('u1') ?? null, + messageId: 'u1', + orderSeq: 1, + role: 'user', + source: 'tape', + reason: 'selected_history' + } + ], + excluded: [], + tokenBudget: { + contextLength: 1000, + requestedMaxTokens: 100, + effectiveMaxTokens: 100, + reserveTokens: 100, + toolReserveTokens: 0 + }, + providerId: 'openai', + modelId: 'gpt-4o', + summaryCursorOrderSeq: 1, + supportsVision: true, + supportsAudioInput: false, + traceDebugEnabled: true, + assembledAt: 200 + }) + const manifestEntry = service.appendViewManifest(manifest) + + const slice = service.exportReplaySlice('s1', 'a1') + + expect(slice).toMatchObject({ + schemaVersion: 1, + sessionId: 's1', + messageId: 'a1', + requestSeq: 1, + mode: 'trace_bound', + refs: { + manifestEntryId: manifestEntry.entry_id, + includedEntryIds: [sourceMaps.entryIdByMessageId.get('u1')], + anchorEntryIds: sourceMaps.anchorEntryIds + }, + hashes: { + manifestHash: manifest.hashes.manifestHash + } + }) + expect(slice?.hashes.sliceHash).toHaveLength(64) + expect(slice?.trace?.bodyHash).toHaveLength(64) + expect(slice?.trace?.bodyJson).toBeUndefined() + expect(slice?.entries.some((entry) => entry.entryId === manifestEntry.entry_id)).toBe(true) + expect( + slice?.entries.every((entry) => entry.payload === undefined && entry.meta === undefined) + ).toBe(true) + }) + + it('exports explicit replay request sequences with opt-in payloads', () => { + const { table } = createTapeTableMock() + const service = createTapeService(table, [ + createTraceRow({ id: 'trace-1', request_seq: 1 }), + createTraceRow({ + id: 'trace-2', + request_seq: 2, + body_json: '{"messages":[{"role":"tool","content":"done"}]}' + }) + ]) + const messageStore = { + getMessages: vi.fn().mockReturnValue([createRecord({ id: 'u1', orderSeq: 1 })]) + } + + service.ensureSessionTapeReady('s1', messageStore as any) + const sourceMaps = service.getViewManifestSourceMaps('s1') + const baseManifestInput = { + sessionId: 's1', + messageId: 'a1', + requestSeq: 1, + taskType: 'chat' as const, + policy: 'legacy_context_v1' as const, + policyVersion: 1, + messages: [{ role: 'user' as const, content: 'hello' }], + tools: [], + latestEntryId: sourceMaps.latestEntryId, + anchorEntryIds: sourceMaps.anchorEntryIds, + included: [ + { + entryId: sourceMaps.entryIdByMessageId.get('u1') ?? null, + messageId: 'u1', + orderSeq: 1, + role: 'user', + source: 'tape', + reason: 'selected_history' + } + ], + excluded: [], + tokenBudget: { + contextLength: 1000, + requestedMaxTokens: 100, + effectiveMaxTokens: 100, + reserveTokens: 100, + toolReserveTokens: 0 + }, + providerId: 'openai', + modelId: 'gpt-4o', + summaryCursorOrderSeq: 1, + supportsVision: true, + supportsAudioInput: false, + traceDebugEnabled: true, + assembledAt: 200 + } + const firstManifest = createTapeViewManifest(baseManifestInput) + const secondManifest = createTapeViewManifest({ + ...baseManifestInput, + requestSeq: 2, + taskType: 'tool_loop', + policy: 'tool_loop_shadow', + policyVersion: null, + assembledAt: 250 + }) + service.appendViewManifest(firstManifest) + service.appendViewManifest(secondManifest) + + const latest = service.exportReplaySlice('s1', 'a1') + const first = service.exportReplaySlice('s1', 'a1', { + requestSeq: 1, + includeTapePayloads: true, + includeTracePayload: true + }) + + expect(latest?.requestSeq).toBe(2) + expect(first?.requestSeq).toBe(1) + expect(first?.trace?.bodyJson).toContain('"hello"') + expect(first?.entries.some((entry) => entry.payload?.record)).toBe(true) + expect(first?.entries.some((entry) => entry.meta?.source === 'backfill')).toBe(true) + }) + + it('returns null when exporting a replay slice without a manifest', () => { + const { table } = createTapeTableMock() + const service = createTapeService(table, [createTraceRow()]) + + expect(service.exportReplaySlice('s1', 'a1')).toBeNull() + }) + it('keeps pending message records for resume but hides pending tool facts from search', () => { const { table } = createTapeTableMock() const pendingBlocks = [ diff --git a/test/main/presenter/agentRuntimePresenter/tapeViewAssembler.test.ts b/test/main/presenter/agentRuntimePresenter/tapeViewAssembler.test.ts new file mode 100644 index 000000000..a13385a9c --- /dev/null +++ b/test/main/presenter/agentRuntimePresenter/tapeViewAssembler.test.ts @@ -0,0 +1,226 @@ +import { describe, expect, it, vi } from 'vitest' +import type { ChatMessageRecord } from '@shared/types/agent-interface' +import { + buildContextWithMetadata, + buildResumeContextWithMetadata +} from '@/presenter/agentRuntimePresenter/contextBuilder' +import { + buildTapeChatView, + buildTapeResumeView, + getTapeContextHistoryRecords, + TAPE_VIEW_ASSEMBLER_VERSION, + TAPE_VIEW_HISTORY_SOURCE +} from '@/presenter/agentRuntimePresenter/tapeViewAssembler' +import { + LEGACY_TAPE_VIEW_POLICY_ID, + LEGACY_TAPE_VIEW_POLICY_VERSION, + type TapeViewPolicy +} from '@/presenter/agentRuntimePresenter/tapeViewPolicy' + +vi.mock('tokenx', () => ({ + approximateTokenSize: vi.fn((text: string) => Math.ceil(text.length / 4)) +})) + +function createMockMessageStore(messages: ChatMessageRecord[] = []) { + return { + getMessages: vi.fn().mockReturnValue(messages) + } as any +} + +function makeUserRecord(orderSeq: number, text: string): ChatMessageRecord { + return { + id: `user-${orderSeq}`, + sessionId: 's1', + orderSeq, + role: 'user', + content: JSON.stringify({ text, files: [], links: [], search: false, think: false }), + status: 'sent', + isContextEdge: 0, + metadata: '{}', + traceCount: 0, + createdAt: orderSeq * 100, + updatedAt: orderSeq * 100 + } +} + +function makeAssistantRecord( + orderSeq: number, + text: string, + status: ChatMessageRecord['status'] = 'sent' +): ChatMessageRecord { + return { + id: orderSeq === 4 ? 'resume-target' : `asst-${orderSeq}`, + sessionId: 's1', + orderSeq, + role: 'assistant', + content: JSON.stringify([ + { type: 'content', content: text, status: 'success', timestamp: orderSeq * 100 } + ]), + status, + isContextEdge: 0, + metadata: '{}', + traceCount: 0, + createdAt: orderSeq * 100, + updatedAt: orderSeq * 100 + } +} + +describe('TapeViewAssembler', () => { + it('matches legacy chat context assembly while recording tape provenance', () => { + const records = [ + makeUserRecord(1, 'old user'), + makeAssistantRecord(2, 'old assistant'), + makeUserRecord(3, 'recent user') + ] + const store = createMockMessageStore(records) + const historyRecords = getTapeContextHistoryRecords(records) + const options = { + summaryCursorOrderSeq: 2, + extraReserveTokens: 16, + supportsAudioInput: false + } + + const legacy = buildContextWithMetadata('s1', 'next user', 'System', 1000, 100, store, false, { + ...options, + historyRecords + }) + const assembled = buildTapeChatView({ + sessionId: 's1', + newUserContent: 'next user', + systemPrompt: 'System', + contextLength: 1000, + reserveTokens: 100, + messageStore: store, + supportsVision: false, + historyRecords, + options + }) + + expect(assembled.messages).toEqual(legacy.messages) + expect(assembled.metadata).toEqual(legacy.metadata) + expect(assembled.historyRecords).toEqual(historyRecords) + expect(assembled.assemblerVersion).toBe(TAPE_VIEW_ASSEMBLER_VERSION) + expect(assembled.historySource).toBe(TAPE_VIEW_HISTORY_SOURCE) + expect(assembled.policyId).toBe(LEGACY_TAPE_VIEW_POLICY_ID) + expect(assembled.policyVersion).toBe(LEGACY_TAPE_VIEW_POLICY_VERSION) + expect(assembled.policySelectionReason).toBe('default') + }) + + it('matches legacy resume context assembly while recording tape provenance', () => { + const records = [ + makeUserRecord(1, 'old user'), + makeAssistantRecord(2, 'old assistant'), + makeUserRecord(3, 'recent user'), + makeAssistantRecord(4, 'partial answer', 'pending') + ] + const store = createMockMessageStore(records) + const options = { + summaryCursorOrderSeq: 1, + fallbackProtectedTurnCount: 1, + extraReserveTokens: 12, + supportsAudioInput: false + } + + const legacy = buildResumeContextWithMetadata( + 's1', + 'resume-target', + 'System', + 260, + 100, + store, + false, + { + ...options, + historyRecords: records + } + ) + const assembled = buildTapeResumeView({ + sessionId: 's1', + assistantMessageId: 'resume-target', + systemPrompt: 'System', + contextLength: 260, + reserveTokens: 100, + messageStore: store, + supportsVision: false, + historyRecords: records, + options + }) + + expect(assembled.messages).toEqual(legacy.messages) + expect(assembled.metadata).toEqual(legacy.metadata) + expect(assembled.historyRecords).toEqual(records) + expect(assembled.assemblerVersion).toBe(TAPE_VIEW_ASSEMBLER_VERSION) + expect(assembled.historySource).toBe(TAPE_VIEW_HISTORY_SOURCE) + expect(assembled.policyId).toBe(LEGACY_TAPE_VIEW_POLICY_ID) + expect(assembled.policyVersion).toBe(LEGACY_TAPE_VIEW_POLICY_VERSION) + expect(assembled.policySelectionReason).toBe('default') + }) + + it('records requested and fallback policy selection reasons', () => { + const records = [makeUserRecord(1, 'old user')] + const store = createMockMessageStore(records) + + const requested = buildTapeChatView({ + sessionId: 's1', + newUserContent: 'next user', + systemPrompt: '', + contextLength: 1000, + reserveTokens: 100, + messageStore: store, + supportsVision: false, + historyRecords: records, + requestedPolicyId: LEGACY_TAPE_VIEW_POLICY_ID + }) + + const fallback = buildTapeChatView({ + sessionId: 's1', + newUserContent: 'next user', + systemPrompt: '', + contextLength: 1000, + reserveTokens: 100, + messageStore: store, + supportsVision: false, + historyRecords: records, + requestedPolicyId: 'missing-policy' + }) + + expect(requested.policySelectionReason).toBe('requested') + expect(fallback.policySelectionReason).toBe('fallback_default') + expect(fallback.policyId).toBe(LEGACY_TAPE_VIEW_POLICY_ID) + }) + + it('delegates assembly to an injected policy', () => { + const records = [makeUserRecord(1, 'old user')] + const store = createMockMessageStore(records) + const customPolicy = { + id: LEGACY_TAPE_VIEW_POLICY_ID, + version: LEGACY_TAPE_VIEW_POLICY_VERSION, + buildChat: vi.fn().mockReturnValue({ + messages: [{ role: 'user', content: 'from policy' }], + metadata: { + includedRecords: [], + excludedRecords: [], + includesSystemPrompt: false + } + }), + buildResume: vi.fn() + } satisfies TapeViewPolicy + + const assembled = buildTapeChatView({ + sessionId: 's1', + newUserContent: 'next user', + systemPrompt: '', + contextLength: 1000, + reserveTokens: 100, + messageStore: store, + supportsVision: false, + historyRecords: records, + policy: customPolicy + }) + + expect(customPolicy.buildChat).toHaveBeenCalledOnce() + expect(assembled.messages).toEqual([{ role: 'user', content: 'from policy' }]) + expect(assembled.policyId).toBe(LEGACY_TAPE_VIEW_POLICY_ID) + expect(assembled.policySelectionReason).toBe('injected') + }) +}) diff --git a/test/main/presenter/agentRuntimePresenter/tapeViewManifest.test.ts b/test/main/presenter/agentRuntimePresenter/tapeViewManifest.test.ts new file mode 100644 index 000000000..c972a5913 --- /dev/null +++ b/test/main/presenter/agentRuntimePresenter/tapeViewManifest.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, it } from 'vitest' +import type { ChatMessageRecord } from '@shared/types/agent-interface' +import { + buildIncludedRefs, + createTapeViewManifest, + hashJson, + resolveTapeViewManifestPolicy +} from '@/presenter/agentRuntimePresenter/tapeViewManifest' + +function createRecord(overrides: Partial): ChatMessageRecord { + return { + id: 'm1', + sessionId: 's1', + orderSeq: 1, + role: 'user', + content: 'secret prompt content', + status: 'sent', + isContextEdge: 0, + metadata: '{}', + traceCount: 0, + createdAt: 100, + updatedAt: 100, + ...overrides + } +} + +describe('tapeViewManifest', () => { + it('hashes JSON with stable object key ordering', () => { + expect(hashJson({ b: 1, a: { d: 4, c: 3 } })).toBe(hashJson({ a: { c: 3, d: 4 }, b: 1 })) + }) + + it('builds refs from context metadata without copying raw message content', () => { + const refs = buildIncludedRefs( + { + includesSystemPrompt: true, + includedRecords: [ + { + record: createRecord({ id: 'u1', orderSeq: 3, content: 'do not persist this text' }), + reason: 'selected_history' + } + ], + excludedRecords: [], + newUserMessageId: 'u2' + }, + { + entryIdByMessageId: new Map([ + ['u1', 11], + ['u2', 12] + ]) + } + ) + + expect(refs).toMatchObject([ + { entryId: null, role: 'system', reason: 'system_prompt', source: 'synthetic' }, + { entryId: 11, messageId: 'u1', orderSeq: 3, reason: 'selected_history', source: 'tape' }, + { entryId: 12, messageId: 'u2', reason: 'new_user_input', source: 'tape' } + ]) + expect(JSON.stringify(refs)).not.toContain('do not persist this text') + }) + + it('creates deterministic prompt and manifest hashes without storing prompt bodies', () => { + const input = { + sessionId: 's1', + messageId: 'a1', + requestSeq: 1, + taskType: 'chat' as const, + policy: 'legacy_context_v1' as const, + policyVersion: 1, + messages: [{ role: 'user' as const, content: 'secret prompt content' }], + tools: [], + latestEntryId: 7, + anchorEntryIds: [1], + included: [ + { + entryId: 2, + messageId: 'u1', + orderSeq: 1, + role: 'user' as const, + source: 'tape' as const, + reason: 'selected_history' as const + } + ], + excluded: [], + tokenBudget: { + contextLength: 1000, + requestedMaxTokens: 100, + effectiveMaxTokens: 100, + reserveTokens: 100, + toolReserveTokens: 0 + }, + providerId: 'openai', + modelId: 'gpt-4o', + summaryCursorOrderSeq: 1, + supportsVision: true, + supportsAudioInput: false, + traceDebugEnabled: false, + assembledAt: 123 + } + + const first = createTapeViewManifest(input) + const second = createTapeViewManifest(input) + + expect(first.hashes).toEqual(second.hashes) + expect(first.policy).toBe('legacy_context_v1') + expect(first.policyVersion).toBe(1) + expect(first.hashes.manifestHash).toHaveLength(64) + expect(first.tokenBudget.estimatedPromptTokens).toBeGreaterThan(0) + expect(JSON.stringify(first)).not.toContain('secret prompt content') + }) + + it('resolves initial Tape policy provenance and request-level shadow policies', () => { + expect( + resolveTapeViewManifestPolicy({ + recoveredFromContextPressure: false, + isInitialViewRequest: true, + viewPolicy: 'legacy_context_v1', + viewPolicyVersion: 1 + }) + ).toEqual({ + policy: 'legacy_context_v1', + policyVersion: 1 + }) + + expect( + resolveTapeViewManifestPolicy({ + recoveredFromContextPressure: false, + isInitialViewRequest: true, + viewPolicy: 'legacy_context_v1' + }) + ).toEqual({ + policy: 'legacy_context_v1', + policyVersion: null + }) + + expect( + resolveTapeViewManifestPolicy({ + recoveredFromContextPressure: false, + isInitialViewRequest: false, + viewPolicy: 'legacy_context_v1', + viewPolicyVersion: 1 + }) + ).toEqual({ + policy: 'tool_loop_shadow', + policyVersion: null + }) + + expect( + resolveTapeViewManifestPolicy({ + recoveredFromContextPressure: true, + isInitialViewRequest: true, + viewPolicy: 'legacy_context_v1', + viewPolicyVersion: 1 + }) + ).toEqual({ + policy: 'context_pressure_recovery_shadow', + policyVersion: null + }) + }) +}) diff --git a/test/main/presenter/agentRuntimePresenter/tapeViewPolicy.test.ts b/test/main/presenter/agentRuntimePresenter/tapeViewPolicy.test.ts new file mode 100644 index 000000000..fc5045d0d --- /dev/null +++ b/test/main/presenter/agentRuntimePresenter/tapeViewPolicy.test.ts @@ -0,0 +1,180 @@ +import { describe, expect, it, vi } from 'vitest' +import type { ChatMessageRecord } from '@shared/types/agent-interface' +import { + buildContextWithMetadata, + buildResumeContextWithMetadata +} from '@/presenter/agentRuntimePresenter/contextBuilder' +import { + LEGACY_TAPE_VIEW_POLICY_ID, + LEGACY_TAPE_VIEW_POLICY_VERSION, + getTapeViewPolicy, + legacyTapeViewPolicy, + listTapeViewPolicies, + resolveTapeViewPolicy +} from '@/presenter/agentRuntimePresenter/tapeViewPolicy' + +vi.mock('tokenx', () => ({ + approximateTokenSize: vi.fn((text: string) => Math.ceil(text.length / 4)) +})) + +function createMockMessageStore(messages: ChatMessageRecord[] = []) { + return { + getMessages: vi.fn().mockReturnValue(messages) + } as any +} + +function makeUserRecord(orderSeq: number, text: string): ChatMessageRecord { + return { + id: `user-${orderSeq}`, + sessionId: 's1', + orderSeq, + role: 'user', + content: JSON.stringify({ text, files: [], links: [], search: false, think: false }), + status: 'sent', + isContextEdge: 0, + metadata: '{}', + traceCount: 0, + createdAt: orderSeq * 100, + updatedAt: orderSeq * 100 + } +} + +function makeAssistantRecord( + orderSeq: number, + text: string, + status: ChatMessageRecord['status'] = 'sent' +): ChatMessageRecord { + return { + id: orderSeq === 4 ? 'resume-target' : `asst-${orderSeq}`, + sessionId: 's1', + orderSeq, + role: 'assistant', + content: JSON.stringify([ + { type: 'content', content: text, status: 'success', timestamp: orderSeq * 100 } + ]), + status, + isContextEdge: 0, + metadata: '{}', + traceCount: 0, + createdAt: orderSeq * 100, + updatedAt: orderSeq * 100 + } +} + +describe('legacyTapeViewPolicy', () => { + it('matches legacy chat context builder output', () => { + const records = [ + makeUserRecord(1, 'old user'), + makeAssistantRecord(2, 'old assistant'), + makeUserRecord(3, 'recent user') + ] + const store = createMockMessageStore(records) + const input = { + sessionId: 's1', + newUserContent: 'next user', + systemPrompt: 'System', + contextLength: 1000, + reserveTokens: 100, + messageStore: store, + supportsVision: false, + historyRecords: records, + options: { + summaryCursorOrderSeq: 2, + extraReserveTokens: 16, + supportsAudioInput: false + } + } + + const legacy = buildContextWithMetadata( + input.sessionId, + input.newUserContent, + input.systemPrompt, + input.contextLength, + input.reserveTokens, + input.messageStore, + input.supportsVision, + { + ...input.options, + historyRecords: input.historyRecords + } + ) + const policyResult = legacyTapeViewPolicy.buildChat(input) + + expect(legacyTapeViewPolicy.id).toBe(LEGACY_TAPE_VIEW_POLICY_ID) + expect(legacyTapeViewPolicy.version).toBe(LEGACY_TAPE_VIEW_POLICY_VERSION) + expect(policyResult).toEqual(legacy) + }) + + it('matches legacy resume context builder output', () => { + const records = [ + makeUserRecord(1, 'old user'), + makeAssistantRecord(2, 'old assistant'), + makeUserRecord(3, 'recent user'), + makeAssistantRecord(4, 'partial answer', 'pending') + ] + const store = createMockMessageStore(records) + const input = { + sessionId: 's1', + assistantMessageId: 'resume-target', + systemPrompt: 'System', + contextLength: 260, + reserveTokens: 100, + messageStore: store, + supportsVision: false, + historyRecords: records, + options: { + summaryCursorOrderSeq: 1, + fallbackProtectedTurnCount: 1, + extraReserveTokens: 12, + supportsAudioInput: false + } + } + + const legacy = buildResumeContextWithMetadata( + input.sessionId, + input.assistantMessageId, + input.systemPrompt, + input.contextLength, + input.reserveTokens, + input.messageStore, + input.supportsVision, + { + ...input.options, + historyRecords: input.historyRecords + } + ) + const policyResult = legacyTapeViewPolicy.buildResume(input) + + expect(policyResult).toEqual(legacy) + }) +}) + +describe('TapeViewPolicy registry', () => { + it('lists and resolves the built-in legacy policy', () => { + expect(listTapeViewPolicies()).toEqual([legacyTapeViewPolicy]) + expect(getTapeViewPolicy(LEGACY_TAPE_VIEW_POLICY_ID)).toBe(legacyTapeViewPolicy) + expect(getTapeViewPolicy(` ${LEGACY_TAPE_VIEW_POLICY_ID} `)).toBe(legacyTapeViewPolicy) + expect(getTapeViewPolicy('missing-policy')).toBeNull() + expect(getTapeViewPolicy('')).toBeNull() + }) + + it('resolves default, requested, and fallback selections', () => { + expect(resolveTapeViewPolicy()).toEqual({ + policy: legacyTapeViewPolicy, + requestedPolicyId: null, + reason: 'default' + }) + + expect(resolveTapeViewPolicy({ requestedPolicyId: LEGACY_TAPE_VIEW_POLICY_ID })).toEqual({ + policy: legacyTapeViewPolicy, + requestedPolicyId: LEGACY_TAPE_VIEW_POLICY_ID, + reason: 'requested' + }) + + expect(resolveTapeViewPolicy({ requestedPolicyId: 'missing-policy' })).toEqual({ + policy: legacyTapeViewPolicy, + requestedPolicyId: 'missing-policy', + reason: 'fallback_default' + }) + }) +}) diff --git a/test/main/presenter/presenterCallErrorHandler.test.ts b/test/main/presenter/presenterCallErrorHandler.test.ts index b93eaa3a2..43f6ea757 100644 --- a/test/main/presenter/presenterCallErrorHandler.test.ts +++ b/test/main/presenter/presenterCallErrorHandler.test.ts @@ -5,18 +5,19 @@ const mocks = vi.hoisted(() => ({ sendToWebContents: vi.fn() })) -vi.mock('@/eventbus', () => ({ - eventBus: { - sendToWebContents: mocks.sendToWebContents - } -})) - const loadModule = async () => await import('../../../src/main/presenter/presenterCallErrorHandler') +const loadEventPublisher = async () => await import('../../../src/main/routes/publishDeepchatEvent') describe('presenterCallErrorHandler', () => { beforeEach(async () => { vi.resetModules() mocks.sendToWebContents.mockReset() + mocks.sendToWebContents.mockResolvedValue(true) + const { setDeepchatEventWindowPresenter } = await loadEventPublisher() + setDeepchatEventWindowPresenter({ + sendToAllWindows: vi.fn(), + sendToWebContents: mocks.sendToWebContents + }) const { resetPresenterCallErrorStateForTests } = await loadModule() resetPresenterCallErrorStateForTests() }) diff --git a/test/renderer/components/trace/TraceDialog.test.ts b/test/renderer/components/trace/TraceDialog.test.ts index e865ada59..a4d04d982 100644 --- a/test/renderer/components/trace/TraceDialog.test.ts +++ b/test/renderer/components/trace/TraceDialog.test.ts @@ -1,13 +1,18 @@ -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { flushPromises, mount } from '@vue/test-utils' -const { listMessageTracesMock } = vi.hoisted(() => ({ - listMessageTracesMock: vi.fn() -})) +const { listMessageTraceDiagnosticsMock, listMessageTracesMock, listMessageViewManifestsMock } = + vi.hoisted(() => ({ + listMessageTraceDiagnosticsMock: vi.fn(), + listMessageTracesMock: vi.fn(), + listMessageViewManifestsMock: vi.fn() + })) vi.mock('@api/SessionClient', () => ({ createSessionClient: vi.fn(() => ({ - listMessageTraces: listMessageTracesMock + listMessageTraceDiagnostics: listMessageTraceDiagnosticsMock, + listMessageTraces: listMessageTracesMock, + listMessageViewManifests: listMessageViewManifestsMock })) })) @@ -57,6 +62,21 @@ vi.mock( { virtual: true } ) +vi.mock( + '@shadcn/components/ui/tabs', + () => ({ + Tabs: { name: 'Tabs', template: '
' }, + TabsContent: { name: 'TabsContent', template: '
' }, + TabsList: { name: 'TabsList', template: '
' }, + TabsTrigger: { + name: 'TabsTrigger', + props: ['value'], + template: '' + } + }), + { virtual: true } +) + vi.mock( '@shadcn/components/ui/spinner', () => ({ @@ -90,42 +110,52 @@ const mountDialog = () => }) describe('TraceDialog', () => { + beforeEach(() => { + listMessageTraceDiagnosticsMock.mockReset() + listMessageTracesMock.mockReset() + listMessageViewManifestsMock.mockReset() + listMessageTraceDiagnosticsMock.mockResolvedValue({ traces: [], manifests: [] }) + }) + it('shows latest trace by default and supports switching trace history', async () => { - listMessageTracesMock.mockResolvedValue([ - { - id: 't2', - messageId: 'm1', - sessionId: 's1', - providerId: 'openai', - modelId: 'gpt-4o', - requestSeq: 2, - endpoint: 'https://api.example.com/second', - headersJson: '{"x":"2"}', - bodyJson: '{"b":2}', - truncated: false, - createdAt: 2000 - }, - { - id: 't1', - messageId: 'm1', - sessionId: 's1', - providerId: 'openai', - modelId: 'gpt-4o', - requestSeq: 1, - endpoint: 'https://api.example.com/first', - headersJson: '{"x":"1"}', - bodyJson: '{"b":1}', - truncated: false, - createdAt: 1000 - } - ]) + listMessageTraceDiagnosticsMock.mockResolvedValue({ + traces: [ + { + id: 't2', + messageId: 'm1', + sessionId: 's1', + providerId: 'openai', + modelId: 'gpt-4o', + requestSeq: 2, + endpoint: 'https://api.example.com/second', + headersJson: '{"x":"2"}', + bodyJson: '{"b":2}', + truncated: false, + createdAt: 2000 + }, + { + id: 't1', + messageId: 'm1', + sessionId: 's1', + providerId: 'openai', + modelId: 'gpt-4o', + requestSeq: 1, + endpoint: 'https://api.example.com/first', + headersJson: '{"x":"1"}', + bodyJson: '{"b":1}', + truncated: false, + createdAt: 1000 + } + ], + manifests: [] + }) const wrapper = mountDialog() await wrapper.setProps({ messageId: 'm1' }) await flushPromises() - expect(listMessageTracesMock).toHaveBeenCalledWith('m1') + expect(listMessageTraceDiagnosticsMock).toHaveBeenCalledWith('m1') expect(wrapper.text()).toContain('https://api.example.com/second') const historyButton = wrapper.findAll('button').find((btn) => btn.text().trim() === '#1') @@ -136,4 +166,76 @@ describe('TraceDialog', () => { expect(wrapper.text()).toContain('https://api.example.com/first') }) + + it('shows view manifest diagnostics when request traces are empty', async () => { + listMessageTraceDiagnosticsMock.mockResolvedValue({ + traces: [], + manifests: [ + { + sessionId: 's1', + messageId: 'm1', + requestSeq: 1, + entryId: 9, + createdAt: 2000, + manifest: { + schemaVersion: 1, + viewId: 'view_abc', + sessionId: 's1', + messageId: 'm1', + requestSeq: 1, + taskType: 'chat', + policy: 'legacy_context_v1', + policyVersion: 1, + contextBuilderVersion: 'legacy-v1', + latestEntryId: 8, + anchorEntryIds: [1], + included: [ + { + entryId: 2, + messageId: 'u1', + orderSeq: 1, + role: 'user', + source: 'tape', + reason: 'selected_history' + } + ], + excluded: [], + tokenBudget: { + contextLength: 1000, + requestedMaxTokens: 100, + effectiveMaxTokens: 100, + reserveTokens: 100, + toolReserveTokens: 0, + estimatedPromptTokens: 12 + }, + hashes: { + promptHash: 'prompt_hash', + toolDefinitionsHash: 'tool_hash', + manifestHash: 'manifest_hash' + }, + meta: { + providerId: 'openai', + modelId: 'gpt-4o', + summaryCursorOrderSeq: 1, + supportsVision: true, + supportsAudioInput: false, + traceDebugEnabled: false + }, + assembledAt: 2000 + } + } + ] + }) + + const wrapper = mountDialog() + + await wrapper.setProps({ messageId: 'm1' }) + await flushPromises() + + expect(listMessageTraceDiagnosticsMock).toHaveBeenCalledWith('m1') + expect(wrapper.text()).toContain('view_abc') + expect(wrapper.text()).toContain('legacy_context_v1') + expect(wrapper.text()).toContain('traceDialog.policyVersion') + expect(wrapper.text()).toContain('1') + }) }) From bd14a7d20992be1fcedde449b5c326fbb56480bf Mon Sep 17 00:00:00 2001 From: zerob13 Date: Mon, 15 Jun 2026 17:32:02 +0800 Subject: [PATCH 3/6] fix: address tape view manifest review feedback --- .../deepchat-tape-view-assembler/spec.md | 6 +- .../deepchat-tape-view-manifest/plan.md | 6 +- .../plan.md | 44 ++++++ .../spec.md | 45 ++++++ .../tasks.md | 10 ++ .../agentRuntimePresenter/contextBuilder.ts | 5 +- .../presenter/agentRuntimePresenter/index.ts | 14 +- .../agentRuntimePresenter/tapeViewManifest.ts | 12 +- .../presenter/agentSessionPresenter/index.ts | 36 +++-- src/renderer/api/SessionClient.ts | 6 +- .../src/components/trace/TraceDialog.vue | 10 +- src/renderer/src/i18n/da-DK/traceDialog.json | 66 ++++---- src/renderer/src/i18n/de-DE/traceDialog.json | 66 ++++---- src/renderer/src/i18n/es-ES/traceDialog.json | 66 ++++---- src/renderer/src/i18n/fa-IR/traceDialog.json | 66 ++++---- src/renderer/src/i18n/fr-FR/traceDialog.json | 66 ++++---- src/renderer/src/i18n/he-IL/traceDialog.json | 66 ++++---- src/renderer/src/i18n/id-ID/traceDialog.json | 66 ++++---- src/renderer/src/i18n/it-IT/traceDialog.json | 66 ++++---- src/renderer/src/i18n/ja-JP/traceDialog.json | 66 ++++---- src/renderer/src/i18n/ko-KR/traceDialog.json | 66 ++++---- src/renderer/src/i18n/ms-MY/traceDialog.json | 66 ++++---- src/renderer/src/i18n/pl-PL/traceDialog.json | 66 ++++---- src/renderer/src/i18n/pt-BR/traceDialog.json | 66 ++++---- src/renderer/src/i18n/ru-RU/traceDialog.json | 66 ++++---- src/renderer/src/i18n/tr-TR/traceDialog.json | 66 ++++---- src/renderer/src/i18n/vi-VN/traceDialog.json | 66 ++++---- src/renderer/src/i18n/zh-HK/traceDialog.json | 58 +++---- src/renderer/src/i18n/zh-TW/traceDialog.json | 58 +++---- src/shared/contracts/routes.ts | 4 +- .../contextBuilder.test.ts | 57 +++++++ .../components/trace/TraceDialog.test.ts | 148 +++++++++++------- 32 files changed, 889 insertions(+), 686 deletions(-) create mode 100644 docs/issues/deepchat-tape-view-manifest-pr-review/plan.md create mode 100644 docs/issues/deepchat-tape-view-manifest-pr-review/spec.md create mode 100644 docs/issues/deepchat-tape-view-manifest-pr-review/tasks.md diff --git a/docs/architecture/deepchat-tape-view-assembler/spec.md b/docs/architecture/deepchat-tape-view-assembler/spec.md index ccd06005c..10db478ae 100644 --- a/docs/architecture/deepchat-tape-view-assembler/spec.md +++ b/docs/architecture/deepchat-tape-view-assembler/spec.md @@ -21,9 +21,9 @@ observer around the production context path. The next architecture step needs a ## Non-Goals -- Changing context selection policy. -- Changing compaction policy. -- Changing provider preflight or context-pressure recovery. +- Context selection policy changes. +- Compaction policy updates. +- Provider preflight or context-pressure recovery modifications. - Adding embedding memory or cross-session recall. - Removing the legacy `contextBuilder.ts` implementation. diff --git a/docs/architecture/deepchat-tape-view-manifest/plan.md b/docs/architecture/deepchat-tape-view-manifest/plan.md index 73df6c229..70e5fb447 100644 --- a/docs/architecture/deepchat-tape-view-manifest/plan.md +++ b/docs/architecture/deepchat-tape-view-manifest/plan.md @@ -143,9 +143,9 @@ states. ## Rollout 1. Land shadow manifest assembly and tape persistence as headless runtime metadata. -2. Add route/client and tests. -3. Add trace-dialog tabs. -4. Add context parity coverage. +2. Implement route/client support and tests. +3. Extend the trace dialog with diagnostic tabs. +4. Establish context parity coverage. 5. Use collected manifest data to design the later `TapeViewAssembler` production path. ## Risks and Mitigations diff --git a/docs/issues/deepchat-tape-view-manifest-pr-review/plan.md b/docs/issues/deepchat-tape-view-manifest-pr-review/plan.md new file mode 100644 index 000000000..657255637 --- /dev/null +++ b/docs/issues/deepchat-tape-view-manifest-pr-review/plan.md @@ -0,0 +1,44 @@ +# Tape ViewManifest PR Review Fixes - Plan + +## Approach + +Apply the PR review fixes in place, keeping the existing Tape architecture and presenter boundaries intact. + +## Changes + +| Area | Plan | +| --- | --- | +| `contextBuilder.ts` | Make resume `out_of_budget` exclusions only include emitted records that were not selected. | +| `agentRuntimePresenter/index.ts` | Carry recovered `summaryCursorOrderSeq` into manifest append; refresh tape history after resume compaction before building the resume view. | +| `agentSessionPresenter/index.ts` | Wrap optional manifest/replay delegation in `try/catch`, log warnings, and return graceful fallbacks. | +| `SessionClient.ts` | Normalize `result.manifests` to an array before returning diagnostics. | +| `TraceDialog.vue` | When a request sequence is selected, only show matching trace/manifest data. | +| `routes.ts` | Replace broad route catalog annotation with `satisfies Record`. | +| i18n `traceDialog.json` | Translate new diagnostic keys for non-English locales and convert Traditional Chinese files. | +| SDD docs | Keep this issue SDD current and address small doc nitpicks. | + +## Compatibility + +- No database or IPC schema changes. +- ViewManifest remains schema version 1. +- Existing sessions without manifests continue to return empty diagnostics. + +## Test Strategy + +Run required project checks: + +```bash +pnpm run format +pnpm run i18n +pnpm run lint +``` + +Run focused checks where practical: + +```bash +pnpm run typecheck +pnpm vitest run test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts +pnpm vitest run test/main/presenter/agentRuntimePresenter/tapeViewAssembler.test.ts +pnpm vitest run test/main/presenter/agentRuntimePresenter/tapeViewManifest.test.ts +pnpm vitest run test/renderer/components/trace/TraceDialog.test.ts +``` diff --git a/docs/issues/deepchat-tape-view-manifest-pr-review/spec.md b/docs/issues/deepchat-tape-view-manifest-pr-review/spec.md new file mode 100644 index 000000000..3ab6b0799 --- /dev/null +++ b/docs/issues/deepchat-tape-view-manifest-pr-review/spec.md @@ -0,0 +1,45 @@ +# Tape ViewManifest PR Review Fixes - Spec + +Status: active issue-fix SDD for PR #1767 review follow-up. + +## Problem + +PR review identified correctness and localization issues in the Tape ViewManifest flow. Some diagnostics can fail hard instead of degrading gracefully, request/manifest diagnostics can show mismatched request sequences, manifest provenance can record stale summary cursors after context-pressure recovery, resume view assembly can use stale tape history after compaction, route-contract typing loses literal key precision, and newly added TraceDialog labels are not properly localized. + +## Goals + +1. Fix still-valid CodeRabbit review findings for PR #1767 with minimal changes. +2. Preserve Tape ViewManifest and replay-slice contracts while correcting provenance and diagnostics behavior. +3. Ensure TraceDialog diagnostic strings are localized for every supported non-English locale touched by the PR. +4. Keep route-contract literal key inference intact. +5. Commit the fixes locally without pushing. + +## Acceptance Criteria + +1. Resume context assembly refreshes tape history after compaction resolution. +2. Context-pressure recovery returns and records the recovered summary cursor in appended manifests. +3. Excluded context metadata cannot classify the same resume record as both `empty_after_formatting` and `out_of_budget`. +4. Message view-manifest listing and replay-slice export return `[]`/`null` when agent resolution fails. +5. Renderer session client always returns an array for manifest diagnostics. +6. TraceDialog returns `null` instead of falling back to another request when a selected request sequence is missing from either traces or manifests. +7. `DEEPCHAT_ROUTE_CATALOG` uses `satisfies Record` so route names remain a literal union. +8. TraceDialog diagnostic labels are translated in supported non-English locale files, including Traditional Chinese variants. +9. Review nitpicks that are small and local are addressed without broad refactors. +10. `pnpm run format`, `pnpm run i18n`, and `pnpm run lint` pass or any blocker is documented. + +## Constraints + +- Do not push changes. +- Do not weaken authentication, authorization, or validation. +- Avoid unrelated refactors and preserve existing presenter boundaries. +- Keep ViewManifest schema compatible. + +## Non-Goals + +- Redesigning Tape ViewManifest or replay contracts. +- Adding new context-selection policies. +- Reworking the full i18n pipeline beyond the reviewed TraceDialog keys. + +## Open Questions + +None. diff --git a/docs/issues/deepchat-tape-view-manifest-pr-review/tasks.md b/docs/issues/deepchat-tape-view-manifest-pr-review/tasks.md new file mode 100644 index 000000000..9fc46b6a6 --- /dev/null +++ b/docs/issues/deepchat-tape-view-manifest-pr-review/tasks.md @@ -0,0 +1,10 @@ +# Tape ViewManifest PR Review Fixes - Tasks + +- [x] T1: Inspect PR review findings and current branch state. +- [x] T2: Fix runtime provenance and resume-history correctness issues. +- [x] T3: Harden diagnostics/replay APIs and renderer request selection. +- [x] T4: Restore route catalog type precision. +- [x] T5: Translate TraceDialog diagnostic labels in non-English locales. +- [x] T6: Address local documentation/nitpick comments. +- [x] T7: Run format, i18n, lint, and focused validation. +- [x] T8: Stage intentional files and create a local commit without pushing. diff --git a/src/main/presenter/agentRuntimePresenter/contextBuilder.ts b/src/main/presenter/agentRuntimePresenter/contextBuilder.ts index 3b970ba6f..b88edd218 100644 --- a/src/main/presenter/agentRuntimePresenter/contextBuilder.ts +++ b/src/main/presenter/agentRuntimePresenter/contextBuilder.ts @@ -172,7 +172,7 @@ function parseUserRecordContent(content: string): SendMessageInput { } } -function isCompactionRecord(record: ChatMessageRecord): boolean { +export function isCompactionRecord(record: ChatMessageRecord): boolean { try { const metadata = JSON.parse(record.metadata) as MessageMetadata return metadata.messageType === 'compaction' @@ -1171,7 +1171,6 @@ export function buildResumeContextWithMetadata( messages.push({ role: 'system', content: systemPrompt }) } messages.push(...selectedHistory) - const eligibleIds = new Set(historyRecords.map((record) => record.id)) const excludedRecords: ContextExcludedRecord[] = [ ...allMessages .filter( @@ -1192,7 +1191,7 @@ export function buildResumeContextWithMetadata( reason: 'empty_after_formatting' as const })), ...historyRecords - .filter((record) => eligibleIds.has(record.id) && !selectedRecordIds.has(record.id)) + .filter((record) => emittedRecordIds.has(record.id) && !selectedRecordIds.has(record.id)) .map((record) => ({ record, reason: 'out_of_budget' as const diff --git a/src/main/presenter/agentRuntimePresenter/index.ts b/src/main/presenter/agentRuntimePresenter/index.ts index 21675bd3c..a46fd9a9b 100644 --- a/src/main/presenter/agentRuntimePresenter/index.ts +++ b/src/main/presenter/agentRuntimePresenter/index.ts @@ -2454,6 +2454,7 @@ export class AgentRuntimePresenter implements IAgentImplementation { let providerMessages = requestMessages let providerMaxTokens = requestMaxTokens let recoveredFromContextPressure = false + let manifestSummaryCursorOrderSeq = viewContext?.summaryCursorOrderSeq ?? 1 const isTtsRequest = isTtsModelConfig(requestModelConfig) || isTtsModelId(requestModelId) const effectiveRequestTools: MCPToolDefinition[] = isTtsRequest ? [] : requestTools @@ -2485,6 +2486,9 @@ export class AgentRuntimePresenter implements IAgentImplementation { signal: abortController.signal }) recoveredFromContextPressure = true + if (recovered.summaryCursorOrderSeq !== undefined) { + manifestSummaryCursorOrderSeq = recovered.summaryCursorOrderSeq + } requestMessages.splice(0, requestMessages.length, ...recovered.messages) if (recovered.systemPrompt) { replaceLeadingSystemPromptInPlace(requestMessages, recovered.systemPrompt) @@ -2537,7 +2541,7 @@ export class AgentRuntimePresenter implements IAgentImplementation { isInitialViewRequest && !recoveredFromContextPressure ? viewContext!.selection : undefined, - summaryCursorOrderSeq: viewContext?.summaryCursorOrderSeq ?? 1, + summaryCursorOrderSeq: manifestSummaryCursorOrderSeq, supportsVision: viewContext?.supportsVision ?? supportsVision, supportsAudioInput: viewContext?.supportsAudioInput ?? supportsAudioInput, traceDebugEnabled: viewContext?.traceDebugEnabled ?? traceEnabled @@ -2761,7 +2765,7 @@ export class AgentRuntimePresenter implements IAgentImplementation { interleavedReasoning: InterleavedReasoningConfig minimumProtectedTailCount: number signal: AbortSignal - }): Promise<{ messages: ChatMessage[]; systemPrompt?: string }> { + }): Promise<{ messages: ChatMessage[]; systemPrompt?: string; summaryCursorOrderSeq?: number }> { let messages = params.requestMessages const systemPromptBase = params.baseSystemPrompt ?? this.getLeadingSystemPrompt(params.requestMessages) ?? '' @@ -2804,7 +2808,8 @@ export class AgentRuntimePresenter implements IAgentImplementation { reserveTokens: params.requestedMaxTokens + estimateToolReserveTokens(params.tools), minimumProtectedTailCount: params.minimumProtectedTailCount }), - systemPrompt + systemPrompt, + summaryCursorOrderSeq: summaryState.summaryCursorOrderSeq } } @@ -3196,6 +3201,7 @@ export class AgentRuntimePresenter implements IAgentImplementation { }) : this.sessionStore.getSummaryState(sessionId) this.throwIfAbortRequested(preStreamAbortSignal) + const resumeTapeReady = this.tapeService.ensureSessionTapeReady(sessionId, this.messageStore) const systemPrompt = appendReconstructionAnchorStateSection( appendSummarySection(baseSystemPrompt, summaryState.summaryText), this.sessionStore.getReconstructionAnchorPromptState(sessionId) @@ -3208,7 +3214,7 @@ export class AgentRuntimePresenter implements IAgentImplementation { reserveTokens: maxTokens, messageStore: this.messageStore, supportsVision: this.supportsVision(state.providerId, state.modelId), - historyRecords: tapeReady.historyRecords, + historyRecords: resumeTapeReady.historyRecords, options: { summaryCursorOrderSeq: summaryState.summaryCursorOrderSeq, fallbackProtectedTurnCount: 1, diff --git a/src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts b/src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts index e4d6145d8..6f709d18f 100644 --- a/src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts +++ b/src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts @@ -1,7 +1,7 @@ import { createHash } from 'crypto' import type { ChatMessage } from '@shared/types/core/chat-message' import type { MCPToolDefinition } from '@shared/types/core/mcp' -import type { ChatMessageRecord, MessageMetadata } from '@shared/types/agent-interface' +import type { ChatMessageRecord } from '@shared/types/agent-interface' import type { DeepChatTapeViewEntryRef, DeepChatTapeViewExcludedRef, @@ -11,6 +11,7 @@ import type { DeepChatTapeViewTokenBudget } from '@shared/types/tape-view-manifest' import { estimateMessagesTokens } from './contextBuilder' +export { isCompactionRecord } from './contextBuilder' export const TAPE_VIEW_MANIFEST_EVENT_NAME = 'view/assembled' export const TAPE_VIEW_CONTEXT_BUILDER_VERSION = 'legacy-v1' as const @@ -259,12 +260,3 @@ export function buildSyntheticRequestRefs(messages: ChatMessage[]): DeepChatTape reason: message.role === 'tool' ? 'tool_loop_message' : 'selected_history' })) } - -export function isCompactionRecord(record: ChatMessageRecord): boolean { - try { - const metadata = JSON.parse(record.metadata) as MessageMetadata - return metadata.messageType === 'compaction' - } catch { - return false - } -} diff --git a/src/main/presenter/agentSessionPresenter/index.ts b/src/main/presenter/agentSessionPresenter/index.ts index 6fd29c4c2..25f8a5c9e 100644 --- a/src/main/presenter/agentSessionPresenter/index.ts +++ b/src/main/presenter/agentSessionPresenter/index.ts @@ -1442,10 +1442,18 @@ export class AgentSessionPresenter { const session = this.sessionManager.get(message.session_id) if (!session) return [] - const agent = await this.resolveAgentImplementation(session.agentId) - if (!agent.listMessageViewManifests) return [] + try { + const agent = await this.resolveAgentImplementation(session.agentId) + if (!agent.listMessageViewManifests) return [] - return await agent.listMessageViewManifests(message.session_id, normalizedMessageId) + return await agent.listMessageViewManifests(message.session_id, normalizedMessageId) + } catch (error) { + logger.warn('[AgentSessionPresenter] Failed to list message view manifests', { + messageId: normalizedMessageId, + error + }) + return [] + } } async exportMessageTapeReplaySlice( @@ -1461,14 +1469,22 @@ export class AgentSessionPresenter { const session = this.sessionManager.get(message.session_id) if (!session) return null - const agent = await this.resolveAgentImplementation(session.agentId) - if (!agent.exportMessageTapeReplaySlice) return null + try { + const agent = await this.resolveAgentImplementation(session.agentId) + if (!agent.exportMessageTapeReplaySlice) return null - return await agent.exportMessageTapeReplaySlice( - message.session_id, - normalizedMessageId, - options - ) + return await agent.exportMessageTapeReplaySlice( + message.session_id, + normalizedMessageId, + options + ) + } catch (error) { + logger.warn('[AgentSessionPresenter] Failed to export tape replay slice', { + messageId: normalizedMessageId, + error + }) + return null + } } async mergeSubagentTape( diff --git a/src/renderer/api/SessionClient.ts b/src/renderer/api/SessionClient.ts index 0858db16f..6d55aa1e4 100644 --- a/src/renderer/api/SessionClient.ts +++ b/src/renderer/api/SessionClient.ts @@ -246,15 +246,17 @@ export function createSessionClient(bridge: DeepchatBridge = getDeepchatBridge() async function listMessageTraceDiagnostics(messageId: string) { const result = await bridge.invoke(sessionsListMessageTracesRoute.name, { messageId }) + const manifests = Array.isArray(result.manifests) ? result.manifests : [] return { traces: result.traces, - manifests: result.manifests as unknown as DeepChatTapeViewManifestRecord[] + manifests: manifests as DeepChatTapeViewManifestRecord[] } } async function listMessageViewManifests(messageId: string) { const result = await bridge.invoke(sessionsListMessageTracesRoute.name, { messageId }) - return result.manifests as unknown as DeepChatTapeViewManifestRecord[] + const manifests = Array.isArray(result.manifests) ? result.manifests : [] + return manifests as DeepChatTapeViewManifestRecord[] } async function exportMessageTapeReplaySlice( diff --git a/src/renderer/src/components/trace/TraceDialog.vue b/src/renderer/src/components/trace/TraceDialog.vue index 92193af6c..c9eb52ffb 100644 --- a/src/renderer/src/components/trace/TraceDialog.vue +++ b/src/renderer/src/components/trace/TraceDialog.vue @@ -334,10 +334,7 @@ const selectedTrace = computed(() => { } if (selectedRequestSeq.value !== null) { - const matched = traceList.value.find((item) => item.requestSeq === selectedRequestSeq.value) - if (matched) { - return matched - } + return traceList.value.find((item) => item.requestSeq === selectedRequestSeq.value) ?? null } return traceList.value[0] ?? null @@ -349,10 +346,7 @@ const selectedManifest = computed(() => { } if (selectedRequestSeq.value !== null) { - const matched = manifestList.value.find((item) => item.requestSeq === selectedRequestSeq.value) - if (matched) { - return matched - } + return manifestList.value.find((item) => item.requestSeq === selectedRequestSeq.value) ?? null } return manifestList.value[0] ?? null diff --git a/src/renderer/src/i18n/da-DK/traceDialog.json b/src/renderer/src/i18n/da-DK/traceDialog.json index 4ba41d6dc..39790c8d2 100644 --- a/src/renderer/src/i18n/da-DK/traceDialog.json +++ b/src/renderer/src/i18n/da-DK/traceDialog.json @@ -15,38 +15,38 @@ "notImplementedDesc": "Denne udbyder understøtter endnu ikke forhåndsvisning", "mayNotMatch": "Bemærk: Denne forhåndsvisning er rekonstrueret ud fra de nuværende samtaleindstillinger og matcher muligvis ikke de faktiske anmodningsparametre helt præcist", "tabs": { - "request": "Request", - "view": "View", - "entries": "Entries", - "budget": "Budget" + "request": "Anmodning", + "view": "Visning", + "entries": "Poster", + "budget": "Tokenbudget" }, - "empty": "No diagnostics", - "emptyDesc": "No request trace or view manifest is available for this message", - "requestUnavailable": "No request trace", - "requestUnavailableDesc": "This message has no persisted provider request trace", - "manifestUnavailable": "No view manifest", - "manifestUnavailableDesc": "This message has no persisted Tape view manifest", - "viewId": "View ID", - "policy": "Policy", - "policyVersion": "Policy version", - "taskType": "Task type", - "requestSeq": "Request #", - "latestEntryId": "Latest entry ID", - "promptHash": "Prompt hash", - "toolDefinitionsHash": "Tool definitions hash", - "manifestHash": "Manifest hash", - "includedEntries": "Included entries", - "excludedEntries": "Excluded entries", - "entryId": "Entry ID", - "messageId": "Message ID", - "orderSeq": "Order seq", - "role": "Role", - "source": "Source", - "reason": "Reason", - "contextLength": "Context length", - "requestedMaxTokens": "Requested max tokens", - "effectiveMaxTokens": "Effective max tokens", - "reserveTokens": "Reserve tokens", - "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "empty": "Ingen diagnostik", + "emptyDesc": "Der er ingen anmodningssporing eller visningsmanifest for denne besked", + "requestUnavailable": "Ingen anmodningssporing", + "requestUnavailableDesc": "Denne besked har ingen gemt udbyderanmodningssporing", + "manifestUnavailable": "Intet visningsmanifest", + "manifestUnavailableDesc": "Denne besked har intet gemt Tape-visningsmanifest", + "viewId": "Visnings-id", + "policy": "Politik", + "policyVersion": "Politikversion", + "taskType": "Opgavetype", + "requestSeq": "Anmodningsnr.", + "latestEntryId": "Seneste post-id", + "promptHash": "Prompt-hash", + "toolDefinitionsHash": "Værktøjsdefinitionshash", + "manifestHash": "Manifest-hash", + "includedEntries": "Inkluderede poster", + "excludedEntries": "Ekskluderede poster", + "entryId": "Post-id", + "messageId": "Besked-id", + "orderSeq": "Rækkefølge", + "role": "Rolle", + "source": "Kilde", + "reason": "Årsag", + "contextLength": "Kontekstlængde", + "requestedMaxTokens": "Anmodede maks. tokens", + "effectiveMaxTokens": "Effektive maks. tokens", + "reserveTokens": "Reserverede tokens", + "toolReserveTokens": "Værktøjsreserverede tokens", + "estimatedPromptTokens": "Estimerede prompt-tokens" } diff --git a/src/renderer/src/i18n/de-DE/traceDialog.json b/src/renderer/src/i18n/de-DE/traceDialog.json index bd9662a66..1740a6d91 100644 --- a/src/renderer/src/i18n/de-DE/traceDialog.json +++ b/src/renderer/src/i18n/de-DE/traceDialog.json @@ -15,38 +15,38 @@ "notImplementedDesc": "Für diesen Anbieter ist die Vorschau noch nicht verfügbar", "mayNotMatch": "Hinweis: Diese Vorschau wird aus den aktuellen Sitzungseinstellungen rekonstruiert und stimmt möglicherweise nicht vollständig mit den tatsächlich gesendeten Parametern überein", "tabs": { - "request": "Request", - "view": "View", - "entries": "Entries", - "budget": "Budget" + "request": "Anfrage", + "view": "Ansicht", + "entries": "Einträge", + "budget": "Tokenbudget" }, - "empty": "No diagnostics", - "emptyDesc": "No request trace or view manifest is available for this message", - "requestUnavailable": "No request trace", - "requestUnavailableDesc": "This message has no persisted provider request trace", - "manifestUnavailable": "No view manifest", - "manifestUnavailableDesc": "This message has no persisted Tape view manifest", - "viewId": "View ID", - "policy": "Policy", - "policyVersion": "Policy version", - "taskType": "Task type", - "requestSeq": "Request #", - "latestEntryId": "Latest entry ID", - "promptHash": "Prompt hash", - "toolDefinitionsHash": "Tool definitions hash", - "manifestHash": "Manifest hash", - "includedEntries": "Included entries", - "excludedEntries": "Excluded entries", - "entryId": "Entry ID", - "messageId": "Message ID", - "orderSeq": "Order seq", - "role": "Role", - "source": "Source", - "reason": "Reason", - "contextLength": "Context length", - "requestedMaxTokens": "Requested max tokens", - "effectiveMaxTokens": "Effective max tokens", - "reserveTokens": "Reserve tokens", - "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "empty": "Keine Diagnosedaten", + "emptyDesc": "Für diese Nachricht ist kein Anfrage-Trace oder Ansichtsmanifest verfügbar", + "requestUnavailable": "Kein Anfrage-Trace", + "requestUnavailableDesc": "Für diese Nachricht ist kein persistierter Anbieter-Anfrage-Trace vorhanden", + "manifestUnavailable": "Kein Ansichtsmanifest", + "manifestUnavailableDesc": "Für diese Nachricht ist kein persistiertes Tape-Ansichtsmanifest vorhanden", + "viewId": "Ansichts-ID", + "policy": "Richtlinie", + "policyVersion": "Richtlinienversion", + "taskType": "Aufgabentyp", + "requestSeq": "Anfrage-Nr.", + "latestEntryId": "Neueste Eintrags-ID", + "promptHash": "Prompt-Hash", + "toolDefinitionsHash": "Werkzeugdefinitions-Hash", + "manifestHash": "Manifest-Hash", + "includedEntries": "Eingeschlossene Einträge", + "excludedEntries": "Ausgeschlossene Einträge", + "entryId": "Eintrags-ID", + "messageId": "Nachrichten-ID", + "orderSeq": "Reihenfolge", + "role": "Rolle", + "source": "Quelle", + "reason": "Grund", + "contextLength": "Kontextlänge", + "requestedMaxTokens": "Angeforderte maximale Tokens", + "effectiveMaxTokens": "Effektive maximale Tokens", + "reserveTokens": "Reservierte Tokens", + "toolReserveTokens": "Für Werkzeuge reservierte Tokens", + "estimatedPromptTokens": "Geschätzte Prompt-Tokens" } diff --git a/src/renderer/src/i18n/es-ES/traceDialog.json b/src/renderer/src/i18n/es-ES/traceDialog.json index a311a80ae..386f59a7c 100644 --- a/src/renderer/src/i18n/es-ES/traceDialog.json +++ b/src/renderer/src/i18n/es-ES/traceDialog.json @@ -15,38 +15,38 @@ "notImplementedDesc": "Este provider aún no ha implementado la vista previa de la solicitud.", "mayNotMatch": "Nota: Esta vista previa se reconstruye a partir de la configuración de conversación actual y es posible que no coincida exactamente con los parámetros de solicitud reales.", "tabs": { - "request": "Request", - "view": "View", - "entries": "Entries", - "budget": "Budget" + "request": "Solicitud", + "view": "Vista", + "entries": "Entradas", + "budget": "Presupuesto" }, - "empty": "No diagnostics", - "emptyDesc": "No request trace or view manifest is available for this message", - "requestUnavailable": "No request trace", - "requestUnavailableDesc": "This message has no persisted provider request trace", - "manifestUnavailable": "No view manifest", - "manifestUnavailableDesc": "This message has no persisted Tape view manifest", - "viewId": "View ID", - "policy": "Policy", - "policyVersion": "Policy version", - "taskType": "Task type", - "requestSeq": "Request #", - "latestEntryId": "Latest entry ID", - "promptHash": "Prompt hash", - "toolDefinitionsHash": "Tool definitions hash", - "manifestHash": "Manifest hash", - "includedEntries": "Included entries", - "excludedEntries": "Excluded entries", - "entryId": "Entry ID", - "messageId": "Message ID", - "orderSeq": "Order seq", - "role": "Role", - "source": "Source", - "reason": "Reason", - "contextLength": "Context length", - "requestedMaxTokens": "Requested max tokens", - "effectiveMaxTokens": "Effective max tokens", - "reserveTokens": "Reserve tokens", - "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "empty": "Sin diagnósticos", + "emptyDesc": "No hay traza de solicitud ni manifiesto de vista disponible para este mensaje", + "requestUnavailable": "Sin traza de solicitud", + "requestUnavailableDesc": "Este mensaje no tiene una traza de solicitud del proveedor guardada", + "manifestUnavailable": "Sin manifiesto de vista", + "manifestUnavailableDesc": "Este mensaje no tiene un manifiesto de vista de Tape guardado", + "viewId": "ID de vista", + "policy": "Política", + "policyVersion": "Versión de política", + "taskType": "Tipo de tarea", + "requestSeq": "N.º de solicitud", + "latestEntryId": "ID de la entrada más reciente", + "promptHash": "Hash del prompt", + "toolDefinitionsHash": "Hash de definiciones de herramientas", + "manifestHash": "Hash del manifiesto", + "includedEntries": "Entradas incluidas", + "excludedEntries": "Entradas excluidas", + "entryId": "ID de entrada", + "messageId": "ID de mensaje", + "orderSeq": "Secuencia de orden", + "role": "Rol", + "source": "Origen", + "reason": "Motivo", + "contextLength": "Longitud del contexto", + "requestedMaxTokens": "Tokens máximos solicitados", + "effectiveMaxTokens": "Tokens máximos efectivos", + "reserveTokens": "Tokens reservados", + "toolReserveTokens": "Tokens reservados para herramientas", + "estimatedPromptTokens": "Tokens estimados del prompt" } diff --git a/src/renderer/src/i18n/fa-IR/traceDialog.json b/src/renderer/src/i18n/fa-IR/traceDialog.json index d97e644ee..1842b674f 100644 --- a/src/renderer/src/i18n/fa-IR/traceDialog.json +++ b/src/renderer/src/i18n/fa-IR/traceDialog.json @@ -15,38 +15,38 @@ "model": "مدل", "title": "پیش‌نمایش پارامتر درخواست", "tabs": { - "request": "Request", - "view": "View", - "entries": "Entries", - "budget": "Budget" + "request": "درخواست", + "view": "نما", + "entries": "ورودی‌ها", + "budget": "بودجه" }, - "empty": "No diagnostics", - "emptyDesc": "No request trace or view manifest is available for this message", - "requestUnavailable": "No request trace", - "requestUnavailableDesc": "This message has no persisted provider request trace", - "manifestUnavailable": "No view manifest", - "manifestUnavailableDesc": "This message has no persisted Tape view manifest", - "viewId": "View ID", - "policy": "Policy", - "policyVersion": "Policy version", - "taskType": "Task type", - "requestSeq": "Request #", - "latestEntryId": "Latest entry ID", - "promptHash": "Prompt hash", - "toolDefinitionsHash": "Tool definitions hash", - "manifestHash": "Manifest hash", - "includedEntries": "Included entries", - "excludedEntries": "Excluded entries", - "entryId": "Entry ID", - "messageId": "Message ID", - "orderSeq": "Order seq", - "role": "Role", - "source": "Source", - "reason": "Reason", - "contextLength": "Context length", - "requestedMaxTokens": "Requested max tokens", - "effectiveMaxTokens": "Effective max tokens", - "reserveTokens": "Reserve tokens", - "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "empty": "بدون دادهٔ عیب‌یابی", + "emptyDesc": "برای این پیام هیچ ردگیری درخواست یا مانیفست نما موجود نیست", + "requestUnavailable": "بدون ردگیری درخواست", + "requestUnavailableDesc": "این پیام ردگیری درخواست ارائه‌دهندهٔ ذخیره‌شده ندارد", + "manifestUnavailable": "بدون مانیفست نما", + "manifestUnavailableDesc": "این پیام مانیفست نمای Tape ذخیره‌شده ندارد", + "viewId": "شناسهٔ نما", + "policy": "سیاست", + "policyVersion": "نسخهٔ سیاست", + "taskType": "نوع وظیفه", + "requestSeq": "شمارهٔ درخواست", + "latestEntryId": "شناسهٔ آخرین ورودی", + "promptHash": "هش پرامپت", + "toolDefinitionsHash": "هش تعریف‌های ابزار", + "manifestHash": "هش مانیفست", + "includedEntries": "ورودی‌های شامل‌شده", + "excludedEntries": "ورودی‌های حذف‌شده", + "entryId": "شناسهٔ ورودی", + "messageId": "شناسهٔ پیام", + "orderSeq": "ترتیب", + "role": "نقش", + "source": "منبع", + "reason": "دلیل", + "contextLength": "طول زمینه", + "requestedMaxTokens": "حداکثر توکن‌های درخواستی", + "effectiveMaxTokens": "حداکثر توکن‌های مؤثر", + "reserveTokens": "توکن‌های رزرو", + "toolReserveTokens": "توکن‌های رزرو ابزار", + "estimatedPromptTokens": "توکن‌های تخمینی پرامپت" } diff --git a/src/renderer/src/i18n/fr-FR/traceDialog.json b/src/renderer/src/i18n/fr-FR/traceDialog.json index 8e44056e7..6a0648b04 100644 --- a/src/renderer/src/i18n/fr-FR/traceDialog.json +++ b/src/renderer/src/i18n/fr-FR/traceDialog.json @@ -15,38 +15,38 @@ "model": "Modèle", "title": "Aperçu des paramètres de requête", "tabs": { - "request": "Request", - "view": "View", - "entries": "Entries", - "budget": "Budget" + "request": "Requête", + "view": "Vue", + "entries": "Entrées", + "budget": "Budget de jetons" }, - "empty": "No diagnostics", - "emptyDesc": "No request trace or view manifest is available for this message", - "requestUnavailable": "No request trace", - "requestUnavailableDesc": "This message has no persisted provider request trace", - "manifestUnavailable": "No view manifest", - "manifestUnavailableDesc": "This message has no persisted Tape view manifest", - "viewId": "View ID", - "policy": "Policy", - "policyVersion": "Policy version", - "taskType": "Task type", - "requestSeq": "Request #", - "latestEntryId": "Latest entry ID", - "promptHash": "Prompt hash", - "toolDefinitionsHash": "Tool definitions hash", - "manifestHash": "Manifest hash", - "includedEntries": "Included entries", - "excludedEntries": "Excluded entries", - "entryId": "Entry ID", - "messageId": "Message ID", - "orderSeq": "Order seq", - "role": "Role", - "source": "Source", - "reason": "Reason", - "contextLength": "Context length", - "requestedMaxTokens": "Requested max tokens", - "effectiveMaxTokens": "Effective max tokens", - "reserveTokens": "Reserve tokens", - "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "empty": "Aucun diagnostic", + "emptyDesc": "Aucune trace de requête ni aucun manifeste de vue n’est disponible pour ce message", + "requestUnavailable": "Aucune trace de requête", + "requestUnavailableDesc": "Ce message ne possède aucune trace de requête fournisseur persistée", + "manifestUnavailable": "Aucun manifeste de vue", + "manifestUnavailableDesc": "Ce message ne possède aucun manifeste de vue Tape persisté", + "viewId": "ID de vue", + "policy": "Politique", + "policyVersion": "Version de la politique", + "taskType": "Type de tâche", + "requestSeq": "N° de requête", + "latestEntryId": "ID de la dernière entrée", + "promptHash": "Hash du prompt", + "toolDefinitionsHash": "Hash des définitions d’outils", + "manifestHash": "Hash du manifeste", + "includedEntries": "Entrées incluses", + "excludedEntries": "Entrées exclues", + "entryId": "ID d’entrée", + "messageId": "ID du message", + "orderSeq": "Ordre", + "role": "Rôle", + "source": "Origine", + "reason": "Raison", + "contextLength": "Longueur du contexte", + "requestedMaxTokens": "Tokens maximum demandés", + "effectiveMaxTokens": "Tokens maximum effectifs", + "reserveTokens": "Tokens réservés", + "toolReserveTokens": "Tokens réservés aux outils", + "estimatedPromptTokens": "Tokens estimés du prompt" } diff --git a/src/renderer/src/i18n/he-IL/traceDialog.json b/src/renderer/src/i18n/he-IL/traceDialog.json index 5c5829537..fe3e56391 100644 --- a/src/renderer/src/i18n/he-IL/traceDialog.json +++ b/src/renderer/src/i18n/he-IL/traceDialog.json @@ -15,38 +15,38 @@ "notImplementedDesc": "ספק זה טרם יישם תצוגה מקדימה של הבקשה", "mayNotMatch": "הערה: תצוגה מקדימה זו משוחזרת מהגדרות השיחה הנוכחיות ועשויה שלא להתאים במדויק לפרמטרי הבקשה בפועל", "tabs": { - "request": "Request", - "view": "View", - "entries": "Entries", - "budget": "Budget" + "request": "בקשה", + "view": "תצוגה", + "entries": "רשומות", + "budget": "תקציב" }, - "empty": "No diagnostics", - "emptyDesc": "No request trace or view manifest is available for this message", - "requestUnavailable": "No request trace", - "requestUnavailableDesc": "This message has no persisted provider request trace", - "manifestUnavailable": "No view manifest", - "manifestUnavailableDesc": "This message has no persisted Tape view manifest", - "viewId": "View ID", - "policy": "Policy", - "policyVersion": "Policy version", - "taskType": "Task type", - "requestSeq": "Request #", - "latestEntryId": "Latest entry ID", - "promptHash": "Prompt hash", - "toolDefinitionsHash": "Tool definitions hash", - "manifestHash": "Manifest hash", - "includedEntries": "Included entries", - "excludedEntries": "Excluded entries", - "entryId": "Entry ID", - "messageId": "Message ID", - "orderSeq": "Order seq", - "role": "Role", - "source": "Source", - "reason": "Reason", - "contextLength": "Context length", - "requestedMaxTokens": "Requested max tokens", - "effectiveMaxTokens": "Effective max tokens", - "reserveTokens": "Reserve tokens", - "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "empty": "אין אבחון", + "emptyDesc": "אין מעקב בקשה או מניפסט תצוגה זמינים עבור הודעה זו", + "requestUnavailable": "אין מעקב בקשה", + "requestUnavailableDesc": "להודעה זו אין מעקב בקשת ספק שנשמר", + "manifestUnavailable": "אין מניפסט תצוגה", + "manifestUnavailableDesc": "להודעה זו אין מניפסט תצוגת Tape שנשמר", + "viewId": "מזהה תצוגה", + "policy": "מדיניות", + "policyVersion": "גרסת מדיניות", + "taskType": "סוג משימה", + "requestSeq": "מס׳ בקשה", + "latestEntryId": "מזהה הרשומה האחרונה", + "promptHash": "גיבוב הפרומפט", + "toolDefinitionsHash": "גיבוב הגדרות הכלים", + "manifestHash": "גיבוב המניפסט", + "includedEntries": "רשומות כלולות", + "excludedEntries": "רשומות שלא נכללו", + "entryId": "מזהה רשומה", + "messageId": "מזהה הודעה", + "orderSeq": "סדר", + "role": "תפקיד", + "source": "מקור", + "reason": "סיבה", + "contextLength": "אורך הקשר", + "requestedMaxTokens": "מספר טוקנים מרבי שהתבקש", + "effectiveMaxTokens": "מספר טוקנים מרבי בפועל", + "reserveTokens": "טוקנים שמורים", + "toolReserveTokens": "טוקנים שמורים לכלים", + "estimatedPromptTokens": "טוקני פרומפט משוערים" } diff --git a/src/renderer/src/i18n/id-ID/traceDialog.json b/src/renderer/src/i18n/id-ID/traceDialog.json index 521d434a5..486ae7b48 100644 --- a/src/renderer/src/i18n/id-ID/traceDialog.json +++ b/src/renderer/src/i18n/id-ID/traceDialog.json @@ -15,38 +15,38 @@ "notImplementedDesc": "Pemasok ini belum tersedia untuk pratinjau", "mayNotMatch": "Catatan: Pratinjau ini dibuat ulang berdasarkan pengaturan sesi saat ini dan mungkin tidak sama persis dengan parameter yang sebenarnya dikirim.", "tabs": { - "request": "Request", - "view": "View", - "entries": "Entries", - "budget": "Budget" + "request": "Permintaan", + "view": "Tampilan", + "entries": "Entri", + "budget": "Anggaran" }, - "empty": "No diagnostics", - "emptyDesc": "No request trace or view manifest is available for this message", - "requestUnavailable": "No request trace", - "requestUnavailableDesc": "This message has no persisted provider request trace", - "manifestUnavailable": "No view manifest", - "manifestUnavailableDesc": "This message has no persisted Tape view manifest", - "viewId": "View ID", - "policy": "Policy", - "policyVersion": "Policy version", - "taskType": "Task type", - "requestSeq": "Request #", - "latestEntryId": "Latest entry ID", - "promptHash": "Prompt hash", - "toolDefinitionsHash": "Tool definitions hash", - "manifestHash": "Manifest hash", - "includedEntries": "Included entries", - "excludedEntries": "Excluded entries", - "entryId": "Entry ID", - "messageId": "Message ID", - "orderSeq": "Order seq", - "role": "Role", - "source": "Source", - "reason": "Reason", - "contextLength": "Context length", - "requestedMaxTokens": "Requested max tokens", - "effectiveMaxTokens": "Effective max tokens", - "reserveTokens": "Reserve tokens", - "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "empty": "Tidak ada diagnostik", + "emptyDesc": "Tidak ada jejak permintaan atau manifes tampilan untuk pesan ini", + "requestUnavailable": "Tidak ada jejak permintaan", + "requestUnavailableDesc": "Pesan ini tidak memiliki jejak permintaan penyedia yang tersimpan", + "manifestUnavailable": "Tidak ada manifes tampilan", + "manifestUnavailableDesc": "Pesan ini tidak memiliki manifes tampilan Tape yang tersimpan", + "viewId": "ID tampilan", + "policy": "Kebijakan", + "policyVersion": "Versi kebijakan", + "taskType": "Jenis tugas", + "requestSeq": "No. permintaan", + "latestEntryId": "ID entri terbaru", + "promptHash": "Hash prompt", + "toolDefinitionsHash": "Hash definisi alat", + "manifestHash": "Hash manifes", + "includedEntries": "Entri yang disertakan", + "excludedEntries": "Entri yang dikecualikan", + "entryId": "ID entri", + "messageId": "ID pesan", + "orderSeq": "Urutan", + "role": "Peran", + "source": "Sumber", + "reason": "Alasan", + "contextLength": "Panjang konteks", + "requestedMaxTokens": "Token maksimum yang diminta", + "effectiveMaxTokens": "Token maksimum efektif", + "reserveTokens": "Token cadangan", + "toolReserveTokens": "Token cadangan alat", + "estimatedPromptTokens": "Estimasi token prompt" } diff --git a/src/renderer/src/i18n/it-IT/traceDialog.json b/src/renderer/src/i18n/it-IT/traceDialog.json index 2daf3e320..3d02fc696 100644 --- a/src/renderer/src/i18n/it-IT/traceDialog.json +++ b/src/renderer/src/i18n/it-IT/traceDialog.json @@ -15,38 +15,38 @@ "notImplementedDesc": "Questo provider non supporta ancora l'anteprima", "mayNotMatch": "Nota: questa anteprima è ricostruita dalle impostazioni correnti della sessione e potrebbe non corrispondere esattamente ai parametri inviati", "tabs": { - "request": "Request", - "view": "View", - "entries": "Entries", - "budget": "Budget" + "request": "Richiesta", + "view": "Vista", + "entries": "Voci", + "budget": "Budget token" }, - "empty": "No diagnostics", - "emptyDesc": "No request trace or view manifest is available for this message", - "requestUnavailable": "No request trace", - "requestUnavailableDesc": "This message has no persisted provider request trace", - "manifestUnavailable": "No view manifest", - "manifestUnavailableDesc": "This message has no persisted Tape view manifest", - "viewId": "View ID", - "policy": "Policy", - "policyVersion": "Policy version", - "taskType": "Task type", - "requestSeq": "Request #", - "latestEntryId": "Latest entry ID", - "promptHash": "Prompt hash", - "toolDefinitionsHash": "Tool definitions hash", - "manifestHash": "Manifest hash", - "includedEntries": "Included entries", - "excludedEntries": "Excluded entries", - "entryId": "Entry ID", - "messageId": "Message ID", - "orderSeq": "Order seq", - "role": "Role", - "source": "Source", - "reason": "Reason", - "contextLength": "Context length", - "requestedMaxTokens": "Requested max tokens", - "effectiveMaxTokens": "Effective max tokens", - "reserveTokens": "Reserve tokens", - "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "empty": "Nessuna diagnostica", + "emptyDesc": "Per questo messaggio non è disponibile alcuna traccia della richiesta o manifesto della vista", + "requestUnavailable": "Nessuna traccia della richiesta", + "requestUnavailableDesc": "Questo messaggio non ha una traccia persistita della richiesta al provider", + "manifestUnavailable": "Nessun manifesto della vista", + "manifestUnavailableDesc": "Questo messaggio non ha un manifesto della vista Tape persistito", + "viewId": "ID vista", + "policy": "Criterio", + "policyVersion": "Versione criterio", + "taskType": "Tipo di attività", + "requestSeq": "N. richiesta", + "latestEntryId": "ID voce più recente", + "promptHash": "Hash del prompt", + "toolDefinitionsHash": "Hash delle definizioni degli strumenti", + "manifestHash": "Hash del manifesto", + "includedEntries": "Voci incluse", + "excludedEntries": "Voci escluse", + "entryId": "ID voce", + "messageId": "ID messaggio", + "orderSeq": "Ordine", + "role": "Ruolo", + "source": "Origine", + "reason": "Motivo", + "contextLength": "Lunghezza del contesto", + "requestedMaxTokens": "Token massimi richiesti", + "effectiveMaxTokens": "Token massimi effettivi", + "reserveTokens": "Token riservati", + "toolReserveTokens": "Token riservati agli strumenti", + "estimatedPromptTokens": "Token stimati del prompt" } diff --git a/src/renderer/src/i18n/ja-JP/traceDialog.json b/src/renderer/src/i18n/ja-JP/traceDialog.json index bd1f1ece0..dc5ecb558 100644 --- a/src/renderer/src/i18n/ja-JP/traceDialog.json +++ b/src/renderer/src/i18n/ja-JP/traceDialog.json @@ -15,38 +15,38 @@ "model": "モデル", "title": "リクエストパラメータプレビュー", "tabs": { - "request": "Request", - "view": "View", - "entries": "Entries", - "budget": "Budget" + "request": "リクエスト", + "view": "ビュー", + "entries": "エントリ", + "budget": "予算" }, - "empty": "No diagnostics", - "emptyDesc": "No request trace or view manifest is available for this message", - "requestUnavailable": "No request trace", - "requestUnavailableDesc": "This message has no persisted provider request trace", - "manifestUnavailable": "No view manifest", - "manifestUnavailableDesc": "This message has no persisted Tape view manifest", - "viewId": "View ID", - "policy": "Policy", - "policyVersion": "Policy version", - "taskType": "Task type", - "requestSeq": "Request #", - "latestEntryId": "Latest entry ID", - "promptHash": "Prompt hash", - "toolDefinitionsHash": "Tool definitions hash", - "manifestHash": "Manifest hash", - "includedEntries": "Included entries", - "excludedEntries": "Excluded entries", - "entryId": "Entry ID", - "messageId": "Message ID", - "orderSeq": "Order seq", - "role": "Role", - "source": "Source", - "reason": "Reason", - "contextLength": "Context length", - "requestedMaxTokens": "Requested max tokens", - "effectiveMaxTokens": "Effective max tokens", - "reserveTokens": "Reserve tokens", - "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "empty": "診断情報なし", + "emptyDesc": "このメッセージにはリクエストトレースまたはビューマニフェストがありません", + "requestUnavailable": "リクエストトレースなし", + "requestUnavailableDesc": "このメッセージには保存済みのプロバイダーリクエストトレースがありません", + "manifestUnavailable": "ビューマニフェストなし", + "manifestUnavailableDesc": "このメッセージには保存済みの Tape ビューマニフェストがありません", + "viewId": "ビュー ID", + "policy": "ポリシー", + "policyVersion": "ポリシーバージョン", + "taskType": "タスク種別", + "requestSeq": "リクエスト番号", + "latestEntryId": "最新エントリ ID", + "promptHash": "プロンプトハッシュ", + "toolDefinitionsHash": "ツール定義ハッシュ", + "manifestHash": "マニフェストハッシュ", + "includedEntries": "含まれるエントリ", + "excludedEntries": "除外されたエントリ", + "entryId": "エントリ ID", + "messageId": "メッセージ ID", + "orderSeq": "順序", + "role": "ロール", + "source": "ソース", + "reason": "理由", + "contextLength": "コンテキスト長", + "requestedMaxTokens": "要求された最大トークン数", + "effectiveMaxTokens": "有効な最大トークン数", + "reserveTokens": "予約トークン", + "toolReserveTokens": "ツール予約トークン", + "estimatedPromptTokens": "推定プロンプトトークン数" } diff --git a/src/renderer/src/i18n/ko-KR/traceDialog.json b/src/renderer/src/i18n/ko-KR/traceDialog.json index 5881e5c45..00b7f0bd2 100644 --- a/src/renderer/src/i18n/ko-KR/traceDialog.json +++ b/src/renderer/src/i18n/ko-KR/traceDialog.json @@ -15,38 +15,38 @@ "model": "모델", "title": "요청 매개변수 미리보기", "tabs": { - "request": "Request", - "view": "View", - "entries": "Entries", - "budget": "Budget" + "request": "요청", + "view": "보기", + "entries": "항목", + "budget": "예산" }, - "empty": "No diagnostics", - "emptyDesc": "No request trace or view manifest is available for this message", - "requestUnavailable": "No request trace", - "requestUnavailableDesc": "This message has no persisted provider request trace", - "manifestUnavailable": "No view manifest", - "manifestUnavailableDesc": "This message has no persisted Tape view manifest", - "viewId": "View ID", - "policy": "Policy", - "policyVersion": "Policy version", - "taskType": "Task type", - "requestSeq": "Request #", - "latestEntryId": "Latest entry ID", - "promptHash": "Prompt hash", - "toolDefinitionsHash": "Tool definitions hash", - "manifestHash": "Manifest hash", - "includedEntries": "Included entries", - "excludedEntries": "Excluded entries", - "entryId": "Entry ID", - "messageId": "Message ID", - "orderSeq": "Order seq", - "role": "Role", - "source": "Source", - "reason": "Reason", - "contextLength": "Context length", - "requestedMaxTokens": "Requested max tokens", - "effectiveMaxTokens": "Effective max tokens", - "reserveTokens": "Reserve tokens", - "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "empty": "진단 없음", + "emptyDesc": "이 메시지에는 요청 추적 또는 보기 매니페스트가 없습니다", + "requestUnavailable": "요청 추적 없음", + "requestUnavailableDesc": "이 메시지에는 저장된 제공자 요청 추적이 없습니다", + "manifestUnavailable": "보기 매니페스트 없음", + "manifestUnavailableDesc": "이 메시지에는 저장된 Tape 보기 매니페스트가 없습니다", + "viewId": "보기 ID", + "policy": "정책", + "policyVersion": "정책 버전", + "taskType": "작업 유형", + "requestSeq": "요청 번호", + "latestEntryId": "최신 항목 ID", + "promptHash": "프롬프트 해시", + "toolDefinitionsHash": "도구 정의 해시", + "manifestHash": "매니페스트 해시", + "includedEntries": "포함된 항목", + "excludedEntries": "제외된 항목", + "entryId": "항목 ID", + "messageId": "메시지 ID", + "orderSeq": "순서", + "role": "역할", + "source": "소스", + "reason": "이유", + "contextLength": "컨텍스트 길이", + "requestedMaxTokens": "요청된 최대 토큰", + "effectiveMaxTokens": "유효 최대 토큰", + "reserveTokens": "예약 토큰", + "toolReserveTokens": "도구 예약 토큰", + "estimatedPromptTokens": "예상 프롬프트 토큰" } diff --git a/src/renderer/src/i18n/ms-MY/traceDialog.json b/src/renderer/src/i18n/ms-MY/traceDialog.json index 9ecf3b8a3..165834146 100644 --- a/src/renderer/src/i18n/ms-MY/traceDialog.json +++ b/src/renderer/src/i18n/ms-MY/traceDialog.json @@ -15,38 +15,38 @@ "notImplementedDesc": "Pembekal ini belum tersedia untuk pratonton", "mayNotMatch": "Nota: Pratonton ini dibina semula berdasarkan tetapan sesi semasa dan mungkin tidak sama persis dengan parameter yang sebenarnya dihantar.", "tabs": { - "request": "Request", - "view": "View", - "entries": "Entries", - "budget": "Budget" + "request": "Permintaan", + "view": "Paparan", + "entries": "Entri", + "budget": "Bajet" }, - "empty": "No diagnostics", - "emptyDesc": "No request trace or view manifest is available for this message", - "requestUnavailable": "No request trace", - "requestUnavailableDesc": "This message has no persisted provider request trace", - "manifestUnavailable": "No view manifest", - "manifestUnavailableDesc": "This message has no persisted Tape view manifest", - "viewId": "View ID", - "policy": "Policy", - "policyVersion": "Policy version", - "taskType": "Task type", - "requestSeq": "Request #", - "latestEntryId": "Latest entry ID", - "promptHash": "Prompt hash", - "toolDefinitionsHash": "Tool definitions hash", - "manifestHash": "Manifest hash", - "includedEntries": "Included entries", - "excludedEntries": "Excluded entries", - "entryId": "Entry ID", - "messageId": "Message ID", - "orderSeq": "Order seq", - "role": "Role", - "source": "Source", - "reason": "Reason", - "contextLength": "Context length", - "requestedMaxTokens": "Requested max tokens", - "effectiveMaxTokens": "Effective max tokens", - "reserveTokens": "Reserve tokens", - "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "empty": "Tiada diagnostik", + "emptyDesc": "Tiada jejak permintaan atau manifes paparan tersedia untuk mesej ini", + "requestUnavailable": "Tiada jejak permintaan", + "requestUnavailableDesc": "Mesej ini tiada jejak permintaan penyedia yang disimpan", + "manifestUnavailable": "Tiada manifes paparan", + "manifestUnavailableDesc": "Mesej ini tiada manifes paparan Tape yang disimpan", + "viewId": "ID paparan", + "policy": "Dasar", + "policyVersion": "Versi dasar", + "taskType": "Jenis tugas", + "requestSeq": "No. permintaan", + "latestEntryId": "ID entri terkini", + "promptHash": "Hash prompt", + "toolDefinitionsHash": "Hash definisi alat", + "manifestHash": "Hash manifes", + "includedEntries": "Entri disertakan", + "excludedEntries": "Entri dikecualikan", + "entryId": "ID entri", + "messageId": "ID mesej", + "orderSeq": "Urutan", + "role": "Peranan", + "source": "Sumber", + "reason": "Sebab", + "contextLength": "Panjang konteks", + "requestedMaxTokens": "Token maksimum diminta", + "effectiveMaxTokens": "Token maksimum berkesan", + "reserveTokens": "Token simpanan", + "toolReserveTokens": "Token simpanan alat", + "estimatedPromptTokens": "Anggaran token prompt" } diff --git a/src/renderer/src/i18n/pl-PL/traceDialog.json b/src/renderer/src/i18n/pl-PL/traceDialog.json index 999ed85ac..c7c43668c 100644 --- a/src/renderer/src/i18n/pl-PL/traceDialog.json +++ b/src/renderer/src/i18n/pl-PL/traceDialog.json @@ -15,38 +15,38 @@ "notImplementedDesc": "Ten dostawca nie wdrożył jeszcze podglądu żądań", "mayNotMatch": "Uwaga: ten podgląd jest rekonstruowany na podstawie bieżących ustawień konwersacji i może nie odpowiadać dokładnie rzeczywistym parametrom żądania", "tabs": { - "request": "Request", - "view": "View", - "entries": "Entries", - "budget": "Budget" + "request": "Żądanie", + "view": "Widok", + "entries": "Wpisy", + "budget": "Budżet" }, - "empty": "No diagnostics", - "emptyDesc": "No request trace or view manifest is available for this message", - "requestUnavailable": "No request trace", - "requestUnavailableDesc": "This message has no persisted provider request trace", - "manifestUnavailable": "No view manifest", - "manifestUnavailableDesc": "This message has no persisted Tape view manifest", - "viewId": "View ID", - "policy": "Policy", - "policyVersion": "Policy version", - "taskType": "Task type", - "requestSeq": "Request #", - "latestEntryId": "Latest entry ID", - "promptHash": "Prompt hash", - "toolDefinitionsHash": "Tool definitions hash", - "manifestHash": "Manifest hash", - "includedEntries": "Included entries", - "excludedEntries": "Excluded entries", - "entryId": "Entry ID", - "messageId": "Message ID", - "orderSeq": "Order seq", - "role": "Role", - "source": "Source", - "reason": "Reason", - "contextLength": "Context length", - "requestedMaxTokens": "Requested max tokens", - "effectiveMaxTokens": "Effective max tokens", - "reserveTokens": "Reserve tokens", - "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "empty": "Brak diagnostyki", + "emptyDesc": "Dla tej wiadomości nie ma śladu żądania ani manifestu widoku", + "requestUnavailable": "Brak śladu żądania", + "requestUnavailableDesc": "Ta wiadomość nie ma zapisanego śladu żądania dostawcy", + "manifestUnavailable": "Brak manifestu widoku", + "manifestUnavailableDesc": "Ta wiadomość nie ma zapisanego manifestu widoku Tape", + "viewId": "ID widoku", + "policy": "Zasada", + "policyVersion": "Wersja zasady", + "taskType": "Typ zadania", + "requestSeq": "Nr żądania", + "latestEntryId": "ID najnowszego wpisu", + "promptHash": "Hash promptu", + "toolDefinitionsHash": "Hash definicji narzędzi", + "manifestHash": "Hash manifestu", + "includedEntries": "Uwzględnione wpisy", + "excludedEntries": "Wykluczone wpisy", + "entryId": "ID wpisu", + "messageId": "ID wiadomości", + "orderSeq": "Kolejność", + "role": "Rola", + "source": "Źródło", + "reason": "Powód", + "contextLength": "Długość kontekstu", + "requestedMaxTokens": "Żądane maks. tokeny", + "effectiveMaxTokens": "Efektywne maks. tokeny", + "reserveTokens": "Zarezerwowane tokeny", + "toolReserveTokens": "Tokeny zarezerwowane dla narzędzi", + "estimatedPromptTokens": "Szacowane tokeny promptu" } diff --git a/src/renderer/src/i18n/pt-BR/traceDialog.json b/src/renderer/src/i18n/pt-BR/traceDialog.json index d32106226..d6d94b205 100644 --- a/src/renderer/src/i18n/pt-BR/traceDialog.json +++ b/src/renderer/src/i18n/pt-BR/traceDialog.json @@ -15,38 +15,38 @@ "model": "Modelo", "title": "Prévia dos parâmetros da solicitação", "tabs": { - "request": "Request", - "view": "View", - "entries": "Entries", - "budget": "Budget" + "request": "Requisição", + "view": "Visualização", + "entries": "Entradas", + "budget": "Orçamento" }, - "empty": "No diagnostics", - "emptyDesc": "No request trace or view manifest is available for this message", - "requestUnavailable": "No request trace", - "requestUnavailableDesc": "This message has no persisted provider request trace", - "manifestUnavailable": "No view manifest", - "manifestUnavailableDesc": "This message has no persisted Tape view manifest", - "viewId": "View ID", - "policy": "Policy", - "policyVersion": "Policy version", - "taskType": "Task type", - "requestSeq": "Request #", - "latestEntryId": "Latest entry ID", - "promptHash": "Prompt hash", - "toolDefinitionsHash": "Tool definitions hash", - "manifestHash": "Manifest hash", - "includedEntries": "Included entries", - "excludedEntries": "Excluded entries", - "entryId": "Entry ID", - "messageId": "Message ID", - "orderSeq": "Order seq", - "role": "Role", - "source": "Source", - "reason": "Reason", - "contextLength": "Context length", - "requestedMaxTokens": "Requested max tokens", - "effectiveMaxTokens": "Effective max tokens", - "reserveTokens": "Reserve tokens", - "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "empty": "Sem diagnósticos", + "emptyDesc": "Não há rastreamento de requisição nem manifesto de visualização disponível para esta mensagem", + "requestUnavailable": "Sem rastreamento de requisição", + "requestUnavailableDesc": "Esta mensagem não tem rastreamento persistido da requisição ao provedor", + "manifestUnavailable": "Sem manifesto de visualização", + "manifestUnavailableDesc": "Esta mensagem não tem manifesto de visualização do Tape persistido", + "viewId": "ID da visualização", + "policy": "Política", + "policyVersion": "Versão da política", + "taskType": "Tipo de tarefa", + "requestSeq": "Nº da requisição", + "latestEntryId": "ID da entrada mais recente", + "promptHash": "Hash do prompt", + "toolDefinitionsHash": "Hash das definições de ferramentas", + "manifestHash": "Hash do manifesto", + "includedEntries": "Entradas incluídas", + "excludedEntries": "Entradas excluídas", + "entryId": "ID da entrada", + "messageId": "ID da mensagem", + "orderSeq": "Ordem", + "role": "Função", + "source": "Origem", + "reason": "Motivo", + "contextLength": "Tamanho do contexto", + "requestedMaxTokens": "Máximo de tokens solicitado", + "effectiveMaxTokens": "Máximo de tokens efetivo", + "reserveTokens": "Tokens reservados", + "toolReserveTokens": "Tokens reservados para ferramentas", + "estimatedPromptTokens": "Tokens estimados do prompt" } diff --git a/src/renderer/src/i18n/ru-RU/traceDialog.json b/src/renderer/src/i18n/ru-RU/traceDialog.json index fabf3baeb..173a7e9dc 100644 --- a/src/renderer/src/i18n/ru-RU/traceDialog.json +++ b/src/renderer/src/i18n/ru-RU/traceDialog.json @@ -15,38 +15,38 @@ "title": "Предварительный просмотр параметров запроса", "model": "Модель", "tabs": { - "request": "Request", - "view": "View", - "entries": "Entries", - "budget": "Budget" + "request": "Запрос", + "view": "Представление", + "entries": "Записи", + "budget": "Бюджет" }, - "empty": "No diagnostics", - "emptyDesc": "No request trace or view manifest is available for this message", - "requestUnavailable": "No request trace", - "requestUnavailableDesc": "This message has no persisted provider request trace", - "manifestUnavailable": "No view manifest", - "manifestUnavailableDesc": "This message has no persisted Tape view manifest", - "viewId": "View ID", - "policy": "Policy", - "policyVersion": "Policy version", - "taskType": "Task type", - "requestSeq": "Request #", - "latestEntryId": "Latest entry ID", - "promptHash": "Prompt hash", - "toolDefinitionsHash": "Tool definitions hash", - "manifestHash": "Manifest hash", - "includedEntries": "Included entries", - "excludedEntries": "Excluded entries", - "entryId": "Entry ID", - "messageId": "Message ID", - "orderSeq": "Order seq", - "role": "Role", - "source": "Source", - "reason": "Reason", - "contextLength": "Context length", - "requestedMaxTokens": "Requested max tokens", - "effectiveMaxTokens": "Effective max tokens", - "reserveTokens": "Reserve tokens", - "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "empty": "Нет диагностических данных", + "emptyDesc": "Для этого сообщения нет трассировки запроса или манифеста представления", + "requestUnavailable": "Нет трассировки запроса", + "requestUnavailableDesc": "Для этого сообщения нет сохранённой трассировки запроса к провайдеру", + "manifestUnavailable": "Нет манифеста представления", + "manifestUnavailableDesc": "Для этого сообщения нет сохранённого манифеста представления Tape", + "viewId": "ID представления", + "policy": "Политика", + "policyVersion": "Версия политики", + "taskType": "Тип задачи", + "requestSeq": "№ запроса", + "latestEntryId": "ID последней записи", + "promptHash": "Хеш промпта", + "toolDefinitionsHash": "Хеш определений инструментов", + "manifestHash": "Хеш манифеста", + "includedEntries": "Включённые записи", + "excludedEntries": "Исключённые записи", + "entryId": "ID записи", + "messageId": "ID сообщения", + "orderSeq": "Порядок", + "role": "Роль", + "source": "Источник", + "reason": "Причина", + "contextLength": "Длина контекста", + "requestedMaxTokens": "Запрошенный максимум токенов", + "effectiveMaxTokens": "Фактический максимум токенов", + "reserveTokens": "Зарезервированные токены", + "toolReserveTokens": "Токены, зарезервированные для инструментов", + "estimatedPromptTokens": "Оценка токенов промпта" } diff --git a/src/renderer/src/i18n/tr-TR/traceDialog.json b/src/renderer/src/i18n/tr-TR/traceDialog.json index d86d7e137..abed5187b 100644 --- a/src/renderer/src/i18n/tr-TR/traceDialog.json +++ b/src/renderer/src/i18n/tr-TR/traceDialog.json @@ -15,38 +15,38 @@ "notImplementedDesc": "Bu sağlayıcı henüz istek önizlemesini uygulamadı", "mayNotMatch": "Not: Bu önizleme mevcut görüşme ayarlarından yeniden oluşturulmuştur ve gerçek istek parametreleriyle tam olarak eşleşmeyebilir", "tabs": { - "request": "Request", - "view": "View", - "entries": "Entries", - "budget": "Budget" + "request": "İstek", + "view": "Görünüm", + "entries": "Girdiler", + "budget": "Bütçe" }, - "empty": "No diagnostics", - "emptyDesc": "No request trace or view manifest is available for this message", - "requestUnavailable": "No request trace", - "requestUnavailableDesc": "This message has no persisted provider request trace", - "manifestUnavailable": "No view manifest", - "manifestUnavailableDesc": "This message has no persisted Tape view manifest", - "viewId": "View ID", - "policy": "Policy", - "policyVersion": "Policy version", - "taskType": "Task type", - "requestSeq": "Request #", - "latestEntryId": "Latest entry ID", - "promptHash": "Prompt hash", - "toolDefinitionsHash": "Tool definitions hash", - "manifestHash": "Manifest hash", - "includedEntries": "Included entries", - "excludedEntries": "Excluded entries", - "entryId": "Entry ID", - "messageId": "Message ID", - "orderSeq": "Order seq", - "role": "Role", - "source": "Source", - "reason": "Reason", - "contextLength": "Context length", - "requestedMaxTokens": "Requested max tokens", - "effectiveMaxTokens": "Effective max tokens", - "reserveTokens": "Reserve tokens", - "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "empty": "Tanılama yok", + "emptyDesc": "Bu ileti için istek izi veya görünüm bildirimi yok", + "requestUnavailable": "İstek izi yok", + "requestUnavailableDesc": "Bu iletinin kalıcı sağlayıcı istek izi yok", + "manifestUnavailable": "Görünüm bildirimi yok", + "manifestUnavailableDesc": "Bu iletinin kalıcı Tape görünüm bildirimi yok", + "viewId": "Görünüm Kimliği", + "policy": "İlke", + "policyVersion": "İlke sürümü", + "taskType": "Görev türü", + "requestSeq": "İstek no.", + "latestEntryId": "En son girdi kimliği", + "promptHash": "Prompt karması", + "toolDefinitionsHash": "Araç tanımları karması", + "manifestHash": "Bildirim karması", + "includedEntries": "Dahil edilen girdiler", + "excludedEntries": "Hariç tutulan girdiler", + "entryId": "Girdi kimliği", + "messageId": "İleti kimliği", + "orderSeq": "Sıra", + "role": "Rol", + "source": "Kaynak", + "reason": "Neden", + "contextLength": "Bağlam uzunluğu", + "requestedMaxTokens": "İstenen en fazla token", + "effectiveMaxTokens": "Geçerli en fazla token", + "reserveTokens": "Ayrılmış tokenlar", + "toolReserveTokens": "Araç için ayrılmış tokenlar", + "estimatedPromptTokens": "Tahmini prompt tokenları" } diff --git a/src/renderer/src/i18n/vi-VN/traceDialog.json b/src/renderer/src/i18n/vi-VN/traceDialog.json index 550859861..49fab3531 100644 --- a/src/renderer/src/i18n/vi-VN/traceDialog.json +++ b/src/renderer/src/i18n/vi-VN/traceDialog.json @@ -15,38 +15,38 @@ "notImplementedDesc": "Nhà cung cấp này chưa triển khai bản xem trước yêu cầu", "mayNotMatch": "Lưu ý: Bản xem trước này được xây dựng lại từ cài đặt cuộc trò chuyện hiện tại và có thể không khớp chính xác với các tham số yêu cầu thực tế", "tabs": { - "request": "Request", - "view": "View", - "entries": "Entries", - "budget": "Budget" + "request": "Yêu cầu", + "view": "Chế độ xem", + "entries": "Mục", + "budget": "Ngân sách" }, - "empty": "No diagnostics", - "emptyDesc": "No request trace or view manifest is available for this message", - "requestUnavailable": "No request trace", - "requestUnavailableDesc": "This message has no persisted provider request trace", - "manifestUnavailable": "No view manifest", - "manifestUnavailableDesc": "This message has no persisted Tape view manifest", - "viewId": "View ID", - "policy": "Policy", - "policyVersion": "Policy version", - "taskType": "Task type", - "requestSeq": "Request #", - "latestEntryId": "Latest entry ID", - "promptHash": "Prompt hash", - "toolDefinitionsHash": "Tool definitions hash", - "manifestHash": "Manifest hash", - "includedEntries": "Included entries", - "excludedEntries": "Excluded entries", - "entryId": "Entry ID", - "messageId": "Message ID", - "orderSeq": "Order seq", - "role": "Role", - "source": "Source", - "reason": "Reason", - "contextLength": "Context length", - "requestedMaxTokens": "Requested max tokens", - "effectiveMaxTokens": "Effective max tokens", - "reserveTokens": "Reserve tokens", - "toolReserveTokens": "Tool reserve tokens", - "estimatedPromptTokens": "Estimated prompt tokens" + "empty": "Không có chẩn đoán", + "emptyDesc": "Không có vết yêu cầu hoặc bản kê chế độ xem cho tin nhắn này", + "requestUnavailable": "Không có vết yêu cầu", + "requestUnavailableDesc": "Tin nhắn này không có vết yêu cầu nhà cung cấp đã lưu", + "manifestUnavailable": "Không có bản kê chế độ xem", + "manifestUnavailableDesc": "Tin nhắn này không có bản kê chế độ xem Tape đã lưu", + "viewId": "ID chế độ xem", + "policy": "Chính sách", + "policyVersion": "Phiên bản chính sách", + "taskType": "Loại tác vụ", + "requestSeq": "Số yêu cầu", + "latestEntryId": "ID mục mới nhất", + "promptHash": "Hash prompt", + "toolDefinitionsHash": "Hash định nghĩa công cụ", + "manifestHash": "Hash bản kê", + "includedEntries": "Mục được bao gồm", + "excludedEntries": "Mục bị loại trừ", + "entryId": "ID mục", + "messageId": "ID tin nhắn", + "orderSeq": "Thứ tự", + "role": "Vai trò", + "source": "Nguồn", + "reason": "Lý do", + "contextLength": "Độ dài ngữ cảnh", + "requestedMaxTokens": "Số token tối đa đã yêu cầu", + "effectiveMaxTokens": "Số token tối đa hiệu dụng", + "reserveTokens": "Token dự trữ", + "toolReserveTokens": "Token dự trữ cho công cụ", + "estimatedPromptTokens": "Token prompt ước tính" } diff --git a/src/renderer/src/i18n/zh-HK/traceDialog.json b/src/renderer/src/i18n/zh-HK/traceDialog.json index 4be9c57be..87ebfaf09 100644 --- a/src/renderer/src/i18n/zh-HK/traceDialog.json +++ b/src/renderer/src/i18n/zh-HK/traceDialog.json @@ -15,38 +15,38 @@ "title": "請求參數預覽", "model": "Model", "tabs": { - "request": "请求", - "view": "视图", - "entries": "条目", - "budget": "预算" + "request": "要求", + "view": "檢視", + "entries": "項目", + "budget": "預算" }, - "empty": "暂无诊断数据", - "emptyDesc": "此消息暂无请求追踪或视图清单", - "requestUnavailable": "暂无请求追踪", - "requestUnavailableDesc": "此消息暂无持久化的供应商请求追踪", - "manifestUnavailable": "暂无视图清单", - "manifestUnavailableDesc": "此消息暂无持久化的 Tape 视图清单", - "viewId": "视图 ID", + "empty": "沒有診斷資料", + "emptyDesc": "此訊息沒有可用的要求追蹤或檢視清單", + "requestUnavailable": "沒有要求追蹤", + "requestUnavailableDesc": "此訊息沒有已儲存的供應商要求追蹤", + "manifestUnavailable": "沒有檢視清單", + "manifestUnavailableDesc": "此訊息沒有已儲存的 Tape 檢視清單", + "viewId": "檢視 ID", "policy": "策略", "policyVersion": "策略版本", - "taskType": "任务类型", - "requestSeq": "请求序号", - "latestEntryId": "最新条目 ID", - "promptHash": "Prompt Hash", - "toolDefinitionsHash": "工具定义 Hash", - "manifestHash": "清单 Hash", - "includedEntries": "包含条目", - "excludedEntries": "排除条目", - "entryId": "条目 ID", - "messageId": "消息 ID", - "orderSeq": "顺序号", + "taskType": "工作類型", + "requestSeq": "要求編號", + "latestEntryId": "最新項目 ID", + "promptHash": "提示詞雜湊", + "toolDefinitionsHash": "工具定義雜湊", + "manifestHash": "清單雜湊", + "includedEntries": "已包含項目", + "excludedEntries": "已排除項目", + "entryId": "項目 ID", + "messageId": "訊息 ID", + "orderSeq": "排序序號", "role": "角色", - "source": "来源", + "source": "來源", "reason": "原因", - "contextLength": "上下文长度", - "requestedMaxTokens": "请求 Max Tokens", - "effectiveMaxTokens": "生效 Max Tokens", - "reserveTokens": "预留 Tokens", - "toolReserveTokens": "工具预留 Tokens", - "estimatedPromptTokens": "估算 Prompt Tokens" + "contextLength": "上下文長度", + "requestedMaxTokens": "要求的最大 Tokens", + "effectiveMaxTokens": "實際最大 Tokens", + "reserveTokens": "預留 Tokens", + "toolReserveTokens": "工具預留 Tokens", + "estimatedPromptTokens": "估算提示詞 Tokens" } diff --git a/src/renderer/src/i18n/zh-TW/traceDialog.json b/src/renderer/src/i18n/zh-TW/traceDialog.json index e21efb193..6197aa0a4 100644 --- a/src/renderer/src/i18n/zh-TW/traceDialog.json +++ b/src/renderer/src/i18n/zh-TW/traceDialog.json @@ -15,38 +15,38 @@ "title": "請求參數預覽", "model": "Model", "tabs": { - "request": "请求", - "view": "视图", - "entries": "条目", - "budget": "预算" + "request": "請求", + "view": "檢視", + "entries": "項目", + "budget": "預算" }, - "empty": "暂无诊断数据", - "emptyDesc": "此消息暂无请求追踪或视图清单", - "requestUnavailable": "暂无请求追踪", - "requestUnavailableDesc": "此消息暂无持久化的供应商请求追踪", - "manifestUnavailable": "暂无视图清单", - "manifestUnavailableDesc": "此消息暂无持久化的 Tape 视图清单", - "viewId": "视图 ID", + "empty": "沒有診斷資料", + "emptyDesc": "此訊息沒有可用的請求追蹤或檢視清單", + "requestUnavailable": "沒有請求追蹤", + "requestUnavailableDesc": "此訊息沒有已儲存的供應商請求追蹤", + "manifestUnavailable": "沒有檢視清單", + "manifestUnavailableDesc": "此訊息沒有已儲存的 Tape 檢視清單", + "viewId": "檢視 ID", "policy": "策略", "policyVersion": "策略版本", - "taskType": "任务类型", - "requestSeq": "请求序号", - "latestEntryId": "最新条目 ID", - "promptHash": "Prompt Hash", - "toolDefinitionsHash": "工具定义 Hash", - "manifestHash": "清单 Hash", - "includedEntries": "包含条目", - "excludedEntries": "排除条目", - "entryId": "条目 ID", - "messageId": "消息 ID", - "orderSeq": "顺序号", + "taskType": "任務類型", + "requestSeq": "請求序號", + "latestEntryId": "最新項目 ID", + "promptHash": "提示詞雜湊", + "toolDefinitionsHash": "工具定義雜湊", + "manifestHash": "清單雜湊", + "includedEntries": "包含的項目", + "excludedEntries": "排除的項目", + "entryId": "項目 ID", + "messageId": "訊息 ID", + "orderSeq": "排序序號", "role": "角色", - "source": "来源", + "source": "來源", "reason": "原因", - "contextLength": "上下文长度", - "requestedMaxTokens": "请求 Max Tokens", - "effectiveMaxTokens": "生效 Max Tokens", - "reserveTokens": "预留 Tokens", - "toolReserveTokens": "工具预留 Tokens", - "estimatedPromptTokens": "估算 Prompt Tokens" + "contextLength": "脈絡長度", + "requestedMaxTokens": "請求的最大 Tokens", + "effectiveMaxTokens": "實際最大 Tokens", + "reserveTokens": "保留 Tokens", + "toolReserveTokens": "工具保留 Tokens", + "estimatedPromptTokens": "預估提示詞 Tokens" } diff --git a/src/shared/contracts/routes.ts b/src/shared/contracts/routes.ts index 656169039..07eb8ed21 100644 --- a/src/shared/contracts/routes.ts +++ b/src/shared/contracts/routes.ts @@ -456,7 +456,7 @@ export * from './routes/upgrade.routes' export * from './routes/window.routes' export * from './routes/workspace.routes' -export const DEEPCHAT_ROUTE_CATALOG: Record = { +export const DEEPCHAT_ROUTE_CATALOG = { [acpTerminalInputRoute.name]: acpTerminalInputRoute, [acpTerminalKillRoute.name]: acpTerminalKillRoute, [shortcutRegisterRoute.name]: shortcutRegisterRoute, @@ -827,7 +827,7 @@ export const DEEPCHAT_ROUTE_CATALOG: Record = { [dialogErrorRoute.name]: dialogErrorRoute, [toolsListDefinitionsRoute.name]: toolsListDefinitionsRoute, [systemOpenSettingsRoute.name]: systemOpenSettingsRoute -} +} satisfies Record export type DeepchatRouteCatalog = typeof DEEPCHAT_ROUTE_CATALOG export type DeepchatRouteName = keyof DeepchatRouteCatalog diff --git a/test/main/presenter/agentRuntimePresenter/contextBuilder.test.ts b/test/main/presenter/agentRuntimePresenter/contextBuilder.test.ts index bb2df629c..e856f4ce4 100644 --- a/test/main/presenter/agentRuntimePresenter/contextBuilder.test.ts +++ b/test/main/presenter/agentRuntimePresenter/contextBuilder.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi } from 'vitest' import { buildContext, buildResumeContext, + buildResumeContextWithMetadata, fitMessagesToContextWindow, truncateContext } from '@/presenter/agentRuntimePresenter/contextBuilder' @@ -1162,6 +1163,62 @@ describe('buildResumeContext', () => { ]) }) + it('does not duplicate empty formatted resume records as out of budget exclusions', () => { + const emptyRecord = { + id: 'empty-user', + sessionId: 's1', + orderSeq: 1, + role: 'user' as const, + content: JSON.stringify({ text: '', files: [], links: [], search: false, think: false }), + status: 'sent' as const, + isContextEdge: 0, + metadata: '{}', + createdAt: Date.now(), + updatedAt: Date.now() + } + const messages = [ + emptyRecord, + makeUserRecord(2, 'recent user'), + { + id: 'resume-target', + sessionId: 's1', + orderSeq: 3, + role: 'assistant' as const, + content: JSON.stringify([ + { type: 'content', content: 'partial answer', status: 'success', timestamp: Date.now() } + ]), + status: 'pending' as const, + isContextEdge: 0, + metadata: '{}', + createdAt: Date.now(), + updatedAt: Date.now() + } + ] + const store = createMockMessageStore(messages) + + const result = buildResumeContextWithMetadata( + 's1', + 'resume-target', + '', + 10000, + 4096, + store, + false, + { + fallbackProtectedTurnCount: 1 + } + ) + + expect( + result.metadata.excludedRecords.filter((item) => item.record.id === 'empty-user') + ).toEqual([ + { + record: emptyRecord, + reason: 'empty_after_formatting' + } + ]) + }) + it('includes prior assistant error records when building resume context', () => { const messages = [ makeUserRecord(1, 'previous user'), diff --git a/test/renderer/components/trace/TraceDialog.test.ts b/test/renderer/components/trace/TraceDialog.test.ts index a4d04d982..202332867 100644 --- a/test/renderer/components/trace/TraceDialog.test.ts +++ b/test/renderer/components/trace/TraceDialog.test.ts @@ -101,6 +101,51 @@ vi.mock('vue-i18n', () => ({ import TraceDialog from '@/components/trace/TraceDialog.vue' +const makeManifestRecord = (requestSeq: number, viewId: string) => ({ + sessionId: 's1', + messageId: 'm1', + requestSeq, + entryId: requestSeq, + createdAt: 2000, + manifest: { + schemaVersion: 1, + viewId, + sessionId: 's1', + messageId: 'm1', + requestSeq, + taskType: 'chat', + policy: 'legacy_context_v1', + policyVersion: 1, + contextBuilderVersion: 'legacy-v1', + latestEntryId: 8, + anchorEntryIds: [1], + included: [], + excluded: [], + tokenBudget: { + contextLength: 1000, + requestedMaxTokens: 100, + effectiveMaxTokens: 100, + reserveTokens: 100, + toolReserveTokens: 0, + estimatedPromptTokens: 12 + }, + hashes: { + promptHash: 'prompt_hash', + toolDefinitionsHash: 'tool_hash', + manifestHash: 'manifest_hash' + }, + meta: { + providerId: 'openai', + modelId: 'gpt-4o', + summaryCursorOrderSeq: 1, + supportsVision: true, + supportsAudioInput: false, + traceDebugEnabled: false + }, + assembledAt: 2000 + } +}) + const mountDialog = () => mount(TraceDialog, { props: { @@ -170,61 +215,7 @@ describe('TraceDialog', () => { it('shows view manifest diagnostics when request traces are empty', async () => { listMessageTraceDiagnosticsMock.mockResolvedValue({ traces: [], - manifests: [ - { - sessionId: 's1', - messageId: 'm1', - requestSeq: 1, - entryId: 9, - createdAt: 2000, - manifest: { - schemaVersion: 1, - viewId: 'view_abc', - sessionId: 's1', - messageId: 'm1', - requestSeq: 1, - taskType: 'chat', - policy: 'legacy_context_v1', - policyVersion: 1, - contextBuilderVersion: 'legacy-v1', - latestEntryId: 8, - anchorEntryIds: [1], - included: [ - { - entryId: 2, - messageId: 'u1', - orderSeq: 1, - role: 'user', - source: 'tape', - reason: 'selected_history' - } - ], - excluded: [], - tokenBudget: { - contextLength: 1000, - requestedMaxTokens: 100, - effectiveMaxTokens: 100, - reserveTokens: 100, - toolReserveTokens: 0, - estimatedPromptTokens: 12 - }, - hashes: { - promptHash: 'prompt_hash', - toolDefinitionsHash: 'tool_hash', - manifestHash: 'manifest_hash' - }, - meta: { - providerId: 'openai', - modelId: 'gpt-4o', - summaryCursorOrderSeq: 1, - supportsVision: true, - supportsAudioInput: false, - traceDebugEnabled: false - }, - assembledAt: 2000 - } - } - ] + manifests: [makeManifestRecord(1, 'view_abc')] }) const wrapper = mountDialog() @@ -238,4 +229,51 @@ describe('TraceDialog', () => { expect(wrapper.text()).toContain('traceDialog.policyVersion') expect(wrapper.text()).toContain('1') }) + + it('does not fall back to a different request when selected manifest has no trace', async () => { + listMessageTraceDiagnosticsMock.mockResolvedValue({ + traces: [ + { + id: 't2', + messageId: 'm1', + sessionId: 's1', + providerId: 'openai', + modelId: 'gpt-4o', + requestSeq: 2, + endpoint: 'https://api.example.com/second', + headersJson: '{"x":"2"}', + bodyJson: '{"b":2}', + truncated: false, + createdAt: 2000 + } + ], + manifests: [makeManifestRecord(1, 'view_only_manifest')] + }) + + const wrapper = mountDialog() + + await wrapper.setProps({ messageId: 'm1' }) + await flushPromises() + + expect(wrapper.text()).toContain('https://api.example.com/second') + + const manifestOnlyButton = wrapper.findAll('button').find((btn) => btn.text().trim() === '#1') + expect(manifestOnlyButton).toBeDefined() + + await manifestOnlyButton!.trigger('click') + await flushPromises() + + expect(wrapper.text()).toContain('traceDialog.requestUnavailable') + expect(wrapper.text()).not.toContain('https://api.example.com/second') + + const viewTab = wrapper + .findAll('button') + .find((btn) => btn.text().trim() === 'traceDialog.tabs.view') + expect(viewTab).toBeDefined() + + await viewTab!.trigger('click') + await flushPromises() + + expect(wrapper.text()).toContain('view_only_manifest') + }) }) From 99b45c18ae5b3cc071de335a61f64d4efe3201fd Mon Sep 17 00:00:00 2001 From: zerob13 Date: Mon, 15 Jun 2026 19:10:16 +0800 Subject: [PATCH 4/6] fix(tape): address view manifest review --- .../agentRuntimePresenter/tapeService.ts | 164 ++++++++++++++++-- .../agentRuntimePresenter/tapeViewManifest.ts | 7 +- src/renderer/api/SessionClient.ts | 5 +- .../contracts/routes/sessions.routes.ts | 3 +- .../agentRuntimePresenter.test.ts | 28 +++ .../agentRuntimePresenter/tapeService.test.ts | 46 +++++ .../tapeViewManifest.test.ts | 15 ++ 7 files changed, 249 insertions(+), 19 deletions(-) diff --git a/src/main/presenter/agentRuntimePresenter/tapeService.ts b/src/main/presenter/agentRuntimePresenter/tapeService.ts index b7c157b3e..f051ce4d5 100644 --- a/src/main/presenter/agentRuntimePresenter/tapeService.ts +++ b/src/main/presenter/agentRuntimePresenter/tapeService.ts @@ -201,6 +201,146 @@ function collectEntryIds(values: Array): number[] { ) } +const VIEW_POLICIES = new Set([ + 'legacy_context_v1', + 'legacy_context_shadow', + 'resume_shadow', + 'tool_loop_shadow', + 'context_pressure_recovery_shadow' +]) + +const VIEW_ENTRY_REASONS = new Set([ + 'system_prompt', + 'selected_history', + 'new_user_input', + 'resume_target', + 'tool_loop_message' +]) + +const VIEW_EXCLUDED_REASONS = new Set([ + 'before_summary_cursor', + 'compaction_indicator', + 'pending_not_context_history', + 'out_of_budget', + 'empty_after_formatting', + 'superseded', + 'retracted' +]) + +function isRecordObject(value: unknown): value is Record { + return Boolean(value && typeof value === 'object' && !Array.isArray(value)) +} + +function isNullableString(value: unknown): value is string | null { + return value === null || typeof value === 'string' +} + +function isNullableNumber(value: unknown): value is number | null { + return value === null || typeof value === 'number' +} + +function isViewEntryRef(value: unknown): value is DeepChatTapeViewManifest['included'][number] { + if (!isRecordObject(value)) { + return false + } + + return ( + isNullableNumber(value.entryId) && + isNullableString(value.messageId) && + isNullableNumber(value.orderSeq) && + (value.role === 'system' || + value.role === 'user' || + value.role === 'assistant' || + value.role === 'tool' || + value.role === null) && + (value.source === 'tape' || value.source === 'synthetic') && + typeof value.reason === 'string' && + VIEW_ENTRY_REASONS.has(value.reason) + ) +} + +function isViewExcludedRef(value: unknown): value is DeepChatTapeViewManifest['excluded'][number] { + if (!isRecordObject(value)) { + return false + } + + return ( + isNullableNumber(value.entryId) && + isNullableString(value.messageId) && + isNullableNumber(value.orderSeq) && + typeof value.reason === 'string' && + VIEW_EXCLUDED_REASONS.has(value.reason) + ) +} + +function hasNumberFields(value: unknown, fields: string[]): value is Record { + if (!isRecordObject(value)) { + return false + } + + return fields.every((field) => typeof value[field] === 'number') +} + +function hasStringFields(value: unknown, fields: string[]): value is Record { + if (!isRecordObject(value)) { + return false + } + + return fields.every((field) => typeof value[field] === 'string') +} + +function isViewManifestMeta(value: unknown): value is DeepChatTapeViewManifest['meta'] { + if (!isRecordObject(value)) { + return false + } + + return ( + typeof value.providerId === 'string' && + typeof value.modelId === 'string' && + typeof value.summaryCursorOrderSeq === 'number' && + typeof value.supportsVision === 'boolean' && + typeof value.supportsAudioInput === 'boolean' && + typeof value.traceDebugEnabled === 'boolean' + ) +} + +function isViewManifest(value: unknown, sessionId: string): value is DeepChatTapeViewManifest { + if (!isRecordObject(value)) { + return false + } + + return ( + value.schemaVersion === 1 && + value.sessionId === sessionId && + typeof value.viewId === 'string' && + typeof value.messageId === 'string' && + typeof value.requestSeq === 'number' && + (value.taskType === 'chat' || value.taskType === 'resume' || value.taskType === 'tool_loop') && + typeof value.policy === 'string' && + VIEW_POLICIES.has(value.policy) && + (typeof value.policyVersion === 'number' || value.policyVersion === null) && + value.contextBuilderVersion === 'legacy-v1' && + typeof value.latestEntryId === 'number' && + Array.isArray(value.anchorEntryIds) && + value.anchorEntryIds.every((entryId) => typeof entryId === 'number') && + Array.isArray(value.included) && + value.included.every(isViewEntryRef) && + Array.isArray(value.excluded) && + value.excluded.every(isViewExcludedRef) && + hasNumberFields(value.tokenBudget, [ + 'contextLength', + 'requestedMaxTokens', + 'effectiveMaxTokens', + 'reserveTokens', + 'toolReserveTokens', + 'estimatedPromptTokens' + ]) && + hasStringFields(value.hashes, ['promptHash', 'toolDefinitionsHash', 'manifestHash']) && + isViewManifestMeta(value.meta) && + typeof value.assembledAt === 'number' + ) +} + function withReplaySliceHash( slice: Omit & { hashes: Omit & { sliceHash: '' } @@ -283,7 +423,12 @@ export class DeepChatTapeService { } appendMessageRecord(record: ChatMessageRecord): number { - return appendMessageRecordToTape(this.table, record, 'live') + const table = this.table + if (!table) { + throw new Error('Tape table is not available.') + } + + return appendMessageRecordToTape(table, record, 'live') } getMessageRecords(sessionId: string): ChatMessageRecord[] { @@ -800,26 +945,17 @@ export class DeepChatTapeService { data && typeof data === 'object' && !Array.isArray(data) ? (data as Record).manifest : undefined - if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { - return null - } - const candidate = manifest as DeepChatTapeViewManifest - if ( - candidate.schemaVersion !== 1 || - candidate.sessionId !== row.session_id || - typeof candidate.messageId !== 'string' || - typeof candidate.requestSeq !== 'number' - ) { + if (!isViewManifest(manifest, row.session_id)) { return null } return { sessionId: row.session_id, - messageId: candidate.messageId, - requestSeq: candidate.requestSeq, + messageId: manifest.messageId, + requestSeq: manifest.requestSeq, entryId: row.entry_id, createdAt: row.created_at, - manifest: candidate + manifest } } diff --git a/src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts b/src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts index 6f709d18f..9e798363c 100644 --- a/src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts +++ b/src/main/presenter/agentRuntimePresenter/tapeViewManifest.ts @@ -257,6 +257,11 @@ export function buildSyntheticRequestRefs(messages: ChatMessage[]): DeepChatTape orderSeq: null, role: message.role, source: 'synthetic', - reason: message.role === 'tool' ? 'tool_loop_message' : 'selected_history' + reason: + message.role === 'system' + ? 'system_prompt' + : message.role === 'tool' + ? 'tool_loop_message' + : 'selected_history' })) } diff --git a/src/renderer/api/SessionClient.ts b/src/renderer/api/SessionClient.ts index 6d55aa1e4..9102dfa8c 100644 --- a/src/renderer/api/SessionClient.ts +++ b/src/renderer/api/SessionClient.ts @@ -65,7 +65,6 @@ import { sessionsUpdateQueuedInputRoute } from '@shared/contracts/routes' import type { CreateSessionInput, SendMessageInput } from '@shared/types/agent-interface' -import type { DeepChatTapeViewManifestRecord } from '@shared/types/tape-view-manifest' import type { DeepChatTapeReplayExportOptions, DeepChatTapeReplaySlice @@ -249,14 +248,14 @@ export function createSessionClient(bridge: DeepchatBridge = getDeepchatBridge() const manifests = Array.isArray(result.manifests) ? result.manifests : [] return { traces: result.traces, - manifests: manifests as DeepChatTapeViewManifestRecord[] + manifests } } async function listMessageViewManifests(messageId: string) { const result = await bridge.invoke(sessionsListMessageTracesRoute.name, { messageId }) const manifests = Array.isArray(result.manifests) ? result.manifests : [] - return manifests as DeepChatTapeViewManifestRecord[] + return manifests } async function exportMessageTapeReplaySlice( diff --git a/src/shared/contracts/routes/sessions.routes.ts b/src/shared/contracts/routes/sessions.routes.ts index 428c7d5d2..d1bd11759 100644 --- a/src/shared/contracts/routes/sessions.routes.ts +++ b/src/shared/contracts/routes/sessions.routes.ts @@ -9,6 +9,7 @@ import type { } from '@shared/types/agent-interface' import type { HistorySearchHit } from '@shared/types/presenters/agent-session.presenter' import type { DeepChatTapeReplaySlice } from '@shared/types/tape-replay' +import type { DeepChatTapeViewManifestRecord } from '@shared/types/tape-view-manifest' import { SessionListItemSchema, SessionPageCursorSchema, @@ -29,7 +30,7 @@ import { AcpConfigStateSchema, UsageDashboardDataSchema } from '../domainSchemas const PendingSessionInputRecordSchema = z.custom() const MessageTraceRecordSchema = z.custom() -const DeepChatTapeViewManifestRecordSchema = z.record(z.unknown()) +const DeepChatTapeViewManifestRecordSchema = z.custom() const DeepChatTapeReplaySliceSchema = z.custom().nullable() const HistorySearchHitSchema = z.custom() const SearchResultSchema = z.custom() diff --git a/test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts b/test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts index 212895af2..04323bcac 100644 --- a/test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts +++ b/test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts @@ -1556,6 +1556,34 @@ describe('AgentRuntimePresenter', () => { expect(manifests[1].hashes.toolDefinitionsHash).toHaveLength(64) }) + it('continues provider requests when view manifest persistence fails', async () => { + await agent.initSession('s1', { providerId: 'openai', modelId: 'gpt-4' }) + await agent.processMessage('s1', 'Hello') + + const callArgs = (processStream as ReturnType).mock.calls[0][0] + const providerCoreStream = llmProvider.getProviderInstance.mock.results[0].value.coreStream + const loggerWarnMock = vi.mocked(logger.warn) + loggerWarnMock.mockClear() + sqlitePresenter.deepchatTapeEntriesTable.appendEvent.mockImplementation(() => { + throw new Error('manifest write failed') + }) + + for await (const _event of callArgs.coreStream( + callArgs.messages, + callArgs.modelId, + callArgs.modelConfig, + callArgs.temperature, + callArgs.maxTokens, + callArgs.tools + )) { + } + + expect(providerCoreStream).toHaveBeenCalledTimes(1) + expect(loggerWarnMock).toHaveBeenCalledWith( + expect.stringContaining('Failed to persist tape view manifest') + ) + }) + it('emits and clears an ephemeral rate-limit message while waiting for the provider gate', async () => { llmProvider.executeWithRateLimit.mockImplementation( async (_providerId: string, options?: { onQueued?: (snapshot: any) => void }) => { diff --git a/test/main/presenter/agentRuntimePresenter/tapeService.test.ts b/test/main/presenter/agentRuntimePresenter/tapeService.test.ts index d66c114e4..b74b3b824 100644 --- a/test/main/presenter/agentRuntimePresenter/tapeService.test.ts +++ b/test/main/presenter/agentRuntimePresenter/tapeService.test.ts @@ -544,6 +544,43 @@ describe('DeepChatTapeService', () => { ]) }) + it('filters malformed view manifest rows when listing by message', () => { + const { table } = createTapeTableMock() + const service = new DeepChatTapeService({ + deepchatTapeEntriesTable: table, + deepchatSessionsTable: { getSummaryState: vi.fn().mockReturnValue(null) } + } as any) + + table.appendEvent({ + sessionId: 's1', + name: 'view/assembled', + source: { + type: 'runtime_event', + id: 'a1', + seq: 1 + }, + data: { + manifest: { + schemaVersion: 1, + sessionId: 's1', + messageId: 'a1', + requestSeq: 1, + included: 'not-an-array' + } + } + }) + + expect(service.listViewManifestsByMessage('s1', 'a1')).toEqual([]) + }) + + it('throws a clear error when appending live messages without a tape table', () => { + const service = new DeepChatTapeService({} as any) + + expect(() => service.appendMessageRecord(createRecord({ id: 'u1' }))).toThrow( + 'Tape table is not available.' + ) + }) + it('exports replay slices with metadata-only payloads by default', () => { const { table } = createTapeTableMock() const service = createTapeService(table, [createTraceRow()]) @@ -704,6 +741,15 @@ describe('DeepChatTapeService', () => { expect(service.exportReplaySlice('s1', 'a1')).toBeNull() }) + it('rejects non-positive replay request sequences', () => { + const { table } = createTapeTableMock() + const service = createTapeService(table, [createTraceRow()]) + + expect(() => service.exportReplaySlice('s1', 'a1', { requestSeq: 0 })).toThrow( + 'requestSeq must be a positive integer.' + ) + }) + it('keeps pending message records for resume but hides pending tool facts from search', () => { const { table } = createTapeTableMock() const pendingBlocks = [ diff --git a/test/main/presenter/agentRuntimePresenter/tapeViewManifest.test.ts b/test/main/presenter/agentRuntimePresenter/tapeViewManifest.test.ts index c972a5913..5b430585a 100644 --- a/test/main/presenter/agentRuntimePresenter/tapeViewManifest.test.ts +++ b/test/main/presenter/agentRuntimePresenter/tapeViewManifest.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import type { ChatMessageRecord } from '@shared/types/agent-interface' import { buildIncludedRefs, + buildSyntheticRequestRefs, createTapeViewManifest, hashJson, resolveTapeViewManifestPolicy @@ -156,4 +157,18 @@ describe('tapeViewManifest', () => { policyVersion: null }) }) + + it('builds synthetic request refs with role-specific reasons', () => { + expect( + buildSyntheticRequestRefs([ + { role: 'system', content: 'system' }, + { role: 'user', content: 'question' }, + { role: 'tool', content: 'tool output' } + ]) + ).toMatchObject([ + { role: 'system', reason: 'system_prompt', source: 'synthetic' }, + { role: 'user', reason: 'selected_history', source: 'synthetic' }, + { role: 'tool', reason: 'tool_loop_message', source: 'synthetic' } + ]) + }) }) From c477e1305cf15d654b831e15db3588d6e357be76 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Mon, 15 Jun 2026 21:55:58 +0800 Subject: [PATCH 5/6] fix(settings): clone save payloads Serialize renderer config payloads before IPC invokes and restore MCP auto-approve checkbox rendering. --- .../plan.md | 15 ++ .../spec.md | 36 +++++ .../tasks.md | 6 + .../issues/settings-save-clone-errors/plan.md | 17 +++ .../issues/settings-save-clone-errors/spec.md | 19 +++ .../settings-save-clone-errors/tasks.md | 8 + src/renderer/api/ConfigClient.ts | 41 +++-- .../components/DeepChatAgentsSettings.vue | 26 ++-- .../components/mcp-config/mcpServerForm.vue | 1 + test/renderer/api/clients.test.ts | 141 ++++++++++++++++++ .../components/DeepChatAgentsSettings.test.ts | 24 ++- .../renderer/components/McpServerForm.test.ts | 121 +++++++++++++++ 12 files changed, 431 insertions(+), 24 deletions(-) create mode 100644 docs/issues/mcp-server-form-auto-approve-controls/plan.md create mode 100644 docs/issues/mcp-server-form-auto-approve-controls/spec.md create mode 100644 docs/issues/mcp-server-form-auto-approve-controls/tasks.md create mode 100644 docs/issues/settings-save-clone-errors/plan.md create mode 100644 docs/issues/settings-save-clone-errors/spec.md create mode 100644 docs/issues/settings-save-clone-errors/tasks.md create mode 100644 test/renderer/components/McpServerForm.test.ts diff --git a/docs/issues/mcp-server-form-auto-approve-controls/plan.md b/docs/issues/mcp-server-form-auto-approve-controls/plan.md new file mode 100644 index 000000000..9702e007b --- /dev/null +++ b/docs/issues/mcp-server-form-auto-approve-controls/plan.md @@ -0,0 +1,15 @@ +# MCP Server Form Auto Approve Controls Plan + +## Approach + +Restore the existing checkbox component binding for the MCP server form auto-approve options. +Keep the submitted `MCPServerConfig.autoApprove` shape unchanged. + +## Implementation + +- Import the shared checkbox component used by the form template. +- Verify edit-mode initial values and submit behavior for read/write permissions. + +## Verification + +- `pnpm vitest --config vitest.config.renderer.ts test/renderer/components/McpServerForm.test.ts` diff --git a/docs/issues/mcp-server-form-auto-approve-controls/spec.md b/docs/issues/mcp-server-form-auto-approve-controls/spec.md new file mode 100644 index 000000000..6b19b10ac --- /dev/null +++ b/docs/issues/mcp-server-form-auto-approve-controls/spec.md @@ -0,0 +1,36 @@ +# MCP Server Form Auto Approve Controls Spec + +## Goal + +Restore editable auto-approve controls in the MCP server add/edit form. + +## Requirements + +- The MCP add server form displays interactive controls for All, Read, and Write auto-approve options. +- The MCP edit server form displays the same controls and initializes them from `initialConfig.autoApprove`. +- Submitting the form persists the selected values through `MCPServerConfig.autoApprove`. +- Existing server fields, route contracts, and store behavior remain unchanged. + +## Layout + +Before: + +```text +Auto Approve + All + Read + Write +``` + +After: + +```text +Auto Approve + [ ] All + [ ] Read + [ ] Write +``` + +## Compatibility + +MCP config keys and saved `autoApprove` values remain unchanged. diff --git a/docs/issues/mcp-server-form-auto-approve-controls/tasks.md b/docs/issues/mcp-server-form-auto-approve-controls/tasks.md new file mode 100644 index 000000000..37a2f2a59 --- /dev/null +++ b/docs/issues/mcp-server-form-auto-approve-controls/tasks.md @@ -0,0 +1,6 @@ +# MCP Server Form Auto Approve Controls Tasks + +- [x] Restore the checkbox component import. +- [x] Add a renderer component test for editable auto-approve controls. +- [x] Assert selected read/write permissions are submitted through `autoApprove`. +- [x] Run targeted renderer test. diff --git a/docs/issues/settings-save-clone-errors/plan.md b/docs/issues/settings-save-clone-errors/plan.md new file mode 100644 index 000000000..0abcc4159 --- /dev/null +++ b/docs/issues/settings-save-clone-errors/plan.md @@ -0,0 +1,17 @@ +# Settings Save Clone Errors Plan + +## Approach + +Normalize renderer-owned settings payloads into plain objects before invoking config routes. +Keep route names, persisted config keys, and presenter contracts unchanged. + +## Implementation + +- Add a small recursive serializer in the renderer config client for arrays, objects, and dates. +- Apply the serializer to settings save paths that can receive Vue reactive proxies. +- Normalize DeepChat Agent model selections before building create/update payloads. +- Cover serialized bridge payloads with structured clone assertions. + +## Verification + +- `pnpm vitest --config vitest.config.renderer.ts test/renderer/api/clients.test.ts test/renderer/components/DeepChatAgentsSettings.test.ts` diff --git a/docs/issues/settings-save-clone-errors/spec.md b/docs/issues/settings-save-clone-errors/spec.md new file mode 100644 index 000000000..0e3cb459c --- /dev/null +++ b/docs/issues/settings-save-clone-errors/spec.md @@ -0,0 +1,19 @@ +# Settings Save Clone Errors Spec + +## Goal + +Fix renderer IPC clone errors when settings save paths receive reactive objects. + +## Requirements + +- Saving an existing DeepChat Agent sends a structured-cloneable payload to the typed route bridge. +- Creating a DeepChat Agent uses the same structured-cloneable payload shape as updating. +- Adding, updating, and replacing custom prompts send structured-cloneable payloads. +- Adding, updating, and replacing system prompts send structured-cloneable payloads. +- Saving shortcut keys sends a structured-cloneable payload. +- Existing saved values and route contracts remain unchanged. +- Tests cover the renderer API client payloads with structured clone validation. + +## Compatibility + +Settings route names, presenter contracts, and persisted config keys remain unchanged. diff --git a/docs/issues/settings-save-clone-errors/tasks.md b/docs/issues/settings-save-clone-errors/tasks.md new file mode 100644 index 000000000..91d2319e5 --- /dev/null +++ b/docs/issues/settings-save-clone-errors/tasks.md @@ -0,0 +1,8 @@ +# Settings Save Clone Errors Tasks + +- [x] Identify settings save routes that can receive reactive objects. +- [x] Serialize shortcut key, custom prompt, system prompt, and DeepChat Agent payloads. +- [x] Normalize DeepChat Agent advanced model selections to plain route values. +- [x] Add renderer API client structured clone coverage. +- [x] Add DeepChat Agent settings save coverage for cloneable model selections. +- [x] Run targeted renderer tests. diff --git a/src/renderer/api/ConfigClient.ts b/src/renderer/api/ConfigClient.ts index 278ea154c..293a6445e 100644 --- a/src/renderer/api/ConfigClient.ts +++ b/src/renderer/api/ConfigClient.ts @@ -161,6 +161,27 @@ function toPlainKnowledgeConfigs(configs: BuiltinKnowledgeConfig[]): BuiltinKnow }) } +function toPlainIpcValue(value: T): T { + if (value === null || typeof value !== 'object') { + return value + } + + if (value instanceof Date) { + return new Date(value.getTime()) as T + } + + if (Array.isArray(value)) { + return value.map((item) => toPlainIpcValue(item)) as T + } + + const plain: Record = {} + for (const [key, nestedValue] of Object.entries(value as Record)) { + plain[key] = toPlainIpcValue(nestedValue) + } + + return plain as T +} + export function createConfigClient(bridge: DeepchatBridge = getDeepchatBridge()) { const settingsClient = createSettingsClient(bridge) @@ -321,7 +342,9 @@ export function createConfigClient(bridge: DeepchatBridge = getDeepchatBridge()) } async function setShortcutKey(shortcuts: ShortcutKeySetting) { - return await bridge.invoke(configSetShortcutKeysRoute.name, { shortcuts }) + return await bridge.invoke(configSetShortcutKeysRoute.name, { + shortcuts: toPlainIpcValue(shortcuts) + }) } async function resetShortcutKeys() { @@ -335,20 +358,20 @@ export function createConfigClient(bridge: DeepchatBridge = getDeepchatBridge()) async function setCustomPrompts(prompts: Prompt[]) { return await bridge.invoke(configSetCustomPromptsRoute.name, { - prompts: prompts as any + prompts: toPlainIpcValue(prompts) as any }) } async function addCustomPrompt(prompt: Prompt) { return await bridge.invoke(configAddCustomPromptRoute.name, { - prompt: prompt as any + prompt: toPlainIpcValue(prompt) as any }) } async function updateCustomPrompt(promptId: string, updates: Partial) { return await bridge.invoke(configUpdateCustomPromptRoute.name, { promptId, - updates: updates as any + updates: toPlainIpcValue(updates) as any }) } @@ -385,20 +408,20 @@ export function createConfigClient(bridge: DeepchatBridge = getDeepchatBridge()) async function setSystemPrompts(prompts: SystemPrompt[]) { return await bridge.invoke(configSetSystemPromptsRoute.name, { - prompts: prompts as any + prompts: toPlainIpcValue(prompts) as any }) } async function addSystemPrompt(prompt: SystemPrompt) { return await bridge.invoke(configAddSystemPromptRoute.name, { - prompt: prompt as any + prompt: toPlainIpcValue(prompt) as any }) } async function updateSystemPrompt(promptId: string, updates: Partial) { return await bridge.invoke(configUpdateSystemPromptRoute.name, { promptId, - updates: updates as any + updates: toPlainIpcValue(updates) as any }) } @@ -495,7 +518,7 @@ export function createConfigClient(bridge: DeepchatBridge = getDeepchatBridge()) async function createDeepChatAgent(input: CreateDeepChatAgentInput): Promise { const result = await bridge.invoke( configCreateDeepChatAgentRoute.name, - input as DeepchatRouteInput + toPlainIpcValue(input) as DeepchatRouteInput ) return result.agent } @@ -506,7 +529,7 @@ export function createConfigClient(bridge: DeepchatBridge = getDeepchatBridge()) ): Promise { const result = await bridge.invoke(configUpdateDeepChatAgentRoute.name, { agentId, - updates + updates: toPlainIpcValue(updates) } as DeepchatRouteInput) return result.agent } diff --git a/src/renderer/settings/components/DeepChatAgentsSettings.vue b/src/renderer/settings/components/DeepChatAgentsSettings.vue index ea52a567f..652c886b1 100644 --- a/src/renderer/settings/components/DeepChatAgentsSettings.vue +++ b/src/renderer/settings/components/DeepChatAgentsSettings.vue @@ -673,6 +673,8 @@ import type { Agent, AgentAvatar as AgentAvatarValue, AgentTransferImpact, + CreateDeepChatAgentInput, + DeepChatAgentModelSelection, DeepChatSubagentSlot, PermissionMode, Project @@ -1061,6 +1063,15 @@ const normalizePath = (value: string | null | undefined) => { const normalized = value?.trim() return normalized ? normalized : null } +const buildModelSelection = ( + selection: EditableModel | null | undefined +): DeepChatAgentModelSelection | null => + selection + ? { + providerId: selection.providerId, + modelId: selection.modelId + } + : null const normalizeNumericInput = ( value: EditableNumberValue | null | undefined, options: { fallback: number; min: number; max: number; integer?: boolean } @@ -1358,21 +1369,16 @@ const saveAgent = async () => { if (!form.name.trim()) return saving.value = true try { - const payload = { + const payload: CreateDeepChatAgentInput = { name: form.name.trim(), enabled: form.enabled, description: form.description.trim() || undefined, avatar: buildAvatar(), config: { - defaultModelPreset: form.chatModel - ? { - providerId: form.chatModel.providerId, - modelId: form.chatModel.modelId - } - : null, - assistantModel: form.assistantModel, - visionModel: form.visionModel, - imageGenerationModel: form.imageGenerationModel, + defaultModelPreset: buildModelSelection(form.chatModel), + assistantModel: buildModelSelection(form.assistantModel), + visionModel: buildModelSelection(form.visionModel), + imageGenerationModel: buildModelSelection(form.imageGenerationModel), defaultProjectPath: normalizePath(form.defaultProjectPath), systemPrompt: form.systemPrompt, permissionMode: form.permissionMode, diff --git a/src/renderer/src/components/mcp-config/mcpServerForm.vue b/src/renderer/src/components/mcp-config/mcpServerForm.vue index 5230924e6..cbe693d2d 100644 --- a/src/renderer/src/components/mcp-config/mcpServerForm.vue +++ b/src/renderer/src/components/mcp-config/mcpServerForm.vue @@ -2,6 +2,7 @@ import { ref, computed, watch } from 'vue' import { useI18n } from 'vue-i18n' import { Button } from '@shadcn/components/ui/button' +import { Checkbox } from '@shadcn/components/ui/checkbox' import { Input } from '@shadcn/components/ui/input' import { Label } from '@shadcn/components/ui/label' import { Textarea } from '@shadcn/components/ui/textarea' diff --git a/test/renderer/api/clients.test.ts b/test/renderer/api/clients.test.ts index 1360bad53..32f38db3e 100644 --- a/test/renderer/api/clients.test.ts +++ b/test/renderer/api/clients.test.ts @@ -1291,6 +1291,147 @@ describe('renderer api clients', () => { }) }) + it('serializes settings save payloads before invoking the config bridge', async () => { + const bridge = createBridge() + const configClient = createConfigClient(bridge) + const shortcutKeys = new Proxy( + { + toggleWindow: 'CommandOrControl+K' + }, + {} + ) + const promptParameters = new Proxy( + [ + { + name: 'topic', + description: 'Topic', + required: true + } + ], + {} + ) + const customPrompt = new Proxy( + { + id: 'prompt-1', + name: 'Writer', + description: 'Write clearly', + content: 'Write about {{topic}}', + parameters: promptParameters, + enabled: true + }, + {} + ) + const customPromptUpdate = new Proxy( + { + description: 'Updated', + parameters: promptParameters + }, + {} + ) + const systemPrompt = new Proxy( + { + id: 'system-1', + name: 'System', + content: 'Be concise' + }, + {} + ) + const modelSelection = new Proxy( + { + providerId: 'openai', + modelId: 'gpt-4.1' + }, + {} + ) + const deepChatAgentInput = new Proxy( + { + name: 'Writer Agent', + enabled: true, + config: new Proxy( + { + assistantModel: modelSelection + }, + {} + ) + }, + {} + ) + + await configClient.setShortcutKey(shortcutKeys) + await configClient.addCustomPrompt(customPrompt) + await configClient.updateCustomPrompt('prompt-1', customPromptUpdate) + await configClient.setCustomPrompts(new Proxy([customPrompt], {})) + await configClient.addSystemPrompt(systemPrompt) + await configClient.updateSystemPrompt('system-1', new Proxy({ content: 'Updated' }, {})) + await configClient.setSystemPrompts(new Proxy([systemPrompt], {})) + await configClient.createDeepChatAgent(deepChatAgentInput) + await configClient.updateDeepChatAgent( + 'writer', + new Proxy( + { + config: new Proxy( + { + visionModel: modelSelection + }, + {} + ) + }, + {} + ) + ) + + const calls = (bridge.invoke as ReturnType).mock.calls + for (const [, payload] of calls) { + expect(() => structuredClone(payload)).not.toThrow() + } + + expect(calls[0]).toEqual([ + 'config.setShortcutKeys', + { + shortcuts: { + toggleWindow: 'CommandOrControl+K' + } + } + ]) + expect(calls[1][1].prompt).toEqual({ + id: 'prompt-1', + name: 'Writer', + description: 'Write clearly', + content: 'Write about {{topic}}', + parameters: [ + { + name: 'topic', + description: 'Topic', + required: true + } + ], + enabled: true + }) + expect(calls[1][1].prompt).not.toBe(customPrompt) + expect(calls[1][1].prompt.parameters).not.toBe(promptParameters) + expect(calls[7][1]).toEqual({ + name: 'Writer Agent', + enabled: true, + config: { + assistantModel: { + providerId: 'openai', + modelId: 'gpt-4.1' + } + } + }) + expect(calls[8][1]).toEqual({ + agentId: 'writer', + updates: { + config: { + visionModel: { + providerId: 'openai', + modelId: 'gpt-4.1' + } + } + } + }) + }) + it('routes ACP config calls through the shared registry names', async () => { const bridge = createBridge() const configClient = createConfigClient(bridge) diff --git a/test/renderer/components/DeepChatAgentsSettings.test.ts b/test/renderer/components/DeepChatAgentsSettings.test.ts index e7f7cb1e9..74fd4a6ee 100644 --- a/test/renderer/components/DeepChatAgentsSettings.test.ts +++ b/test/renderer/components/DeepChatAgentsSettings.test.ts @@ -127,7 +127,7 @@ describe('DeepChatAgentsSettings', () => { clientMocks.toolClient.getAllToolDefinitions.mockReset() }) - it('mounts and saves DeepChat agents without advanced model overrides', async () => { + it('mounts and saves DeepChat agents with cloneable model selections', async () => { vi.resetModules() const existingAgent = { @@ -150,8 +150,8 @@ describe('DeepChatAgentsSettings', () => { verbosity: 'high', forceInterleavedThinkingCompat: true }, - assistantModel: null, - visionModel: null, + assistantModel: { providerId: 'anthropic', modelId: 'claude-3-5-sonnet' }, + visionModel: { providerId: 'openai', modelId: 'gpt-4.1-vision' }, imageGenerationModel: { providerId: 'openai', modelId: 'gpt-image-1' }, systemPrompt: 'system prompt', permissionMode: 'default', @@ -194,8 +194,13 @@ describe('DeepChatAgentsSettings', () => { providerId: 'openai', models: [ { id: 'gpt-4.1', name: 'GPT-4.1' }, + { id: 'gpt-4.1-vision', name: 'GPT-4.1 Vision' }, { id: 'gpt-image-1', name: 'GPT Image 1', type: ModelType.ImageGeneration } ] + }, + { + providerId: 'anthropic', + models: [{ id: 'claude-3-5-sonnet', name: 'Claude 3.5 Sonnet' }] } ], findModelByIdOrName: vi.fn((modelId: string) => @@ -300,8 +305,8 @@ describe('DeepChatAgentsSettings', () => { providerId: 'openai', modelId: 'gpt-4.1' }, - assistantModel: null, - visionModel: null, + assistantModel: { providerId: 'anthropic', modelId: 'claude-3-5-sonnet' }, + visionModel: { providerId: 'openai', modelId: 'gpt-4.1-vision' }, imageGenerationModel: { providerId: 'openai', modelId: 'gpt-image-1' }, defaultProjectPath: null, systemPrompt: 'system prompt', @@ -316,10 +321,19 @@ describe('DeepChatAgentsSettings', () => { providerId: 'openai', modelId: 'gpt-4.1' }) + expect(payload.config.assistantModel).toEqual({ + providerId: 'anthropic', + modelId: 'claude-3-5-sonnet' + }) + expect(payload.config.visionModel).toEqual({ + providerId: 'openai', + modelId: 'gpt-4.1-vision' + }) expect(payload.config.imageGenerationModel).toEqual({ providerId: 'openai', modelId: 'gpt-image-1' }) + expect(() => structuredClone(payload)).not.toThrow() }) it('filters the image generation model selector to image models', async () => { diff --git a/test/renderer/components/McpServerForm.test.ts b/test/renderer/components/McpServerForm.test.ts new file mode 100644 index 000000000..8a7b4d9a6 --- /dev/null +++ b/test/renderer/components/McpServerForm.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, it, vi } from 'vitest' +import { defineComponent } from 'vue' +import { mount } from '@vue/test-utils' + +const passthrough = (name: string, tag = 'div') => + defineComponent({ + name, + template: `<${tag} v-bind="$attrs">` + }) + +const buttonStub = defineComponent({ + name: 'Button', + emits: ['click'], + template: + '' +}) + +const inputStub = defineComponent({ + name: 'Input', + props: { + modelValue: { type: [String, Number], default: '' } + }, + emits: ['update:modelValue'], + template: + '' +}) + +const textareaStub = defineComponent({ + name: 'Textarea', + props: { + modelValue: { type: String, default: '' } + }, + emits: ['update:modelValue'], + template: + '