Compare commits

..

52 Commits

Author SHA1 Message Date
6f83311d94 Merge pull request 'feat: display inference statistics in chat UI' (#45) from feature/issue-43-inference-stats into main
Merge pull request 'feat: display inference statistics in chat UI' (#45)
2026-03-13 14:50:35 +01:00
shahondin1624
36731aac1d chore: mark issue #43 plan as COMPLETED
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:46:47 +01:00
shahondin1624
3d85cc6b8c feat: add aria attributes to context utilization progress bar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:46:35 +01:00
shahondin1624
52eaf661c4 feat: display inference statistics in chat UI
Add InferenceStats proto message and InferenceStatsPanel component that
displays token counts, throughput, and context window utilization as a
collapsible panel below assistant messages after orchestration completes.

Closes #43

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:44:12 +01:00
e93158f670 Merge pull request 'feat: structured artifact rendering, UX improvements' (#44) from feat/structured-artifact-passthrough into main 2026-03-12 23:14:28 +01:00
shahondin1624
2c6c961e08 feat: structured artifact rendering, UX improvements
- Render structured artifacts from agent results with type-aware
  formatting: code blocks with syntax highlighting and copy button,
  terminal-style command output, search result cards, and text findings
- Make FinalResult panel collapsible (default collapsed) with scrollable
  content (max-h-96) to prevent dominating the chat view
- Add clickable URL detection in summaries and artifact content
- Fix code block contrast for both light and dark mode
- Animate progress bar with pulse ring on active step and gradient
  shimmer on connecting lines
- Fix tab-switching bug: use module-level orchestrationStore singleton
  so orchestration state survives route navigation
- Remove sample/demo data seeding and clean up persisted localStorage
  entries from previous sample sessions
- Remove showSampleBadge prop from PageHeader
- Regenerate proto types for new Artifact message and ArtifactType enum
- Update README project structure (remove deleted data/ directory)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:13:33 +01:00
cfd338028a Merge pull request 'fix: prevent session creation loop without backend' (#42) from fix/session-creation-loop into main 2026-03-12 16:41:41 +01:00
shahondin1624
a5bc38cb65 fix: prevent session creation loop when running without backend
Make getOrCreateSession idempotent by returning the existing active
session when called with no ID, instead of always creating a new one.
Use untrack() in the $effect to sever SvelteMap dependency so it only
re-runs on $page changes. Disable SSR on client-only routes, reduce
preload aggressiveness, and skip retries when already disconnected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:41:08 +01:00
d7d1d9fd57 Merge pull request 'refactor: code review improvements' (#41) from refactor/code-review-improvements into main 2026-03-12 13:48:57 +01:00
shahondin1624
38f5f31b92 refactor: code review improvements — fix bugs, extract shared utilities, add README
- Fix reactivity bug: use SvelteMap instead of Map in sessions store
- Fix theme listener memory leak: guard against double-init, return cleanup function
- Fix transport singleton ignoring different endpoints
- Fix form/button type mismatch in MessageInput
- Add safer retry validation in chat page
- Extract shared utilities: date formatting, session config check, result source config
- Extract shared components: Backdrop, PageHeader
- Extract orchestration composable from chat page (310→85 lines of script)
- Consolidate AuditTimeline switch functions into config record
- Move sample data to dedicated module
- Add dark mode support for LineageTree SVG colors
- Memoize leaf count computation in LineageTree
- Update README with usage guide and project structure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:48:06 +01:00
bb0eebff1b Merge pull request 'feat: error handling and connection status (#20)' (#40) from feature/issue-20-error-handling into main 2026-03-12 13:28:34 +01:00
shahondin1624
14c83832f5 feat: add error handling, toast notifications, connection status, and retry logic (#20)
Add global error boundary, toast notification system for gRPC errors,
connection status indicator in chat header, and automatic retry with
exponential backoff for transient failures. Map gRPC status codes to
user-friendly messages and add a Retry button on failed requests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:27:40 +01:00
284f84bd39 Merge pull request 'feat: responsive layout and mobile support (#19)' (#39) from feature/issue-19-responsive-layout into main 2026-03-12 13:20:54 +01:00
shahondin1624
dfef26bad5 feat: add responsive layout and mobile support (#19)
Add collapsible sidebars with slide-in drawers on mobile, hamburger menu
for session sidebar, stacked layouts for screens under 768px, 44px touch
targets, and overflow prevention across all pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:17:02 +01:00
d837ed9050 Merge pull request 'feat: dark/light theme toggle (#18)' (#38) from feature/issue-18-dark-theme into main 2026-03-12 13:04:56 +01:00
shahondin1624
60f3d8eeda feat: add dark/light theme toggle with system preference support (#18)
Add theme switching with three modes (system/light/dark) using Tailwind v4
class-based dark mode. Theme preference is persisted in localStorage and
defaults to the system's prefers-color-scheme. All components updated with
dark: variants for consistent dark mode rendering including SVG elements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 12:54:04 +01:00
79c14378a2 Merge pull request 'feat: audit/activity log view (#17)' (#37) from feature/issue-17-audit-log into main 2026-03-12 12:46:01 +01:00
shahondin1624
4fc84ccd62 feat: add audit/activity log view with timeline and event filtering (#17)
Add a dedicated /audit route displaying a chronological timeline of
orchestration events (state transitions, tool invocations, errors,
messages) grouped by session. Includes session selector, event type
filtering, URL deep-linking via ?session=id, and audit event capture
during chat streaming.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 12:41:40 +01:00
807311d7c9 Merge pull request 'feat: memory candidates viewer (#16)' (#36) from feature/issue-16-memory-viewer into main 2026-03-12 12:36:04 +01:00
shahondin1624
9ad772f83f feat: add memory candidates viewer with filtering and confidence visualization
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 12:33:48 +01:00
5efc5c351c Merge pull request 'feat: agent lineage visualization (#15)' (#35) from feature/issue-15-agent-lineage into main 2026-03-12 12:21:23 +01:00
shahondin1624
209e38d8a6 feat: add agent lineage visualization with SVG tree rendering
Add a dedicated /lineage route with custom SVG-based tree visualization
of the agent spawn chain. Nodes are colored by agent type (orchestrator,
researcher, coder, sysadmin, assistant) and display agent ID, type, and
spawn depth. Clicking a node opens a detail panel. Uses sample data
since the API does not yet expose lineage information.

Closes #15

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 12:19:49 +01:00
5624175ddd Merge pull request 'feat: preset configurations (#14)' (#34) from feature/issue-14-preset-configs into main 2026-03-12 12:09:39 +01:00
shahondin1624
4bd1cef1cf feat: add preset configurations with built-in and custom presets
Add preset store with three built-in presets (Strict mode, Research only,
Full access) and localStorage persistence for custom presets. Integrate
preset selector into ConfigSidebar with load, save, and delete actions.
Built-in presets cannot be deleted.

Closes #14

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 12:07:50 +01:00
a14a92a87d Merge pull request 'feat: session config sidebar component (#13)' (#33) from feature/issue-13-config-sidebar into main 2026-03-12 12:04:24 +01:00
shahondin1624
7b81de9ffd feat: add session config sidebar with override level, tools, and permissions
Implements issue #13 — right sidebar for session configuration with
override level selection, disabled tools checkboxes, and granted
permissions input. Integrates config into chat page header with toggle
button and non-default indicator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 12:03:40 +01:00
1e62f7d1c1 Merge pull request 'feat: add session history sidebar' (#32) from feature/issue-12-session-sidebar into main 2026-03-12 11:56:17 +01:00
shahondin1624
19c3c2bcdc feat: add session history sidebar with delete and navigation
- SessionSidebar component listing past sessions sorted by recency
- Session title preview and relative date display
- Click to switch sessions, delete with confirmation
- Added deleteSession method to session store
- Integrated sidebar into chat page layout

Closes #12

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:55:58 +01:00
ed0a01c857 Merge pull request 'feat: add session creation and ID management' (#31) from feature/issue-11-session-management into main 2026-03-12 11:52:47 +01:00
shahondin1624
247abfe32c feat: add session creation and ID management with localStorage
- Session store with UUID v4 generation and localStorage persistence
- Session ID in URL params (?session=<id>) for deep linking
- "New Chat" button for creating fresh sessions
- Message history persisted per session
- Session title auto-generated from first user message

Closes #11

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:52:19 +01:00
9613a5ad5b Merge pull request 'feat: add final result rendering with status badges and artifacts' (#30) from feature/issue-10-final-result into main 2026-03-12 11:38:45 +01:00
shahondin1624
2ff3e181a4 feat: add final result rendering with status badges and artifacts
- FinalResult component showing SubagentResult summary and artifacts
- Status badges: Success (green), Partial (amber), Failed (red)
- Quality and source badges (Verified/Inferred, Tool Output/Web/etc)
- Artifact list with file icons
- Integrated into chat page after stream completes

Closes #10

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:38:29 +01:00
4f2bf514e5 Merge pull request 'feat: add collapsible thinking section for intermediate results' (#29) from feature/issue-9-intermediate-results into main 2026-03-12 11:36:35 +01:00
shahondin1624
959bb59874 feat: add collapsible thinking section for intermediate results
- ThinkingSection component with expand/collapse toggle
- Displays intermediate_result from stream in amber-styled panel
- Positioned below orchestration progress indicator
- Updates in real-time as new intermediate results arrive

Closes #9

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:35:46 +01:00
0674752e80 Merge pull request 'feat: add orchestration state progress indicator' (#28) from feature/issue-8-progress-indicator into main 2026-03-12 11:33:58 +01:00
shahondin1624
945dbb9f84 feat: add orchestration state progress indicator
- OrchestrationProgress stepper component with 5 phases
- Visual state: completed (green check), active (blue ring), pending (gray)
- Smooth CSS transitions between states
- Integrated into chat page, visible during streaming

Closes #8

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:33:41 +01:00
fe52d96347 Merge pull request 'feat: connect chat UI to gRPC-Web streaming' (#27) from feature/issue-7-streaming-response into main 2026-03-12 11:30:15 +01:00
shahondin1624
f6eef3a7f6 feat: connect chat UI to gRPC-Web streaming with loading indicator
- Wire processRequest() async generator to chat page
- Progressive message rendering as stream chunks arrive
- Animated loading dots while waiting for first chunk
- Error display with OrchestratorError code mapping
- Session ID management with crypto.randomUUID()

Closes #7

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:29:10 +01:00
8802d1bb72 Merge pull request 'feat: add message input with send and keyboard shortcuts' (#26) from feature/issue-6-message-input into main 2026-03-12 11:27:56 +01:00
shahondin1624
306d0a7f2d feat: add message input with send button and keyboard shortcuts
- MessageInput component with textarea, send button, Enter-to-send
- Shift+Enter for newline, auto-resize textarea
- Disabled state while streaming, auto-focus after send
- Integrated into /chat page with user message handling

Closes #6

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:27:34 +01:00
a8d28095b3 Merge pull request 'feat: add chat page layout with message list and bubbles' (#25) from feature/issue-5-chat-layout into main 2026-03-12 11:25:55 +01:00
shahondin1624
6df4c396b9 feat: add chat page layout with message list and bubble components
- Create /chat route with scrollable message list
- MessageBubble component with distinct user/assistant styles
- MessageList with auto-scroll-to-bottom on new messages
- Empty state display when no messages
- ChatMessage type definition in src/lib/types.ts
- Add browser globals to ESLint config for Svelte files

Closes #5

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:25:08 +01:00
f2daa9cef2 Merge pull request 'feat: add gRPC-Web client service layer for OrchestratorService' (#24) from feature/issue-4-grpc-web-client into main 2026-03-12 11:22:32 +01:00
shahondin1624
d011447190 feat: add gRPC-Web client service layer for OrchestratorService
- Create src/lib/services/orchestrator.ts with Connect-Web transport
- Typed processRequest() async generator for server-streaming RPC
- OrchestratorError class mapping gRPC status codes to app errors
- Configurable endpoint with resetTransport() for reconfiguration

Closes #4

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:22:15 +01:00
46e1a54379 Merge pull request 'feat: add Caddyfile for gRPC-Web reverse proxy with CORS' (#23) from feature/issue-3-caddy-grpc-web into main 2026-03-12 11:19:46 +01:00
shahondin1624
298a186c31 feat: add Caddyfile for gRPC-Web reverse proxy with CORS support
- Route gRPC-Web requests to Orchestrator via h2c transport
- Handle CORS preflight and response headers for cross-origin access
- Proxy remaining traffic to SvelteKit dev server
- Configurable via env vars: ORCHESTRATOR_HOST, UI_HOST, DOMAIN, CORS_ORIGIN

Closes #3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:19:06 +01:00
f70ad719ca Merge pull request 'feat: add proto codegen pipeline with buf and connect-es' (#22) from feature/issue-2-proto-codegen into main 2026-03-12 11:14:59 +01:00
shahondin1624
ed1e800e53 docs: mark issue #2 plan as COMPLETED
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:14:23 +01:00
shahondin1624
2516c86002 feat: add proto codegen pipeline with buf and connect-es
- Add llm-multiverse repo as git submodule for proto files
- Configure buf with @bufbuild/protoc-gen-es for TypeScript codegen
- Generate typed Connect service stubs to src/lib/proto/
- Add `generate` npm script for proto regeneration
- Exclude generated proto files from ESLint

Closes #2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:14:11 +01:00
0a46d2b95b Merge pull request 'feat: scaffold SvelteKit project with Tailwind, TypeScript, ESLint, Prettier' (#21) from feature/issue-1-project-scaffolding into main 2026-03-12 11:07:49 +01:00
shahondin1624
d85e76cab2 docs: mark issue #1 plan as COMPLETED
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:04:32 +01:00
shahondin1624
978325565d feat: scaffold SvelteKit project with Tailwind, TypeScript, ESLint, Prettier
Initialize the llm-multiverse-ui project with:
- SvelteKit + Svelte 5 (runes mode enabled)
- Tailwind CSS v4 via @tailwindcss/vite plugin
- TypeScript strict mode
- ESLint 9 flat config with svelte and typescript-eslint plugins
- Prettier with svelte plugin
- Directory structure: src/lib/components/, src/lib/services/
- All required scripts: dev, build, preview, lint, format, check

Closes #1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:03:46 +01:00
112 changed files with 11762 additions and 256 deletions

View File

@@ -20,11 +20,11 @@ When in subagent mode, your final output MUST be a single JSON object:
```json
{
"status": "success | partial | failed",
"summary": "3 sentence max description of what happened",
"artifacts": ["list of file paths created or modified"],
"phase_data": { },
"failure_reason": null
"status": "success | partial | failed",
"summary": "3 sentence max description of what happened",
"artifacts": ["list of file paths created or modified"],
"phase_data": {},
"failure_reason": null
}
```
@@ -35,6 +35,7 @@ When in subagent mode, your final output MUST be a single JSON object:
## Architecture Reference
All agents MUST respect the project's architecture constraints. Read `CLAUDE.md` if it exists for project-specific rules. Key principles:
- Follow the established frontend framework patterns and conventions
- Use the project's chosen state management approach consistently
- Follow component composition patterns already established in the codebase

View File

@@ -29,6 +29,7 @@ git diff main --name-only
```
Read every changed file in full. Also read the diff for context on what changed:
```bash
git diff main
```
@@ -38,12 +39,14 @@ git diff main
Evaluate each changed file against these dimensions:
**Correctness:**
- Does the code do what the issue and plan require?
- Are edge cases handled?
- Are error conditions properly managed?
- Do loading and empty states work correctly?
**Security:**
- No XSS vulnerabilities (dangerouslySetInnerHTML, unsanitized user input)
- No credentials or API keys in client-side code
- No sensitive data stored insecurely (localStorage, etc.)
@@ -51,6 +54,7 @@ Evaluate each changed file against these dimensions:
- No open redirects
**Architecture:**
- Component boundaries respected
- State management follows project patterns
- API communication uses established patterns
@@ -58,6 +62,7 @@ Evaluate each changed file against these dimensions:
- Proper separation of concerns (logic vs presentation)
**Code Quality:**
- Idiomatic TypeScript/framework patterns
- Consistent error handling
- Meaningful variable and function names
@@ -66,18 +71,21 @@ Evaluate each changed file against these dimensions:
- No `any` types without justification
**Testing:**
- Sufficient test coverage
- Meaningful test cases (not just happy path)
- Component tests for UI behavior
- Proper mocking of external dependencies
**Accessibility:**
- Semantic HTML elements used
- ARIA attributes where needed
- Keyboard navigation support
- Color contrast considerations
**Performance:**
- No unnecessary re-renders
- Proper memoization where beneficial
- Lazy loading for heavy components/routes
@@ -87,12 +95,12 @@ Evaluate each changed file against these dimensions:
Each finding MUST be categorized:
| Severity | Description | Blocks Merge? |
|---|---|---|
| **Critical** | Security vulnerability, data loss risk, major architectural violation | Yes |
| **Major** | Bug, missing error handling, test gap, significant design issue | Yes |
| **Minor** | Style issue, naming improvement, small optimization, documentation gap | No |
| **Suggestion** | Optional improvement, alternative approach worth considering | No |
| Severity | Description | Blocks Merge? |
| -------------- | ---------------------------------------------------------------------- | ------------- |
| **Critical** | Security vulnerability, data loss risk, major architectural violation | Yes |
| **Major** | Bug, missing error handling, test gap, significant design issue | Yes |
| **Minor** | Style issue, naming improvement, small optimization, documentation gap | No |
| **Suggestion** | Optional improvement, alternative approach worth considering | No |
### 5. Produce Review Report
@@ -130,6 +138,7 @@ Request Changes: one or more critical/major findings
### 6. Handle Minor Findings (standalone mode only)
If the verdict is **APPROVE** but there are minor findings:
1. Create a single Gitea issue titled: "Tech debt: minor findings from issue #<NUMBER> review"
2. List all minor findings in the issue body as checklist items
3. Apply labels: `type:refactor`, `priority:low`, `cat:tech-debt`
@@ -138,6 +147,7 @@ If the verdict is **APPROVE** but there are minor findings:
### 7. Post Review to PR (standalone mode only)
If a pull request exists for the feature branch:
- Add a review comment via `mcp__gitea__pull_request_review_write`
- If APPROVE: approve the PR
- If REQUEST_CHANGES: request changes with the critical/major findings listed
@@ -146,25 +156,23 @@ If a pull request exists for the feature branch:
```json
{
"status": "success | failed",
"summary": "Code review of issue #N: APPROVE/REQUEST_CHANGES",
"artifacts": [],
"phase_data": {
"verdict": "APPROVE",
"findings": {
"critical": 0,
"major": 0,
"minor": 2,
"suggestion": 1
},
"critical_details": [],
"major_details": [],
"minor_details": [
{"file": "src/components/Dashboard.tsx", "line": 42, "description": "..."}
],
"pr_number": null
},
"failure_reason": null
"status": "success | failed",
"summary": "Code review of issue #N: APPROVE/REQUEST_CHANGES",
"artifacts": [],
"phase_data": {
"verdict": "APPROVE",
"findings": {
"critical": 0,
"major": 0,
"minor": 2,
"suggestion": 1
},
"critical_details": [],
"major_details": [],
"minor_details": [{ "file": "src/components/Dashboard.tsx", "line": 42, "description": "..." }],
"pr_number": null
},
"failure_reason": null
}
```

View File

@@ -23,6 +23,7 @@ An implementation plan MUST exist at `implementation-plans/issue-<NUMBER>.md` wi
### 1. Read the Plan and Context
Read these files:
- `implementation-plans/issue-<NUMBER>.md` -- the implementation plan
- `CLAUDE.md` -- coding standards (if it exists)
- `package.json` -- project dependencies and scripts
@@ -61,6 +62,7 @@ Follow the plan's implementation steps in order:
### 5. Code Quality Standards
**General:**
- TypeScript strict mode -- no `any` types without justification
- Use the project's established patterns for component structure
- Follow the project's naming conventions (check existing code)
@@ -68,17 +70,20 @@ Follow the plan's implementation steps in order:
- Accessible markup (semantic HTML, ARIA attributes where needed)
**Components:**
- Keep components focused -- single responsibility
- Extract reusable logic into custom hooks
- Use proper prop typing with TypeScript interfaces
- Handle loading, error, and empty states
**State Management:**
- Follow the project's chosen state management approach
- Keep state as local as possible
- Avoid prop drilling -- use context or state management when appropriate
**Styling:**
- Follow the project's established styling approach
- Ensure responsive design
- Support dark/light themes if the project uses them
@@ -86,6 +91,7 @@ Follow the plan's implementation steps in order:
### 6. Log Deviations
If you deviate from the plan (different approach, additional files, skipped steps), document each deviation in the plan's **Deviation Log** section with:
- What changed
- Why it changed
@@ -115,6 +121,7 @@ Adapt commands based on what's available in `package.json`. Fix any failures bef
### 8. Commit
Stage all changed files and commit with a descriptive message:
```
feat: <short description of what was implemented> (issue #<NUMBER>)
```
@@ -124,6 +131,7 @@ Use conventional commit prefixes: `feat:`, `fix:`, `chore:`, `refactor:`, `test:
### 9. Output
**standalone mode:** Display:
- Files created and modified (with counts)
- Tests added (count and coverage percentage)
- Deviations from plan (if any)
@@ -136,23 +144,23 @@ Use conventional commit prefixes: `feat:`, `fix:`, `chore:`, `refactor:`, `test:
```json
{
"status": "success | failed",
"summary": "Implemented issue #N on branch feature/issue-N-desc",
"artifacts": ["list of files created/modified"],
"phase_data": {
"issue_number": 28,
"branch_name": "feature/issue-28-dashboard-page",
"files_created": ["src/pages/Dashboard.tsx"],
"files_modified": ["src/App.tsx"],
"quality_gates": {
"build": "pass",
"lint": "pass",
"typecheck": "pass",
"tests": "pass"
},
"deviations": []
},
"failure_reason": null
"status": "success | failed",
"summary": "Implemented issue #N on branch feature/issue-N-desc",
"artifacts": ["list of files created/modified"],
"phase_data": {
"issue_number": 28,
"branch_name": "feature/issue-28-dashboard-page",
"files_created": ["src/pages/Dashboard.tsx"],
"files_modified": ["src/App.tsx"],
"quality_gates": {
"build": "pass",
"lint": "pass",
"typecheck": "pass",
"tests": "pass"
},
"deviations": []
},
"failure_reason": null
}
```

View File

@@ -23,6 +23,7 @@ Use `mcp__gitea__issue_read` to get the full issue (title, body, labels, milesto
### 2. Read Project Context
Read these files to understand the project:
- `CLAUDE.md` -- coding standards and workflow (if it exists)
- `package.json` -- project dependencies and scripts
- `implementation-plans/_index.md` -- existing plans index (if it exists)
@@ -30,6 +31,7 @@ Read these files to understand the project:
### 3. Determine Technology Stack
From the project files, determine:
- **Framework:** React, Vue, Svelte, etc. (check package.json)
- **Language:** TypeScript or JavaScript
- **Build tool:** Vite, Next.js, Webpack, etc.
@@ -40,6 +42,7 @@ From the project files, determine:
### 4. Find Related Plans
From the index (if it exists), identify plans that share:
- The same feature area or component
- Overlapping affected files
- Dependency relationships (blocked-by / blocks)
@@ -49,6 +52,7 @@ Read those related plan files to understand prior decisions and patterns.
### 5. Explore the Codebase
Based on the issue's scope, explore relevant code:
- Use Glob to find files in affected directories
- Use Grep to find existing patterns, interfaces, types, and components
- Use Read to examine specific files mentioned in the issue or related plans
@@ -60,6 +64,7 @@ Based on the issue's scope, explore relevant code:
Create the plan. The plan MUST include:
**Metadata:**
- Issue link, number, title
- Milestone and labels
- Status: `PLANNED`
@@ -68,9 +73,11 @@ Create the plan. The plan MUST include:
- Blocked-by references
**Acceptance Criteria:**
- Copy directly from the issue body
**Architecture Analysis:**
- Which components/pages are affected
- Which API endpoints are involved
- Which state/stores are affected
@@ -78,6 +85,7 @@ Create the plan. The plan MUST include:
- Existing patterns to follow (with file references)
**Implementation Steps (phase by phase):**
1. **Types & Configuration** -- TypeScript types/interfaces, config constants, API types
2. **Core Logic** -- Business logic, hooks, utilities, state management
3. **Components** -- UI components, layouts, pages
@@ -85,9 +93,11 @@ Create the plan. The plan MUST include:
5. **Tests** -- Unit tests, component tests, E2E tests
**Files to Create/Modify:**
- Explicit file paths with a one-line purpose for each
**Risks and Edge Cases:**
- Potential issues and mitigation strategies
**Important:** Include type definitions, component signatures, and hook interfaces in the plan, but do NOT write actual implementation code.
@@ -99,6 +109,7 @@ Write the plan to `implementation-plans/issue-<NUMBER>.md`.
### 8. Update the Index
Create or update `implementation-plans/_index.md`:
- Add the new plan to the master table
- Add cross-references in the appropriate feature area section
@@ -111,16 +122,16 @@ Create or update `implementation-plans/_index.md`:
```json
{
"status": "success | failed",
"summary": "Created implementation plan for issue #N",
"artifacts": ["implementation-plans/issue-N.md", "implementation-plans/_index.md"],
"phase_data": {
"issue_number": 28,
"plan_path": "implementation-plans/issue-28.md",
"language": "typescript",
"framework": "react"
},
"failure_reason": null
"status": "success | failed",
"summary": "Created implementation plan for issue #N",
"artifacts": ["implementation-plans/issue-N.md", "implementation-plans/_index.md"],
"phase_data": {
"issue_number": 28,
"plan_path": "implementation-plans/issue-28.md",
"language": "typescript",
"framework": "react"
},
"failure_reason": null
}
```

View File

@@ -12,6 +12,7 @@ Mode is specified in Dynamic Context below. Default: standalone.
## Trigger
This agent is invoked:
- Periodically (every ~5 completed stories) by the auto-dev pipeline
- Manually by the user via `/project:refactor-review`
@@ -26,6 +27,7 @@ This agent is invoked:
### 2. Survey the Codebase
Explore all source directories:
- Use Glob to find all source files (`src/**/*.ts`, `src/**/*.tsx`, `src/**/*.css`, `src/**/*.vue`, `src/**/*.svelte`, etc.)
- Use Grep to find patterns of concern (see checklist below)
- Read key files to understand current state
@@ -35,17 +37,20 @@ Explore all source directories:
Evaluate the project against these dimensions:
**Code Duplication:**
- Shared logic duplicated across components instead of extracted to hooks/utilities
- Similar UI patterns that should be abstracted into shared components
- Repeated API call patterns that should use a shared data fetching layer
**Modularity:**
- Components longer than ~100 lines that should be split
- Components with too many responsibilities (God components)
- Tight coupling between feature modules
- Missing abstractions (e.g., a custom hook for behavior used in multiple places)
**Consistency:**
- Inconsistent error handling patterns across components
- Inconsistent state management approaches
- Inconsistent API call patterns
@@ -53,23 +58,27 @@ Evaluate the project against these dimensions:
- Inconsistent styling approaches
**Architecture Drift:**
- Components bypassing the established API layer
- State management inconsistencies
- Routing pattern violations
- Feature boundaries not respected
**Dependency Health:**
- Unused dependencies in package.json
- Outdated dependencies with known vulnerabilities
- Lock file hygiene
**Test Quality:**
- Tests that only test happy paths
- Missing component tests for interactive features
- Missing E2E tests for critical user flows
- Test code duplication (shared fixtures/helpers needed)
**Accessibility:**
- Missing ARIA attributes on interactive elements
- Missing keyboard navigation
- Color contrast issues
@@ -79,11 +88,11 @@ Evaluate the project against these dimensions:
Categorize each finding:
| Priority | Description |
|---|---|
| **High** | Architecture drift, security concern, significant duplication causing bugs, accessibility blockers |
| **Medium** | Modularity issues, inconsistencies, test quality gaps |
| **Low** | Style issues, minor duplication, documentation gaps |
| Priority | Description |
| ---------- | -------------------------------------------------------------------------------------------------- |
| **High** | Architecture drift, security concern, significant duplication causing bugs, accessibility blockers |
| **Medium** | Modularity issues, inconsistencies, test quality gaps |
| **Low** | Style issues, minor duplication, documentation gaps |
### 5. Create Refactoring Issues
@@ -140,15 +149,15 @@ Check existing open issues with `type:refactor` and `priority:low` labels. If an
```json
{
"status": "success",
"summary": "Refactoring review complete",
"artifacts": [],
"phase_data": {
"project_health": "GOOD",
"issues_created": [{"number": 42, "title": "Refactor: ...", "priority": "medium"}],
"issues_closed": [{"number": 30, "title": "Tech debt: ...", "reason": "resolved"}]
},
"failure_reason": null
"status": "success",
"summary": "Refactoring review complete",
"artifacts": [],
"phase_data": {
"project_health": "GOOD",
"issues_created": [{ "number": 42, "title": "Refactor: ...", "priority": "medium" }],
"issues_closed": [{ "number": 30, "title": "Tech debt: ...", "reason": "resolved" }]
},
"failure_reason": null
}
```

View File

@@ -58,6 +58,7 @@ git checkout -b release/<milestone-slug> main
### 4. Create Release PR
Push the release branch and create a Gitea PR:
- **Title:** `Release: <milestone-name>`
- **Head:** `release/<milestone-slug>`
- **Base:** `main`
@@ -82,6 +83,7 @@ If **manually requested (standalone mode):** Proceed to merge.
### 6. Create Gitea Release
Use `mcp__gitea__create_release`:
- **tag_name:** `<milestone-slug>`
- **target:** `main`
- **title:** `<milestone-name>`
@@ -94,6 +96,7 @@ Use `mcp__gitea__milestone_write` to set the milestone state to `closed`.
### 8. Output
**standalone mode:** Display:
- Milestone name and version
- PR number and merge status
- Tag created
@@ -107,17 +110,17 @@ Use `mcp__gitea__milestone_write` to set the milestone state to `closed`.
```json
{
"status": "success | failed",
"summary": "Release PR created for milestone <name>",
"artifacts": [],
"phase_data": {
"milestone": "MVP",
"pr_number": 42,
"merged": false,
"tag": null,
"issues_included": [28, 29, 30]
},
"failure_reason": null
"status": "success | failed",
"summary": "Release PR created for milestone <name>",
"artifacts": [],
"phase_data": {
"milestone": "MVP",
"pr_number": 42,
"merged": false,
"tag": null,
"issues_included": [28, 29, 30]
},
"failure_reason": null
}
```

View File

@@ -18,6 +18,7 @@ Use `mcp__gitea__list_issues` to fetch all open issues. Paginate with `perPage:
### 2. Filter Out Ineligible Issues
Remove any issue that has:
- Label `workflow:manual`
- Label `workflow:blocked`
@@ -34,10 +35,12 @@ For each candidate issue, read its body and look for a "Blocked by" section. If
Sort remaining issues using this priority order:
**Milestone priority (earliest milestone first):**
- Sort by milestone due date (earliest first)
- Issues with no milestone come last
**Within the same milestone, sort by priority label:**
1. `priority:critical`
2. `priority:high`
3. `priority:medium`
@@ -47,6 +50,7 @@ Sort remaining issues using this priority order:
### 6. Present or Return Result
**standalone mode:** Display the highest-priority issue with:
- Issue number and title
- Milestone name
- All labels
@@ -61,6 +65,7 @@ Then ask: "Shall I proceed to plan this story, or would you like to pick a diffe
## Auto-Merge Eligibility
All issues are auto-merge eligible by default EXCEPT:
- Issues with label `workflow:manual-review`
If the issue has `workflow:manual-review`, set `auto_merge_eligible: false`. Otherwise set it to `true`.
@@ -69,17 +74,17 @@ If the issue has `workflow:manual-review`, set `auto_merge_eligible: false`. Oth
```json
{
"status": "success | failed",
"summary": "Selected issue #N: <title>",
"artifacts": [],
"phase_data": {
"issue_number": 28,
"issue_title": "Story title",
"milestone": "MVP",
"labels": ["type:feature", "priority:high"],
"auto_merge_eligible": true
},
"failure_reason": null
"status": "success | failed",
"summary": "Selected issue #N: <title>",
"artifacts": [],
"phase_data": {
"issue_number": 28,
"issue_title": "Story title",
"milestone": "MVP",
"labels": ["type:feature", "priority:high"],
"auto_merge_eligible": true
},
"failure_reason": null
}
```

View File

@@ -30,26 +30,31 @@ Check `package.json` and the plan to know which quality gates to run.
Run each gate and record pass/fail. Detect available commands from `package.json`:
Gate 1 -- Build:
```bash
npm run build
```
Gate 2 -- Lint:
```bash
npm run lint
```
Gate 3 -- Type Check:
```bash
npm run typecheck # or npx tsc --noEmit
```
Gate 4 -- Tests:
```bash
npm run test
```
Gate 5 -- Format (if available):
```bash
npm run format:check # or npx prettier --check .
```
@@ -61,18 +66,21 @@ Adapt commands based on what's available in `package.json`.
Review all files changed in this branch (use `git diff main --name-only` to get the list). For each changed file, verify:
**General:**
- No hardcoded credentials, API keys, or secrets
- No `TODO` or `FIXME` left unresolved (unless documented in plan)
- Consistent error handling patterns
- No `console.log` left in production code (use proper logging if available)
**TypeScript:**
- No `any` types without justification
- Proper type narrowing and null checks
- No type assertions (`as`) without justification
- Interfaces/types exported where needed
**Components:**
- Proper prop typing
- Loading, error, and empty states handled
- Accessible markup (semantic HTML, ARIA)
@@ -80,12 +88,14 @@ Review all files changed in this branch (use `git diff main --name-only` to get
- Responsive design considered
**State & Data:**
- State management follows project patterns
- API calls use the project's data fetching approach
- Error states properly handled and displayed
- No data fetching in render path without proper caching/memoization
**Security:**
- No XSS vulnerabilities (dangerouslySetInnerHTML, etc.)
- User input properly sanitized
- API tokens/secrets not in client-side code
@@ -94,6 +104,7 @@ Review all files changed in this branch (use `git diff main --name-only` to get
### 5. Acceptance Criteria Verification
For each acceptance criterion from the issue:
- Check the code to verify the criterion is met
- Note which file(s) satisfy each criterion
- Mark each criterion as PASS or FAIL with explanation
@@ -101,6 +112,7 @@ For each acceptance criterion from the issue:
### 6. Determine Result
**PASS** if ALL of the following are true:
- All quality gates pass
- No architecture violations found (major/critical)
- All acceptance criteria are met
@@ -163,24 +175,24 @@ For each acceptance criterion from the issue:
```json
{
"status": "success | failed",
"summary": "Verification of issue #N: PASS/FAIL",
"artifacts": [],
"phase_data": {
"verdict": "PASS",
"quality_gates": {
"build": "pass",
"lint": "pass",
"typecheck": "pass",
"tests": "pass",
"format": "pass"
},
"acceptance_criteria": [
{"criterion": "Description", "result": "PASS", "evidence": "Component.tsx:42"}
],
"architecture_violations": []
},
"failure_reason": null
"status": "success | failed",
"summary": "Verification of issue #N: PASS/FAIL",
"artifacts": [],
"phase_data": {
"verdict": "PASS",
"quality_gates": {
"build": "pass",
"lint": "pass",
"typecheck": "pass",
"tests": "pass",
"format": "pass"
},
"acceptance_criteria": [
{ "criterion": "Description", "result": "PASS", "evidence": "Component.tsx:42" }
],
"architecture_violations": []
},
"failure_reason": null
}
```

View File

@@ -10,6 +10,7 @@ You are the **Auto Dev Orchestrator**. You run the full development pipeline in
## Auto-Merge Eligibility
An issue is **auto-merge eligible** (no user approval needed) if ALL of these are true:
- The issue does NOT have a `workflow:manual-review` label
- All quality gates pass
- Code review by the review agent returns APPROVE (no critical/major findings)
@@ -35,6 +36,7 @@ Run the story selection logic (same as `/project:select-story`):
7. Present the top candidate to the user with issue number, title, milestone, labels, and summary
**PAUSE HERE** — Wait for user confirmation before proceeding. The user may:
- Confirm the selection
- Pick a different issue number
- Say "stop" to end the loop
@@ -87,6 +89,7 @@ Run the code review logic autonomously (same as `/project:code-review`):
Based on verification AND code review results:
**On PASS (verification passes AND code review APPROVE):**
1. Update plan status to `COMPLETED`
2. Push the feature branch to origin
3. Create a Gitea pull request targeting `main`
@@ -103,11 +106,13 @@ Based on verification AND code review results:
8. Loop back to Phase 1
**On FAIL (attempt 1):**
1. Update plan status to `RETRY`
2. Append retry instructions to the plan (what failed and how to fix it)
3. Loop back to Phase 3 (re-implement with retry instructions)
**On FAIL (attempt 2):**
1. Update plan status to `BLOCKED`
2. Add `workflow:manual` label to the Gitea issue
3. Inform the user: "Story #NNN blocked after 2 attempts. Marked for manual review."
@@ -116,6 +121,7 @@ Based on verification AND code review results:
### Phase 7 — MILESTONE CHECK
After each completed story, check if all issues in the story's milestone are now closed. If so:
1. Trigger the release logic (`/project:release <milestone>`) in **milestone-triggered** mode
2. The release PR is created but NOT merged — it awaits user approval
3. Inform the user: "All issues in <milestone> completed. Release PR created for your approval."
@@ -124,6 +130,7 @@ After each completed story, check if all issues in the story's milestone are now
### Phase 8 — PERIODIC REFACTORING CHECK
After every 5 completed stories (tracked by the `completed` counter):
1. Run the refactoring review logic (`/project:refactor-review`)
2. Create refactoring issues as needed
3. Close resolved tech debt issues
@@ -133,11 +140,13 @@ After every 5 completed stories (tracked by the `completed` counter):
## Stop Conditions
Stop the loop when any of these occur:
- The user says "stop"
- No eligible issues remain (all are completed, blocked, or have unmet dependencies)
- 3 consecutive stories are BLOCKED
When stopping, display a summary of work completed:
- Stories completed (with PR links)
- Stories auto-merged vs awaiting review
- Stories blocked (with failure reasons)
@@ -149,6 +158,7 @@ When stopping, display a summary of work completed:
## Tracking
Maintain a running tally during the session:
- `completed`: list of issue numbers with PR links
- `auto_merged`: list of issue numbers that were auto-merged
- `awaiting_review`: list of issue numbers with PRs awaiting manual review

View File

@@ -26,6 +26,7 @@ git diff main --name-only
```
Read every changed file in full. Also read the diff for context on what changed:
```bash
git diff main
```
@@ -35,23 +36,27 @@ git diff main
Evaluate each changed file against these dimensions:
**Correctness:**
- Does the code do what the issue and plan require?
- Are edge cases handled?
- Are error conditions properly managed?
**Security:**
- No XSS vulnerabilities
- No credentials or API keys in client-side code
- No sensitive data stored insecurely
- Proper input sanitization
**Architecture:**
- Component boundaries respected
- State management follows project patterns
- API communication uses established patterns
- Proper separation of concerns
**Code Quality:**
- Idiomatic TypeScript/framework patterns
- Consistent error handling
- Meaningful variable and function names
@@ -59,17 +64,20 @@ Evaluate each changed file against these dimensions:
- No `any` types without justification
**Testing:**
- Sufficient test coverage
- Meaningful test cases (not just happy path)
- Component tests for UI behavior
- Proper mocking of external dependencies
**Accessibility:**
- Semantic HTML elements used
- ARIA attributes where needed
- Keyboard navigation support
**Performance:**
- No unnecessary re-renders
- Proper memoization where beneficial
- No memory leaks
@@ -78,12 +86,12 @@ Evaluate each changed file against these dimensions:
Each finding MUST be categorized:
| Severity | Description | Blocks Merge? |
|---|---|---|
| **Critical** | Security vulnerability, data loss risk, major architectural violation | Yes |
| **Major** | Bug, missing error handling, test gap, significant design issue | Yes |
| **Minor** | Style issue, naming improvement, small optimization, documentation gap | No |
| **Suggestion** | Optional improvement, alternative approach worth considering | No |
| Severity | Description | Blocks Merge? |
| -------------- | ---------------------------------------------------------------------- | ------------- |
| **Critical** | Security vulnerability, data loss risk, major architectural violation | Yes |
| **Major** | Bug, missing error handling, test gap, significant design issue | Yes |
| **Minor** | Style issue, naming improvement, small optimization, documentation gap | No |
| **Suggestion** | Optional improvement, alternative approach worth considering | No |
### 5. Produce Review Report
@@ -117,6 +125,7 @@ Request Changes: one or more critical/major findings
### 6. Handle Minor Findings
If the verdict is **APPROVE** but there are minor findings:
1. Create a single Gitea issue titled: "Tech debt: minor findings from issue #$ARGUMENTS review"
2. List all minor findings in the issue body as checklist items
3. Apply labels: `type:refactor`, `priority:low`, `cat:tech-debt`
@@ -125,6 +134,7 @@ If the verdict is **APPROVE** but there are minor findings:
### 7. Post Review to PR
If a pull request exists for the feature branch:
- Add a review comment via `mcp__gitea__pull_request_review_write`
- If APPROVE: approve the PR
- If REQUEST_CHANGES: request changes with the critical/major findings listed

View File

@@ -20,6 +20,7 @@ An implementation plan MUST exist at `implementation-plans/issue-$ARGUMENTS.md`
### 1. Read the Plan and Context
Read these files:
- `implementation-plans/issue-$ARGUMENTS.md` — the implementation plan
- `CLAUDE.md` — coding standards (if it exists)
- `package.json` — project dependencies and scripts
@@ -64,6 +65,7 @@ Follow the plan's implementation steps in order:
### 6. Log Deviations
If you deviate from the plan (different approach, additional files, skipped steps), document each deviation in the plan's **Deviation Log** section with:
- What changed
- Why it changed
@@ -81,6 +83,7 @@ Adapt commands based on what's available in `package.json`. Fix any failures bef
### 8. Commit
Stage all changed files and commit with a descriptive message:
```
feat: <short description of what was implemented> (issue #$ARGUMENTS)
```
@@ -90,6 +93,7 @@ Use conventional commit prefixes: `feat:`, `fix:`, `chore:`, `refactor:`, `test:
### 9. Output Summary
Display:
- Files created and modified (with counts)
- Tests added (count)
- Deviations from plan (if any)

View File

@@ -20,6 +20,7 @@ Use `mcp__gitea__issue_read` to get the full issue (title, body, labels, milesto
### 2. Read Project Context
Read these files to understand the project:
- `CLAUDE.md` — coding standards and workflow (if it exists)
- `package.json` — project dependencies and scripts
- `implementation-plans/_index.md` — existing plans index (if it exists)
@@ -27,6 +28,7 @@ Read these files to understand the project:
### 3. Determine Technology Stack
From the project files, determine:
- **Framework:** React, Vue, Svelte, etc. (check package.json)
- **Language:** TypeScript or JavaScript
- **Build tool:** Vite, Next.js, Webpack, etc.
@@ -37,6 +39,7 @@ From the project files, determine:
### 4. Find Related Plans
From the index (if it exists), identify plans that share:
- The same feature area or component
- Overlapping affected files
- Dependency relationships (blocked-by / blocks)
@@ -46,6 +49,7 @@ Read those related plan files to understand prior decisions and patterns.
### 5. Explore the Codebase
Based on the issue's scope, explore relevant code:
- Use Glob to find files in affected directories
- Use Grep to find existing patterns, interfaces, types, and components
- Use Read to examine specific files mentioned in the issue or related plans
@@ -56,6 +60,7 @@ Based on the issue's scope, explore relevant code:
The plan MUST include:
**Metadata:**
- Issue link, number, title
- Milestone and labels
- Status: `PLANNED`
@@ -64,9 +69,11 @@ The plan MUST include:
- Blocked-by references
**Acceptance Criteria:**
- Copy directly from the issue body
**Architecture Analysis:**
- Which components/pages are affected
- Which API endpoints are involved
- Which state/stores are affected
@@ -74,6 +81,7 @@ The plan MUST include:
- Existing patterns to follow (with file references)
**Implementation Steps (phase by phase):**
1. **Types & Configuration** — TypeScript types/interfaces, config constants, API types
2. **Core Logic** — Business logic, hooks, utilities, state management
3. **Components** — UI components, layouts, pages
@@ -81,9 +89,11 @@ The plan MUST include:
5. **Tests** — Unit tests, component tests, E2E tests
**Files to Create/Modify:**
- Explicit file paths with a one-line purpose for each
**Risks and Edge Cases:**
- Potential issues and mitigation strategies
**Important:** Include type definitions, component signatures, and hook interfaces in the plan, but do NOT write actual implementation code.
@@ -95,6 +105,7 @@ Write the plan to `implementation-plans/issue-$ARGUMENTS.md`.
### 8. Update the Index
Create or update `implementation-plans/_index.md`:
- Add the new plan to the master table
- Add cross-references in the appropriate feature area section

View File

@@ -10,6 +10,7 @@ You are the **Refactoring Reviewer** agent. Your job is to review the entire pro
## Trigger
This command is triggered:
- Periodically (every ~5 completed stories) by the auto-dev pipeline
- Manually by the user via `/project:refactor-review`
@@ -24,6 +25,7 @@ This command is triggered:
### 2. Survey the Codebase
Explore all source directories:
- Use Glob to find all source files (`src/**/*.ts`, `src/**/*.tsx`, `src/**/*.css`, `src/**/*.vue`, `src/**/*.svelte`, etc.)
- Use Grep to find patterns of concern (see checklist below)
- Read key files to understand current state
@@ -33,17 +35,20 @@ Explore all source directories:
Evaluate the project against these dimensions:
**Code Duplication:**
- Shared logic duplicated across components instead of extracted to hooks/utilities
- Similar UI patterns that should be abstracted into shared components
- Repeated API call patterns that should use a shared data fetching layer
**Modularity:**
- Components longer than ~100 lines that should be split
- Components with too many responsibilities
- Tight coupling between feature modules
- Missing abstractions
**Consistency:**
- Inconsistent error handling patterns
- Inconsistent state management approaches
- Inconsistent API call patterns
@@ -51,21 +56,25 @@ Evaluate the project against these dimensions:
- Inconsistent styling approaches
**Architecture Drift:**
- Components bypassing the established API layer
- State management inconsistencies
- Routing pattern violations
**Dependency Health:**
- Unused dependencies in package.json
- Outdated dependencies with known vulnerabilities
- Lock file hygiene
**Test Quality:**
- Tests that only test happy paths
- Missing component tests for interactive features
- Test code duplication
**Accessibility:**
- Missing ARIA attributes on interactive elements
- Missing keyboard navigation
- Missing alt text on images
@@ -74,11 +83,11 @@ Evaluate the project against these dimensions:
Categorize each finding:
| Priority | Description |
|---|---|
| **High** | Architecture drift, security concern, significant duplication, accessibility blockers |
| **Medium** | Modularity issues, inconsistencies, test quality gaps |
| **Low** | Style issues, minor duplication, documentation gaps |
| Priority | Description |
| ---------- | ------------------------------------------------------------------------------------- |
| **High** | Architecture drift, security concern, significant duplication, accessibility blockers |
| **Medium** | Modularity issues, inconsistencies, test quality gaps |
| **Low** | Style issues, minor duplication, documentation gaps |
### 5. Create Refactoring Issues

View File

@@ -55,6 +55,7 @@ git checkout -b release/<milestone-slug> main
### 4. Create Release PR
Push the release branch and create a Gitea PR:
- **Title:** `Release: <milestone-name>`
- **Head:** `release/<milestone-slug>`
- **Base:** `main`
@@ -79,6 +80,7 @@ If **manually requested:** Proceed to merge.
### 6. Create Gitea Release
Use `mcp__gitea__create_release`:
- **tag_name:** `<milestone-slug>`
- **target:** `main`
- **title:** `<milestone-name>`
@@ -91,6 +93,7 @@ Use `mcp__gitea__milestone_write` to set the milestone state to `closed`.
### 8. Output Summary
Display:
- Milestone name and version
- PR number and merge status
- Tag created

View File

@@ -16,6 +16,7 @@ Use `mcp__gitea__list_issues` to fetch all open issues. Paginate with `perPage:
### 2. Filter Out Ineligible Issues
Remove any issue that has:
- Label `workflow:manual`
- Label `workflow:blocked`
@@ -32,10 +33,12 @@ For each candidate issue, read its body and look for a "Blocked by" section. If
Sort remaining issues using this priority order:
**Milestone priority (earliest milestone first):**
- Sort by milestone due date (earliest first)
- Issues with no milestone come last
**Within the same milestone, sort by priority label:**
1. `priority:critical`
2. `priority:high`
3. `priority:medium`
@@ -45,6 +48,7 @@ Sort remaining issues using this priority order:
### 6. Present the Top Candidate
Display the highest-priority issue with:
- Issue number and title
- Milestone name
- All labels

View File

@@ -27,26 +27,31 @@ Check `package.json` and the plan to know which quality gates to run.
Run each gate and record pass/fail:
Gate 1 — Build:
```bash
npm run build
```
Gate 2 — Lint:
```bash
npm run lint
```
Gate 3 — Type Check:
```bash
npm run typecheck # or npx tsc --noEmit
```
Gate 4 — Tests:
```bash
npm run test
```
Gate 5 — Format (if available):
```bash
npm run format:check # or npx prettier --check .
```
@@ -58,23 +63,27 @@ Adapt commands based on what's available in `package.json`.
Review all files changed in this branch (use `git diff main --name-only` to get the list). For each changed file, verify:
**General:**
- [ ] No hardcoded credentials, API keys, or secrets
- [ ] No `TODO` or `FIXME` left unresolved (unless documented in plan)
- [ ] Consistent error handling patterns
- [ ] No `console.log` left in production code
**TypeScript:**
- [ ] No `any` types without justification
- [ ] Proper type narrowing and null checks
- [ ] Interfaces/types exported where needed
**Components:**
- [ ] Proper prop typing
- [ ] Loading, error, and empty states handled
- [ ] Accessible markup (semantic HTML, ARIA)
- [ ] Responsive design considered
**Security:**
- [ ] No XSS vulnerabilities
- [ ] User input properly sanitized
- [ ] API tokens/secrets not in client-side code
@@ -82,6 +91,7 @@ Review all files changed in this branch (use `git diff main --name-only` to get
### 5. Acceptance Criteria Verification
For each acceptance criterion from the issue:
- Check the code to verify the criterion is met
- Note which file(s) satisfy each criterion
- Mark each criterion as PASS or FAIL with explanation
@@ -89,6 +99,7 @@ For each acceptance criterion from the issue:
### 6. Determine Result
**PASS** if ALL of the following are true:
- All quality gates pass
- No architecture violations found (major/critical)
- All acceptance criteria are met

151
.gitignore vendored
View File

@@ -1,138 +1,23 @@
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
node_modules
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# OS
.DS_Store
Thumbs.db
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
# Env
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "proto/upstream"]
path = proto/upstream
url = https://git.shahondin1624.de/llm-multiverse/llm-multiverse.git

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

11
.prettierignore Normal file
View File

@@ -0,0 +1,11 @@
# Package
node_modules
package-lock.json
# Build output
build
.svelte-kit
dist
# Generated
src/lib/proto

15
.prettierrc Normal file
View File

@@ -0,0 +1,15 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

84
Caddyfile Normal file
View File

@@ -0,0 +1,84 @@
# Caddy v2 reverse proxy for llm-multiverse-ui
#
# Handles:
# 1. gRPC-Web → gRPC translation for browser clients
# 2. CORS headers for cross-origin gRPC-Web requests
# 3. SvelteKit dev server proxy (local development)
#
# Usage:
# caddy run --config Caddyfile
#
# Environment variables:
# ORCHESTRATOR_HOST — gRPC backend (default: localhost:50058)
# UI_HOST — SvelteKit dev server (default: localhost:5173)
# DOMAIN — domain for TLS (default: localhost)
# TLS_MODE — "internal" for self-signed, omit for Let's Encrypt
{
admin off
servers {
protocols h1 h2 h2c
}
}
{$DOMAIN:localhost} {
tls {$TLS_MODE:internal}
# Health check
handle /healthz {
respond "OK" 200
}
# gRPC-Web and gRPC traffic to Orchestrator
# Match any request with gRPC or gRPC-Web content types
@grpc_web {
header Content-Type application/grpc-web*
}
@grpc {
protocol grpc
}
# CORS preflight for gRPC-Web
@cors_preflight {
method OPTIONS
header Access-Control-Request-Method *
}
handle @cors_preflight {
header Access-Control-Allow-Origin "{$CORS_ORIGIN:*}"
header Access-Control-Allow-Methods "POST, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, X-Grpc-Web, X-User-Agent, Grpc-Timeout"
header Access-Control-Max-Age "86400"
respond "" 204
}
# gRPC-Web requests → Orchestrator with CORS headers
handle @grpc_web {
header Access-Control-Allow-Origin "{$CORS_ORIGIN:*}"
header Access-Control-Expose-Headers "Grpc-Status, Grpc-Message, Grpc-Status-Details-Bin"
reverse_proxy {$ORCHESTRATOR_HOST:localhost:50058} {
transport http {
versions h2c
}
}
}
# Native gRPC requests → Orchestrator
handle @grpc {
reverse_proxy {$ORCHESTRATOR_HOST:localhost:50058} {
transport http {
versions h2c
}
}
}
# All other traffic → SvelteKit dev server
handle {
reverse_proxy {$UI_HOST:localhost:5173}
}
log {
output stdout
format json
}
}
}

View File

@@ -1,3 +1,97 @@
# llm-multiverse-ui
Web frontend for the llm-multiverse orchestration system
Web frontend for the **llm-multiverse** orchestration system. Built with SvelteKit 5, TypeScript, Tailwind CSS v4, and gRPC-Web (Connect).
## Prerequisites
- Node.js 20+
- A running llm-multiverse gRPC backend (the UI proxies requests through Vite/your production reverse proxy)
## Getting Started
```bash
# Install dependencies
npm install
# Generate protobuf TypeScript definitions (only needed after .proto changes)
npm run generate
# Start the dev server
npm run dev
```
The app starts at `http://localhost:5173` by default.
## Scripts
| Command | Description |
| ------------------- | ------------------------------------------------ |
| `npm run dev` | Start Vite dev server with HMR |
| `npm run build` | Production build |
| `npm run preview` | Preview the production build locally |
| `npm run check` | Run svelte-check for TypeScript/Svelte errors |
| `npm run lint` | Run ESLint |
| `npm run format` | Format code with Prettier |
| `npm run generate` | Regenerate protobuf TypeScript from `.proto` files |
## Using the UI
### Landing Page
The root page (`/`) links to every section of the app.
### Chat (`/chat`)
The main interface for interacting with the orchestrator.
- **Sessions** are listed in the left sidebar. Click **+ New Chat** to start a session, or select an existing one. On mobile, tap the hamburger menu to open the sidebar.
- Type a message and press **Enter** (or the **Send** button) to send it. While the orchestrator is processing, a progress indicator and thinking section show the current state.
- **Session Config** (gear icon / "Config" button) opens a right sidebar where you can adjust the override level, disable specific tools, grant permissions, and save/load presets.
- If a request fails, an error banner appears with a **Retry** button.
- Navigation links to Lineage, Memory, and Audit are in the header (hidden on small screens).
### Agent Lineage (`/lineage`)
Visualizes the agent spawn tree as an interactive SVG diagram.
- Click a node to view agent details (ID, type, depth, children) in a side panel.
- A legend at the bottom shows agent type colors.
- Supports dark mode with theme-aware SVG colors.
### Memory Candidates (`/memory`)
Lists memory candidates captured during orchestration, grouped by session.
- **Source filter** — filter by Tool Output, Model Knowledge, or Web.
- **Confidence slider** — set a minimum confidence threshold.
- Each card shows the candidate content, source badge, and a confidence bar.
### Audit Log (`/audit`)
Timeline view of orchestration events for each session.
- Select a session from the dropdown, then optionally filter by event type (State Change, Tool, Error, Message).
- Events are displayed chronologically with date separators and color-coded badges.
### Theme
Use the theme toggle (sun/moon icon in any header) to cycle between **System**, **Light**, and **Dark** modes. The preference is persisted in localStorage.
## Project Structure
```
src/
lib/
components/ Svelte components (Backdrop, PageHeader, MessageInput, ...)
composables/ Reactive composables (useOrchestration)
proto/ Generated protobuf TypeScript
services/ gRPC client (orchestrator)
stores/ Svelte 5 reactive stores (sessions, audit, memory, theme, ...)
types/ TypeScript types and helpers (lineage, resultSource)
utils/ Shared utilities (date formatting, session config)
routes/
chat/ Chat page
lineage/ Agent lineage visualization
memory/ Memory candidates browser
audit/ Audit log timeline
```

8
buf.gen.yaml Normal file
View File

@@ -0,0 +1,8 @@
version: v2
plugins:
- local: protoc-gen-es
out: src/lib/proto
opt:
- target=ts
inputs:
- directory: proto/upstream/proto

27
eslint.config.js Normal file
View File

@@ -0,0 +1,27 @@
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import prettier from 'eslint-config-prettier';
import ts from 'typescript-eslint';
import globals from 'globals';
export default ts.config(
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
prettier,
...svelte.configs.prettier,
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
languageOptions: {
globals: {
...globals.browser
},
parserOptions: {
parser: ts.parser
}
}
},
{
ignores: ['build/', '.svelte-kit/', 'dist/', 'src/lib/proto/']
}
);

View File

@@ -0,0 +1,25 @@
# Implementation Plans Index
| Issue | Title | Status | Plan |
| ----- | ------------------------------------------------------ | ------------ | ---------------------------- |
| #1 | Project scaffolding: SvelteKit + Tailwind + TypeScript | COMPLETED | [issue-001.md](issue-001.md) |
| #2 | Proto codegen pipeline for TypeScript gRPC-Web stubs | COMPLETED | [issue-002.md](issue-002.md) |
| #3 | Configure Caddy for gRPC-Web support | COMPLETED | [issue-003.md](issue-003.md) |
| #4 | gRPC-Web client service layer | COMPLETED | [issue-004.md](issue-004.md) |
| #5 | Chat page layout and message list component | COMPLETED | [issue-005.md](issue-005.md) |
| #6 | Message input with send and keyboard shortcuts | COMPLETED | [issue-006.md](issue-006.md) |
| #7 | Streaming response rendering | COMPLETED | [issue-007.md](issue-007.md) |
| #8 | Orchestration state progress indicator | COMPLETED | [issue-008.md](issue-008.md) |
| #9 | Intermediate results display | COMPLETED | [issue-009.md](issue-009.md) |
| #10 | Final result rendering with artifacts | COMPLETED | [issue-010.md](issue-010.md) |
| #11 | Session creation and ID management | COMPLETED | [issue-011.md](issue-011.md) |
| #12 | Session history sidebar | COMPLETED | [issue-012.md](issue-012.md) |
| #13 | Session config sidebar component | COMPLETED | [issue-013.md](issue-013.md) |
| #14 | Preset configurations | COMPLETED | [issue-014.md](issue-014.md) |
| #15 | Agent lineage visualization | COMPLETED | [issue-015.md](issue-015.md) |
| #16 | Memory candidates viewer | COMPLETED | [issue-016.md](issue-016.md) |
| #17 | Audit/activity log view | COMPLETED | [issue-017.md](issue-017.md) |
| #18 | Dark/light theme toggle | COMPLETED | [issue-018.md](issue-018.md) |
| #19 | Responsive layout and mobile support | COMPLETED | [issue-019.md](issue-019.md) |
| #20 | Error handling and connection status | COMPLETED | [issue-020.md](issue-020.md) |
| #43 | Display inference statistics in chat UI | COMPLETED | [issue-043.md](issue-043.md) |

View File

@@ -0,0 +1,66 @@
# Issue #1: Project scaffolding: SvelteKit + Tailwind + TypeScript
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/1
**Branch:** `feature/issue-1-project-scaffolding`
## Summary
Initialize the SvelteKit project with Svelte 5 (runes mode), Tailwind CSS, TypeScript strict mode, proper directory structure, and ESLint + Prettier configuration.
## Acceptance Criteria
- [ ] SvelteKit project initialized with Svelte 5 (runes mode)
- [ ] Tailwind CSS configured and working
- [ ] TypeScript strict mode enabled
- [ ] Directory structure: `src/lib/`, `src/routes/`, `src/lib/components/`
- [ ] ESLint + Prettier configured
- [ ] `dev`, `build`, `preview`, `lint`, `format` scripts in package.json
## Implementation Phases
### Phase 1: SvelteKit + TypeScript Initialization
Use the SvelteKit scaffolding tool (`npx sv create`) to create the project in the current directory with:
- SvelteKit with Svelte 5
- TypeScript strict mode
- Vite as the build tool
Since we're in an existing git repo with just a README, we'll scaffold into a temp directory and move files over.
### Phase 2: Tailwind CSS Integration
Add Tailwind CSS v4 using the SvelteKit integration:
- Install `@tailwindcss/vite` and `tailwindcss`
- Configure the Vite plugin
- Add Tailwind CSS import to app.css
### Phase 3: Directory Structure
Create the required directory structure:
- `src/lib/` (SvelteKit default)
- `src/lib/components/` — reusable UI components
- `src/lib/services/` — API/service layer (for future issues)
- `src/routes/` (SvelteKit default)
### Phase 4: ESLint + Prettier
Configure ESLint and Prettier using the SvelteKit recommended setup:
- `eslint` + `@eslint/js` + `eslint-plugin-svelte` + `typescript-eslint`
- `prettier` + `prettier-plugin-svelte`
- Add `lint` and `format` scripts to package.json
### Phase 5: Verify
- Run `dev` server to confirm it starts
- Run `build` to confirm production build works
- Run `lint` and `format` to confirm tooling works
- Verify TypeScript strict mode is active
## Deviations
(none yet)

View File

@@ -0,0 +1,49 @@
---
---
# Issue #2: Proto codegen pipeline for TypeScript gRPC-Web stubs
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/2
**Branch:** `feature/issue-2-proto-codegen`
## Summary
Set up buf + @connectrpc/connect-web to generate TypeScript client stubs from the llm-multiverse proto files. Use a git submodule to consume proto files. Output generated code to `src/lib/proto/`.
## Acceptance Criteria
- [ ] Codegen toolchain chosen and configured (buf + connect-es)
- [ ] Proto files from llm-multiverse repo consumed (git submodule)
- [ ] TypeScript gRPC-Web client stubs generated to `src/lib/proto/`
- [ ] `generate` npm script added to package.json
- [ ] Generated types are importable and type-safe
## Implementation Phases
### Phase 1: Add proto files via git submodule
Add the `llm-multiverse` repo as a git submodule at `proto/upstream` to keep proto files in sync.
### Phase 2: Configure buf for connect-es codegen
Install buf CLI dependencies and configure:
- `@bufbuild/protobuf` — runtime protobuf library
- `@connectrpc/connect` — Connect protocol runtime
- `@connectrpc/connect-web` — browser transport
- `@bufbuild/protoc-gen-es` — protobuf ES codegen plugin
- `@connectrpc/protoc-gen-connect-es` — Connect service codegen plugin
Create `buf.gen.yaml` in project root targeting `src/lib/proto/` output.
### Phase 3: Generate stubs and verify
Run `npx buf generate` and verify generated TypeScript files are importable and type-safe.
### Phase 4: Add npm script
Add `"generate": "buf generate proto/upstream/proto"` to package.json.
## Deviations
(none yet)

View File

@@ -0,0 +1,34 @@
---
---
# Issue #3: Configure Caddy for gRPC-Web support
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/3
**Branch:** `feature/issue-3-caddy-grpc-web`
## Summary
Create a Caddyfile that handles gRPC-Web translation, CORS headers, and proxies both gRPC-Web traffic to the Orchestrator and regular traffic to the SvelteKit dev server.
## Acceptance Criteria
- [x] Caddyfile with gRPC-Web reverse proxy configuration
- [x] CORS headers configured for local dev and production origins
- [x] Content-Type mapping for gRPC-Web ↔ gRPC translation
- [x] grpc_web directive enabled (via content-type matching + h2c transport)
- [x] Documented how to run Caddy alongside the orchestrator
- [x] Browser can successfully call the Orchestrator through Caddy
## Implementation
Single Caddyfile in the project root that:
1. Routes `application/grpc-web*` requests to the Orchestrator via h2c
2. Handles CORS preflight (OPTIONS) with configurable origin
3. Adds CORS response headers for gRPC-Web responses
4. Routes all other traffic to the SvelteKit dev server
5. Configurable via environment variables: ORCHESTRATOR_HOST, UI_HOST, DOMAIN, TLS_MODE, CORS_ORIGIN
## Deviations
- Caddy v2 doesn't have a built-in `grpc_web` directive. Instead, we match on Content-Type headers and use h2c transport, which is the standard Caddy approach for gRPC-Web proxying. The Connect protocol used by connect-es handles the gRPC-Web encoding/decoding on the client side.

View File

@@ -0,0 +1,25 @@
---
---
# Issue #4: gRPC-Web client service layer
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/4
**Branch:** `feature/issue-4-grpc-web-client`
## Summary
Create `src/lib/services/orchestrator.ts` wrapping Connect-Web client for the OrchestratorService, with typed `processRequest()` async generator and error mapping.
## Acceptance Criteria
- [x] `src/lib/services/orchestrator.ts` module created
- [x] Wraps generated gRPC-Web stubs with typed interface
- [x] Connection setup with configurable endpoint
- [x] Error mapping from gRPC status codes to application errors
- [x] `processRequest()` returns an async iterator of `ProcessRequestResponse`
- [x] Handles streaming responses correctly
## Deviations
None.

View File

@@ -0,0 +1,25 @@
---
---
# Issue #5: Chat page layout and message list component
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/5
**Branch:** `feature/issue-5-chat-layout`
## Summary
Build the `/chat` route with scrollable message list, user/assistant message bubbles, auto-scroll, and empty state. Uses Svelte 5 runes.
## Acceptance Criteria
- [x] `/chat` route created
- [x] Scrollable message list component
- [x] Distinct user and assistant message bubble styles
- [x] Auto-scroll to bottom on new messages
- [x] Svelte 5 runes used for reactive message state (`$state`, `$derived`)
- [x] Empty state shown when no messages
## Deviations
- Added `globals` package to ESLint config for browser globals in Svelte files (fixes `Element`/`HTMLDivElement` not defined errors).

View File

@@ -0,0 +1,17 @@
---
---
# Issue #6: Message input with send and keyboard shortcuts
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/6
**Branch:** `feature/issue-6-message-input`
## Acceptance Criteria
- [x] Text input/textarea component for composing messages
- [x] Send button triggers message submission
- [x] Enter key sends message
- [x] Shift+Enter inserts newline
- [x] Input and send button disabled while a response is streaming
- [x] Input auto-focuses on page load and after send

View File

@@ -0,0 +1,16 @@
---
---
# Issue #7: Streaming response rendering
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/7
**Branch:** `feature/issue-7-streaming-response`
## Acceptance Criteria
- [x] Chat UI connected to gRPC-Web client service from #4
- [x] Streaming responses rendered in real-time as chunks arrive
- [x] `message` field displayed progressively
- [x] Handles stream completion and errors gracefully
- [x] Loading indicator shown while waiting for first chunk

View File

@@ -0,0 +1,16 @@
---
---
# Issue #8: Orchestration state progress indicator
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/8
**Branch:** `feature/issue-8-progress-indicator`
## Acceptance Criteria
- [x] Stepper/progress bar component showing orchestration phases
- [x] Phases: Decomposing → Dispatching → Executing → Compacting → Complete
- [x] Updates in real-time as OrchestrationState changes in the stream
- [x] Current phase visually highlighted, completed phases marked
- [x] Smooth transitions between states

View File

@@ -0,0 +1,17 @@
---
---
# Issue #9: Intermediate results display
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/9
**Branch:** `feature/issue-9-intermediate-results`
## Acceptance Criteria
- [x] Collapsible "thinking" section component
- [x] Displays intermediate_result content when present in stream
- [x] Positioned below the orchestration progress indicator
- [x] Collapsed by default, expandable on click
- [x] Updates as new intermediate results arrive
- [x] Visually distinct from final results (muted amber styling)

View File

@@ -0,0 +1,18 @@
---
---
# Issue #10: Final result rendering with artifacts
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/10
**Branch:** `feature/issue-10-final-result`
## Acceptance Criteria
- [x] summary field rendered as the main response content
- [x] artifacts displayed as viewable items
- [x] result_quality shown as a badge (Verified/Inferred/Uncertain)
- [x] source shown as a badge (Tool Output/Model Knowledge/Web)
- [x] FAILED status styled with error/red treatment
- [x] PARTIAL status styled with warning/yellow treatment
- [x] Successful results styled with success/green treatment

View File

@@ -0,0 +1,17 @@
---
---
# Issue #11: Session creation and ID management
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/11
**Branch:** `feature/issue-11-session-management`
## Acceptance Criteria
- [x] UUID v4 session ID generated on new chat creation
- [x] Active session ID persisted in URL params (?session=<id>)
- [x] Session ID also stored in localStorage for recovery
- [x] "New Chat" button creates a fresh session
- [x] Switching sessions updates URL and loads correct message history
- [x] Session ID passed to ProcessRequest gRPC calls

View File

@@ -0,0 +1,17 @@
---
---
# Issue #12: Session history sidebar
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/12
**Branch:** `feature/issue-12-session-sidebar`
## Acceptance Criteria
- [x] Left sidebar component listing past sessions
- [x] Each entry shows timestamp and first-message preview
- [x] Click on a session loads its message history
- [x] Session message history persisted in localStorage
- [x] Sessions sorted by most recent first
- [x] Delete session option (with confirmation)

View File

@@ -0,0 +1,30 @@
---
---
# Issue #13: Session config sidebar component
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/13
**Branch:** `feature/issue-13-config-sidebar`
## Acceptance Criteria
- [x] Right sidebar component for session configuration
- [x] Override level selection (None / Relax / All) with radio-style buttons
- [x] Disabled tools toggle checkboxes using ToolType enum values
- [x] Granted permissions free-text input with add/remove
- [x] Toggle button in header with non-default config indicator dot
- [x] Reset button to restore defaults
- [x] Config passed to processRequest on send
## Implementation
### Components
- `ConfigSidebar.svelte` — right sidebar with override level, disabled tools, and granted permissions sections
- Updated `+page.svelte` — added config toggle button in header, sessionConfig state, and ConfigSidebar integration
### Key Decisions
- Used `@bufbuild/protobuf` `create()` with `SessionConfigSchema` for immutable config updates
- Config state lives in `+page.svelte` and is passed down as prop
- Non-default config indicator (amber dot) shown in both the header toggle button and sidebar header
- Disabled tools stored as string labels matching ToolType enum display names

View File

@@ -0,0 +1,35 @@
---
---
# Issue #14: Preset configurations
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/14
**Branch:** `feature/issue-14-preset-configs`
## Acceptance Criteria
- [x] Save current config as a named preset
- [x] Load preset from a list
- [x] Delete custom presets
- [x] 2-3 built-in default presets shipped:
- "Strict mode" — no overrides, restricted tools (FS Write, Run Shell, Run Code, Package Install disabled)
- "Research only" — limited tool set for read-only operations (Memory Write, FS Write, Run Code, Run Shell, Package Install disabled)
- "Full access" — all overrides relaxed (OverrideLevel.ALL), no tools disabled
- [x] Presets persisted in localStorage
- [x] Built-in presets cannot be deleted
## Implementation
### New Files
- `src/lib/stores/presets.svelte.ts` — preset store with built-in presets and localStorage persistence for custom presets
### Modified Files
- `src/lib/components/ConfigSidebar.svelte` — added preset selector section at the top with load, save, and delete functionality
### Key Decisions
- Built-in presets are hardcoded constants, not stored in localStorage
- Custom presets stored under `llm-multiverse-presets` localStorage key
- Overwriting a custom preset with the same name is allowed; overwriting built-in presets is not
- Preset config uses a plain interface (`PresetConfig`) rather than protobuf types for clean serialization
- Preset section placed at the top of the sidebar before Override Level, matching the issue instructions

View File

@@ -0,0 +1,39 @@
---
---
# Issue #15: Agent lineage visualization
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/15
**Branch:** `feature/issue-15-agent-lineage`
## Acceptance Criteria
- [x] Dedicated `/lineage` route
- [x] Tree/graph visualization of agent spawn chain
- [x] Each node displays: agent_id, agent_type, spawn_depth
- [x] Visual hierarchy reflecting parent-child agent relationships
- [x] Custom SVG-based rendering (no external graph library needed)
- [x] Updates as new agents appear in the stream (reactive via `$derived`)
- [x] Clickable nodes to show agent details
## Implementation
### New Files
- `src/lib/types/lineage.ts` — lineage types (`LineageNode`, `SimpleAgentIdentifier`), tree builder, color/label helpers, sample data
- `src/lib/components/LineageTree.svelte` — custom SVG tree visualization with horizontal layout, bezier edges, colored nodes by agent type, click selection
- `src/routes/lineage/+page.svelte` — dedicated lineage route with tree, legend, and detail panel
- `src/routes/lineage/+page.ts` — SSR disabled for this route
### Modified Files
- `src/routes/+page.svelte` — added navigation links to Chat and Agent Lineage
- `src/routes/chat/+page.svelte` — added Lineage link in header alongside Config button
### Key Decisions
- Uses sample/demo data since the API does not yet expose lineage information (AuditService is write-only, ProcessRequestResponse does not include lineage)
- `SimpleAgentIdentifier` type decouples the UI from protobuf Message dependency, making it easy to adapt when real data arrives
- Horizontal tree layout: root on left, children branching right, computed via recursive leaf-counting algorithm
- SVG bezier curves for edges, rounded rectangles for nodes
- Agent type colors: Orchestrator=blue, Researcher=green, Coder=purple, SysAdmin=orange, Assistant=teal, Unspecified=gray
- Detail panel slides in from right when a node is clicked, showing full agent info and child list
- Tree layout is fully reactive via `$derived` runes — updating the `agents` array recomputes the tree automatically

View File

@@ -0,0 +1,37 @@
---
---
# Issue #16: Memory candidates viewer
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/16
**Branch:** `feature/issue-16-memory-viewer`
## Acceptance Criteria
- [x] Dedicated `/memory` route
- [x] List of `MemoryCandidate` entries
- [x] Each entry shows: content, source badge, confidence score
- [x] Entries grouped by session
- [x] Confidence score visualized (progress bar with color-coding)
- [x] Filterable by source or confidence threshold
## Implementation
### New Files
- `src/lib/stores/memory.svelte.ts` — reactive store for memory candidates grouped by session ID, with localStorage persistence, sample/demo data, and `addCandidates()` / `getAllBySession()` / `clearSession()` API
- `src/lib/components/MemoryCandidateCard.svelte` — card component displaying a single memory candidate with content, color-coded source badge (Tool Output=blue, Model Knowledge=purple, Web=green, Unspecified=gray), and horizontal confidence progress bar with percentage
- `src/routes/memory/+page.svelte` — dedicated memory route with session-grouped layout, source dropdown filter, confidence threshold slider, empty state, and back-to-chat link
- `src/routes/memory/+page.ts` — SSR disabled for this route
### Modified Files
- `src/routes/+page.svelte` — added Memory Candidates navigation link on the home page
- `src/routes/chat/+page.svelte` — added Memory link in the header next to Lineage; wired up capture of `finalResult.newMemoryCandidates` into the memory store
### Key Decisions
- Uses sample/demo data seeded into localStorage on first load, since the orchestrator doesn't currently include memory candidates in typical responses
- `StoredMemoryCandidate` type decouples the UI store from protobuf Message dependency (same pattern as lineage's `SimpleAgentIdentifier`)
- Confidence bar color-coded: green (>=80%), amber (>=50%), red (<50%)
- Source badge colors match the project's existing badge conventions (blue, purple, green, gray)
- Grid layout (1-3 columns responsive) for candidate cards within each session group
- Filter controls use a source dropdown and a range slider for confidence threshold

View File

@@ -0,0 +1,56 @@
# Issue #17 — Audit/Activity Log View
**Status: COMPLETED**
## Overview
Timeline view of all orchestration events for a session — state transitions, tool invocations, errors. Populated from the streamed responses captured during the session.
## Implementation Steps
### 1. Audit Event Store (`src/lib/stores/audit.svelte.ts`) — COMPLETED
- Created `AuditEvent` type with `id`, `sessionId`, `timestamp`, `eventType`, `details`, `state?`
- Event types: `state_change`, `tool_invocation`, `error`, `message`
- Store events grouped by session using `SvelteMap`, persisted to `localStorage`
- Functions: `addEvent()`, `getEventsBySession()`, `getAllSessions()`, `clearSession()`
- Sample data seeded for demo sessions
### 2. AuditTimeline Component (`src/lib/components/AuditTimeline.svelte`) — COMPLETED
- Vertical timeline with left-aligned time markers and event cards
- Color-coded type badges: state change=blue, tool=green, error=red, message=gray
- Each event shows timestamp, type badge, state label (when applicable), and detail text
- Date headers when events span multiple days
- Hover shadow effect on event cards
### 3. Audit Route (`src/routes/audit/+page.svelte`) — COMPLETED
- Dedicated `/audit` route with session selector dropdown and timeline
- Filter controls for event type (all, state changes, tool invocations, errors, messages)
- URL query param `?session=id` for deep linking to specific session
- Empty states when no events or no session selected
- Header with back-to-chat link and "Sample Data" badge
### 4. Audit Route Config (`src/routes/audit/+page.ts`) — COMPLETED
- `export const prerender = false` and `export const ssr = false`
### 5. Homepage Navigation (`src/routes/+page.svelte`) — COMPLETED
- Added "Audit Log" navigation link alongside existing Chat, Lineage, Memory links
### 6. Chat Page Integration (`src/routes/chat/+page.svelte`) — COMPLETED
- Added "Audit" link in the header navigation bar
- Capture orchestration state changes during streaming into the audit store
- Capture errors into the audit store
## Files Changed
- `src/lib/stores/audit.svelte.ts` (new)
- `src/lib/components/AuditTimeline.svelte` (new)
- `src/routes/audit/+page.svelte` (new)
- `src/routes/audit/+page.ts` (new)
- `src/routes/+page.svelte` (modified)
- `src/routes/chat/+page.svelte` (modified)

View File

@@ -0,0 +1,68 @@
# Issue #18: Dark/Light Theme Toggle
## Status: COMPLETED
## Overview
Add a dark/light theme toggle to the UI with three modes (system, light, dark), persisted in localStorage, defaulting to system preference.
## Implementation Details
### 1. Tailwind v4 Dark Mode Configuration
- Updated `src/app.css` with `@variant dark (&:where(.dark, .dark *));` for class-based dark mode
### 2. Theme Store (`src/lib/stores/theme.svelte.ts`)
- Svelte 5 runes-based store with three modes: 'light', 'dark', 'system'
- Loads preference from localStorage on init, defaults to 'system'
- Listens to `prefers-color-scheme` media query when in 'system' mode
- Applies/removes `dark` class on `document.documentElement`
- Adds temporary `theme-transition` class for smooth CSS transitions
### 3. ThemeToggle Component (`src/lib/components/ThemeToggle.svelte`)
- Compact button that cycles: system -> light -> dark -> system
- SVG icons: monitor (system), sun (light), moon (dark)
- Text label showing current mode
- Dark mode aware styling
### 4. Layout Integration
- Theme store initialized via `$effect` in `+layout.svelte`
- Global CSS transition styles for smooth theme switching
- ThemeToggle added to all page headers (chat, lineage, memory, audit, home)
### 5. Dark Mode Classes Applied To
- `src/routes/+page.svelte` (home page)
- `src/routes/chat/+page.svelte` (chat page)
- `src/routes/lineage/+page.svelte` (lineage page)
- `src/routes/memory/+page.svelte` (memory page)
- `src/routes/audit/+page.svelte` (audit page)
- `src/lib/components/SessionSidebar.svelte`
- `src/lib/components/ConfigSidebar.svelte`
- `src/lib/components/MessageBubble.svelte`
- `src/lib/components/MessageList.svelte`
- `src/lib/components/MessageInput.svelte`
- `src/lib/components/OrchestrationProgress.svelte`
- `src/lib/components/ThinkingSection.svelte`
- `src/lib/components/FinalResult.svelte`
- `src/lib/components/LineageTree.svelte`
- `src/lib/components/MemoryCandidateCard.svelte`
- `src/lib/components/AuditTimeline.svelte`
### Color Mapping Applied
- `bg-white` -> `dark:bg-gray-900`
- `bg-gray-50` -> `dark:bg-gray-800`
- `bg-gray-100` -> `dark:bg-gray-700`
- `bg-gray-200` -> `dark:bg-gray-600`/`dark:bg-gray-700`
- `text-gray-900` -> `dark:text-gray-100`
- `text-gray-700` -> `dark:text-gray-300`
- `text-gray-500` -> `dark:text-gray-400`
- `text-gray-400` -> `dark:text-gray-500`
- `border-gray-200` -> `dark:border-gray-700`
- `border-gray-300` -> `dark:border-gray-600`
- Colored backgrounds (blue, green, amber, red, purple) use opacity-based dark variants (e.g., `dark:bg-blue-900/40`)
- SVG elements in LineageTree use reactive color values based on `themeStore.isDark`
## Files Changed
- `src/app.css` - Added dark mode variant configuration
- `src/lib/stores/theme.svelte.ts` - New theme store
- `src/lib/components/ThemeToggle.svelte` - New toggle component
- `src/routes/+layout.svelte` - Theme init and transition styles
- All page and component files listed above - Added dark: variants

View File

@@ -0,0 +1,80 @@
# Issue #19: Responsive layout and mobile support
**Status: COMPLETED**
## Summary
Make the dashboard fully usable on mobile with collapsible sidebars, stacked layouts for small screens, touch-friendly tap targets, and no horizontal scrolling.
## Changes
### Chat Page (`src/routes/chat/+page.svelte`)
- Added hamburger menu button visible only on mobile (`md:hidden`) to toggle the session sidebar
- SessionSidebar rendered twice: always-visible wrapper for desktop (`hidden md:flex`) and a mobile drawer controlled by `showSessionSidebar` state
- Config sidebar now passes `onClose` callback to enable closing on mobile
- Nav links (Lineage, Memory, Audit) hidden on small screens (`hidden sm:inline-flex`)
- Config button shows gear icon on small screens, text label on larger screens
- Added `overflow-hidden` to root container to prevent horizontal scrolling
- All header padding reduced on mobile (`px-3` vs `md:px-4`)
### SessionSidebar (`src/lib/components/SessionSidebar.svelte`)
- Accepts new `open` and `onClose` props for mobile drawer control
- On mobile: renders as fixed overlay (`fixed inset-y-0 left-0 z-50`) with slide-in transition
- On desktop: renders as relative-positioned sidebar with no transition (`md:relative md:translate-x-0`)
- Backdrop overlay (`bg-black/50`) shown on mobile when sidebar is open
- Close button visible only on mobile (`md:hidden`)
- Session selection and new chat actions auto-close the sidebar on mobile
### ConfigSidebar (`src/lib/components/ConfigSidebar.svelte`)
- Accepts new optional `onClose` prop
- On mobile: renders as fixed overlay from the right (`fixed inset-y-0 right-0 z-50`)
- On desktop: renders as relative-positioned sidebar (`md:relative md:z-auto`)
- Backdrop overlay shown when `onClose` is provided (mobile only)
- Close button visible only on mobile
### MessageInput (`src/lib/components/MessageInput.svelte`)
- Send button has `min-h-[44px] min-w-[44px]` for touch-friendly 44px tap target
- Textarea uses `text-base` on mobile (prevents iOS zoom) and `md:text-sm` on desktop
- Reduced padding on mobile (`p-3` vs `md:p-4`)
### MessageBubble (`src/lib/components/MessageBubble.svelte`)
- Increased max-width on mobile from 75% to 85% (`max-w-[85%] md:max-w-[75%]`)
- Slightly reduced padding on mobile
### Home Page (`src/routes/+page.svelte`)
- Nav cards stack vertically on mobile (`flex-col sm:flex-row`)
- Full-width centered layout with `max-w-md` constraint on mobile
- Larger touch targets for nav links (`py-3 sm:py-2`)
- Added horizontal padding to prevent edge clipping
### Lineage Page (`src/routes/lineage/+page.svelte`)
- Content area stacks vertically on mobile (`flex-col md:flex-row`)
- Detail panel renders as fixed overlay on mobile with backdrop
- Reduced padding on mobile
- "Sample Data" badge hidden on very small screens
### Memory Page (`src/routes/memory/+page.svelte`)
- Filters stack vertically on mobile (`flex-col sm:flex-row`)
- Reduced padding on mobile
- "Sample Data" badge hidden on very small screens
### Audit Page (`src/routes/audit/+page.svelte`)
- Filters stack vertically on mobile (`flex-col sm:flex-row`)
- Session select constrained to prevent horizontal overflow (`min-w-0 max-w-full truncate`)
- Reduced padding on mobile
- "Sample Data" badge hidden on very small screens
## Acceptance Criteria Met
- [x] Sidebars collapsible on mobile (hamburger menu for sessions, tap to close)
- [x] Stacked layout for screens < 768px (md breakpoint = 768px)
- [x] Chat input remains accessible and usable on mobile (always at bottom, full width)
- [x] Touch-friendly tap targets (min 44px on send button, hamburger, config toggle)
- [x] No horizontal scrolling on any viewport (overflow-hidden on containers)
- [x] Works on common mobile breakpoints (320px, 375px, 414px) via responsive Tailwind classes
## Quality Gates
- `npm run build` - PASSED
- `npm run lint` - PASSED (0 errors)
- `npm run check` - PASSED (0 errors, 0 warnings)

View File

@@ -0,0 +1,44 @@
# Issue #20 — Error handling and connection status
**Status: COMPLETED**
## Overview
Global error boundary, toast notifications for gRPC errors, connection status indicator, and retry logic with exponential backoff.
## Implemented Components
### Toast System
- **`src/lib/stores/toast.svelte.ts`** — Svelte 5 runes-based toast store with `addToast`, `removeToast`, `clear`. Auto-dismiss with configurable durations (5s for success/info, 10s for errors, 8s for warnings).
- **`src/lib/components/ToastContainer.svelte`** — Fixed bottom-right toast stack. Color-coded by type (red/amber/green/blue) with icons, dismiss buttons, and full dark mode support.
### Connection Status
- **`src/lib/stores/connection.svelte.ts`** — Tracks gRPC connection state (`connected` / `reconnecting` / `disconnected`) based on consecutive failures.
- **`src/lib/components/ConnectionStatus.svelte`** — Header indicator with colored dot (green/amber-pulse/red) and label. Dark mode support.
### Retry Logic
- **`src/lib/services/orchestrator.ts`** — Added:
- Exponential backoff retry (up to 3 retries) for transient gRPC codes (`UNAVAILABLE`, `DEADLINE_EXCEEDED`, `ABORTED`, `INTERNAL`).
- `friendlyMessage()` mapper from gRPC codes to user-friendly strings.
- Connection store integration (reports success/failure).
- Toast notifications on errors.
### Error Boundary
- **`src/routes/+error.svelte`** — Global SvelteKit error page with status code, friendly message, and "Go back" / "Try again" / "Home" buttons. Full dark mode support.
### Integration
- **`src/routes/+layout.svelte`** — Added `ToastContainer` to global layout.
- **`src/routes/chat/+page.svelte`** — Added `ConnectionStatus` in header, "Retry" button on failed requests, toast notifications on errors, user-friendly error messages.
## Files Changed
| File | Change |
|------|--------|
| `src/lib/stores/toast.svelte.ts` | New |
| `src/lib/stores/connection.svelte.ts` | New |
| `src/lib/components/ToastContainer.svelte` | New |
| `src/lib/components/ConnectionStatus.svelte` | New |
| `src/routes/+error.svelte` | New |
| `src/lib/services/orchestrator.ts` | Modified — retry, friendly messages, store integration |
| `src/routes/+layout.svelte` | Modified — added ToastContainer |
| `src/routes/chat/+page.svelte` | Modified — ConnectionStatus, Retry button, toast integration |
| `implementation-plans/issue-020.md` | New |
| `implementation-plans/_index.md` | Modified |

View File

@@ -0,0 +1,60 @@
# Issue #43: Display Inference Statistics in Chat UI
**Status:** COMPLETED
**Issue:** [#43](https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/43)
**Branch:** `feature/issue-43-inference-stats`
## Overview
Add a collapsible UI panel that displays LLM inference statistics (token counts, context window utilization, throughput) below assistant messages after orchestration completes.
## Phases
### Phase 1: Proto Types
**Files:**
- `proto/upstream/proto/llm_multiverse/v1/common.proto` — Add `InferenceStats` message
- `proto/upstream/proto/llm_multiverse/v1/orchestrator.proto` — Add optional `inference_stats` field to `ProcessRequestResponse`
**InferenceStats message fields:**
- `prompt_tokens` (uint32) — tokens in the prompt
- `completion_tokens` (uint32) — tokens generated
- `total_tokens` (uint32) — sum of prompt + completion
- `context_window_size` (uint32) — model's maximum context length
- `tokens_per_second` (float) — generation throughput
**Then regenerate types:** `npm run generate`
### Phase 2: Orchestration State
**Files:**
- `src/lib/composables/useOrchestration.svelte.ts` — Extract `inferenceStats` from response, expose via store getter
### Phase 3: InferenceStatsPanel Component
**Files:**
- `src/lib/components/InferenceStatsPanel.svelte` — New component
**Design:**
- Follow `<details>` pattern from FinalResult.svelte
- Collapsed by default
- Summary line shows key stat (e.g., total tokens + tokens/sec)
- Expanded content shows all stats in a grid layout
- Context utilization shown as a progress bar
- Blue/indigo color scheme (neutral, info-like)
- Full dark mode support
### Phase 4: Chat Page Integration
**Files:**
- `src/routes/chat/+page.svelte` — Render `InferenceStatsPanel` after `FinalResult` when stats available
## Acceptance Criteria
- [x] InferenceStats proto message defined and TypeScript types generated
- [x] InferenceStatsPanel displays all required metrics
- [x] Panel is collapsible, collapsed by default
- [x] Context utilization shows visual progress bar
- [x] Integrates cleanly into chat page below assistant message
- [x] Dark mode support
- [x] Build, lint, typecheck pass

4126
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

41
package.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "llm-multiverse-ui",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"generate": "buf generate",
"lint": "eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@bufbuild/buf": "^1.66.1",
"@bufbuild/protobuf": "^2.11.0",
"@bufbuild/protoc-gen-es": "^2.11.0",
"@connectrpc/connect": "^2.1.1",
"@connectrpc/connect-web": "^2.1.1",
"@eslint/js": "^9.0.0",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/vite": "^4.0.0",
"eslint": "^9.0.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^17.4.0",
"prettier": "^3.0.0",
"prettier-plugin-svelte": "^3.0.0",
"svelte": "^5.51.0",
"svelte-check": "^4.4.2",
"tailwindcss": "^4.0.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.0.0",
"vite": "^7.3.1"
}
}

1
proto/upstream Submodule

Submodule proto/upstream added at aae98895d2

3
src/app.css Normal file
View File

@@ -0,0 +1,3 @@
@import 'tailwindcss';
@variant dark (&:where(.dark, .dark *));

13
src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

11
src/app.html Normal file
View File

@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="tap">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,92 @@
<script lang="ts">
import type { AuditEvent, AuditEventType } from '$lib/stores/audit.svelte';
import { formatShortDate } from '$lib/utils/date';
let { events }: { events: AuditEvent[] } = $props();
function formatTimestamp(date: Date): string {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
const eventTypeConfig: Record<AuditEventType, { badge: string; dot: string; label: string }> = {
state_change: {
badge: 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300',
dot: 'bg-blue-500',
label: 'State Change'
},
tool_invocation: {
badge: 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300',
dot: 'bg-green-500',
label: 'Tool'
},
error: {
badge: 'bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300',
dot: 'bg-red-500',
label: 'Error'
},
message: {
badge: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400',
dot: 'bg-gray-400',
label: 'Message'
}
};
const defaultEventConfig = { badge: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400', dot: 'bg-gray-400', label: 'Unknown' };
function getEventConfig(eventType: string) {
return eventTypeConfig[eventType as AuditEventType] ?? defaultEventConfig;
}
</script>
{#if events.length === 0}
<div class="flex flex-col items-center justify-center py-16 text-center">
<div class="mb-3 text-4xl text-gray-300 dark:text-gray-600">&#128196;</div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">No events for this session</p>
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">Events will appear here as orchestration runs.</p>
</div>
{:else}
<div class="relative ml-4">
<!-- Vertical line -->
<div class="absolute top-0 bottom-0 left-3 w-0.5 bg-gray-200 dark:bg-gray-700"></div>
<ol class="space-y-4">
{#each events as event, i (event.id)}
{@const showDate = i === 0 || formatShortDate(event.timestamp) !== formatShortDate(events[i - 1].timestamp)}
{@const config = getEventConfig(event.eventType)}
{#if showDate}
<li class="relative pl-10 pt-2">
<span class="text-xs font-medium text-gray-400 dark:text-gray-500">{formatShortDate(event.timestamp)}</span>
</li>
{/if}
<li class="group relative flex items-start pl-10">
<!-- Dot on the timeline -->
<div
class="absolute left-1.5 top-1.5 h-3 w-3 rounded-full ring-2 ring-white dark:ring-gray-900 {config.dot}"
></div>
<!-- Event card -->
<div
class="flex-1 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-4 py-3 shadow-sm transition-shadow group-hover:shadow-md"
>
<div class="flex flex-wrap items-center gap-2">
<span class="text-xs text-gray-400 dark:text-gray-500">{formatTimestamp(event.timestamp)}</span>
<span
class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium {config.badge}"
>
{config.label}
</span>
{#if event.state}
<span class="rounded-md bg-blue-50 dark:bg-blue-900/30 px-1.5 py-0.5 text-xs font-mono text-blue-600 dark:text-blue-400">
{event.state}
</span>
{/if}
</div>
<p class="mt-1.5 text-sm text-gray-700 dark:text-gray-300">{event.details}</p>
</div>
</li>
{/each}
</ol>
</div>
{/if}

View File

@@ -0,0 +1,12 @@
<script lang="ts">
let { onClose }: { onClose: () => void } = $props();
</script>
<div
class="fixed inset-0 z-40 bg-black/50 md:hidden"
role="button"
tabindex="-1"
onclick={onClose}
onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}
aria-label="Close overlay"
></div>

View File

@@ -0,0 +1,301 @@
<script lang="ts">
import { OverrideLevel, ToolType } from '$lib/proto/llm_multiverse/v1/common_pb';
import type { SessionConfig } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
import { SessionConfigSchema } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
import { create } from '@bufbuild/protobuf';
import { presetStore } from '$lib/stores/presets.svelte';
import { isNonDefaultConfig } from '$lib/utils/sessionConfig';
import Backdrop from '$lib/components/Backdrop.svelte';
let {
config,
onConfigChange,
onClose
}: {
config: SessionConfig;
onConfigChange: (config: SessionConfig) => void;
onClose?: () => void;
} = $props();
let newPermission = $state('');
let newPresetName = $state('');
let showSavePreset = $state(false);
let saveError = $state('');
const toolTypes = [
{ value: ToolType.MEMORY_READ, label: 'Memory Read' },
{ value: ToolType.MEMORY_WRITE, label: 'Memory Write' },
{ value: ToolType.WEB_SEARCH, label: 'Web Search' },
{ value: ToolType.FS_READ, label: 'FS Read' },
{ value: ToolType.FS_WRITE, label: 'FS Write' },
{ value: ToolType.RUN_CODE, label: 'Run Code' },
{ value: ToolType.RUN_SHELL, label: 'Run Shell' },
{ value: ToolType.PACKAGE_INSTALL, label: 'Package Install' }
];
const overrideLevels = [
{ value: OverrideLevel.NONE, label: 'None', description: 'Full enforcement' },
{ value: OverrideLevel.RELAX, label: 'Relax', description: 'High-risk tools unlocked' },
{ value: OverrideLevel.ALL, label: 'All', description: 'No enforcement' }
];
const isNonDefault = $derived(isNonDefaultConfig(config));
function setOverrideLevel(level: OverrideLevel) {
const updated = create(SessionConfigSchema, {
...config,
overrideLevel: level
});
onConfigChange(updated);
}
function toggleTool(tool: string) {
const disabled = [...config.disabledTools];
const idx = disabled.indexOf(tool);
if (idx >= 0) {
disabled.splice(idx, 1);
} else {
disabled.push(tool);
}
const updated = create(SessionConfigSchema, {
...config,
disabledTools: disabled
});
onConfigChange(updated);
}
function addPermission() {
const trimmed = newPermission.trim();
if (!trimmed || config.grantedPermissions.includes(trimmed)) return;
const updated = create(SessionConfigSchema, {
...config,
grantedPermissions: [...config.grantedPermissions, trimmed]
});
onConfigChange(updated);
newPermission = '';
}
function removePermission(perm: string) {
const updated = create(SessionConfigSchema, {
...config,
grantedPermissions: config.grantedPermissions.filter((p) => p !== perm)
});
onConfigChange(updated);
}
function resetConfig() {
onConfigChange(create(SessionConfigSchema, { overrideLevel: OverrideLevel.NONE }));
}
function handleLoadPreset(name: string) {
const presetConfig = presetStore.loadPreset(name);
if (!presetConfig) return;
const updated = create(SessionConfigSchema, {
overrideLevel: presetConfig.overrideLevel,
disabledTools: [...presetConfig.disabledTools],
grantedPermissions: [...presetConfig.grantedPermissions]
});
onConfigChange(updated);
}
function handleSavePreset() {
saveError = '';
try {
presetStore.savePreset(newPresetName, {
overrideLevel: config.overrideLevel,
disabledTools: [...config.disabledTools],
grantedPermissions: [...config.grantedPermissions]
});
newPresetName = '';
showSavePreset = false;
} catch (err) {
saveError = err instanceof Error ? err.message : 'Failed to save preset';
}
}
function handleDeletePreset(name: string) {
presetStore.deletePreset(name);
}
</script>
{#if onClose}
<Backdrop onClose={onClose} />
{/if}
<aside
class="flex h-full w-72 flex-col border-l border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900
fixed inset-y-0 right-0 z-50
md:relative md:z-auto"
>
<div class="flex items-center justify-between border-b border-gray-200 dark:border-gray-700 px-4 py-3">
<div class="flex items-center gap-2">
<h2 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Session Config</h2>
{#if isNonDefault}
<span class="h-2 w-2 rounded-full bg-amber-500" title="Non-default config active"></span>
{/if}
</div>
<div class="flex items-center gap-2">
<button
type="button"
onclick={resetConfig}
class="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
>
Reset
</button>
{#if onClose}
<button
type="button"
onclick={onClose}
class="flex h-7 w-7 items-center justify-center rounded text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300 md:hidden"
aria-label="Close config sidebar"
>
&#10005;
</button>
{/if}
</div>
</div>
<div class="flex-1 overflow-y-auto p-4 space-y-5">
<!-- Presets -->
<div>
<p class="mb-2 text-xs font-medium text-gray-700 dark:text-gray-300">Presets</p>
<div class="space-y-1">
{#each presetStore.getAllPresets() as preset (preset.name)}
<div class="flex items-center gap-1">
<button
type="button"
onclick={() => handleLoadPreset(preset.name)}
class="flex-1 rounded-lg px-3 py-1.5 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
>
{preset.name}
{#if preset.builtIn}
<span class="text-[10px] text-gray-400 dark:text-gray-500">built-in</span>
{/if}
</button>
{#if !preset.builtIn}
<button
type="button"
onclick={() => handleDeletePreset(preset.name)}
class="rounded px-1.5 py-1 text-xs text-gray-400 dark:text-gray-500 hover:text-red-500"
>
&#10005;
</button>
{/if}
</div>
{/each}
</div>
{#if showSavePreset}
<div class="mt-2 flex gap-1">
<input
type="text"
bind:value={newPresetName}
onkeydown={(e) => { if (e.key === 'Enter') handleSavePreset(); }}
placeholder="Preset name"
class="flex-1 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2 py-1.5 text-xs text-gray-900 dark:text-gray-100 focus:border-blue-500 focus:outline-none"
/>
<button
type="button"
onclick={handleSavePreset}
disabled={!newPresetName.trim()}
class="rounded-lg bg-blue-50 dark:bg-blue-900/40 px-2 py-1.5 text-xs font-medium text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/60 disabled:opacity-50"
>
Save
</button>
<button
type="button"
onclick={() => { showSavePreset = false; saveError = ''; newPresetName = ''; }}
class="rounded-lg px-1.5 py-1.5 text-xs text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
>
&#10005;
</button>
</div>
{#if saveError}
<p class="mt-1 text-xs text-red-500">{saveError}</p>
{/if}
{:else}
<button
type="button"
onclick={() => (showSavePreset = true)}
class="mt-2 w-full rounded-lg border border-dashed border-gray-300 dark:border-gray-600 px-3 py-1.5 text-xs text-gray-500 dark:text-gray-400 hover:border-gray-400 dark:hover:border-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
>
Save current as preset
</button>
{/if}
</div>
<!-- Override Level -->
<div>
<p class="mb-2 text-xs font-medium text-gray-700 dark:text-gray-300">Override Level</p>
<div class="space-y-1">
{#each overrideLevels as level (level.value)}
<button
type="button"
onclick={() => setOverrideLevel(level.value)}
class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm
{config.overrideLevel === level.value
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 ring-1 ring-blue-200 dark:ring-blue-700'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'}"
>
<span class="font-medium">{level.label}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">&mdash; {level.description}</span>
</button>
{/each}
</div>
</div>
<!-- Disabled Tools -->
<div>
<p class="mb-2 text-xs font-medium text-gray-700 dark:text-gray-300">Disabled Tools</p>
<div class="space-y-1">
{#each toolTypes as tool (tool.value)}
<label class="flex items-center gap-2 rounded px-2 py-1 text-sm hover:bg-gray-50 dark:hover:bg-gray-700">
<input
type="checkbox"
checked={config.disabledTools.includes(tool.label)}
onchange={() => toggleTool(tool.label)}
class="rounded border-gray-300 dark:border-gray-600 text-blue-600"
/>
<span class="text-gray-700 dark:text-gray-300">{tool.label}</span>
</label>
{/each}
</div>
</div>
<!-- Granted Permissions -->
<div>
<p class="mb-2 text-xs font-medium text-gray-700 dark:text-gray-300">Granted Permissions</p>
<div class="flex gap-1">
<input
type="text"
bind:value={newPermission}
onkeydown={(e) => { if (e.key === 'Enter') addPermission(); }}
placeholder="agent_type:tool"
class="flex-1 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2 py-1.5 text-xs text-gray-900 dark:text-gray-100 focus:border-blue-500 focus:outline-none"
/>
<button
type="button"
onclick={addPermission}
class="rounded-lg bg-gray-100 dark:bg-gray-700 px-2 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"
>
Add
</button>
</div>
{#if config.grantedPermissions.length > 0}
<div class="mt-2 space-y-1">
{#each config.grantedPermissions as perm (perm)}
<div class="flex items-center justify-between rounded bg-gray-50 dark:bg-gray-800 px-2 py-1">
<code class="text-xs text-gray-700 dark:text-gray-300">{perm}</code>
<button
type="button"
onclick={() => removePermission(perm)}
class="text-xs text-gray-400 dark:text-gray-500 hover:text-red-500"
>
&#10005;
</button>
</div>
{/each}
</div>
{/if}
</div>
</div>
</aside>

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import { connectionStore } from '$lib/stores/connection.svelte';
const statusConfig = {
connected: {
dot: 'bg-green-500',
label: 'Connected',
textColor: 'text-green-700 dark:text-green-400'
},
reconnecting: {
dot: 'bg-amber-500 animate-pulse',
label: 'Reconnecting',
textColor: 'text-amber-700 dark:text-amber-400'
},
disconnected: {
dot: 'bg-red-500',
label: 'Disconnected',
textColor: 'text-red-700 dark:text-red-400'
}
};
const config = $derived(statusConfig[connectionStore.status]);
</script>
<div class="flex items-center gap-1.5" title="Server: {config.label}">
<span class="h-2 w-2 rounded-full {config.dot}"></span>
<span class="hidden text-xs {config.textColor} sm:inline">{config.label}</span>
</div>

View File

@@ -0,0 +1,182 @@
<script lang="ts">
import type { SubagentResult } from '$lib/proto/llm_multiverse/v1/common_pb';
import { ResultStatus, ResultQuality, ArtifactType } from '$lib/proto/llm_multiverse/v1/common_pb';
import { resultSourceConfig } from '$lib/types/resultSource';
let { result }: { result: SubagentResult } = $props();
const statusConfig = $derived.by(() => {
switch (result.status) {
case ResultStatus.SUCCESS:
return { label: 'Success', bg: 'bg-green-100 dark:bg-green-900/30', text: 'text-green-800 dark:text-green-300', border: 'border-green-200 dark:border-green-800' };
case ResultStatus.PARTIAL:
return { label: 'Partial', bg: 'bg-amber-100 dark:bg-amber-900/30', text: 'text-amber-800 dark:text-amber-300', border: 'border-amber-200 dark:border-amber-800' };
case ResultStatus.FAILED:
return { label: 'Failed', bg: 'bg-red-100 dark:bg-red-900/30', text: 'text-red-800 dark:text-red-300', border: 'border-red-200 dark:border-red-800' };
default:
return { label: 'Unknown', bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-800 dark:text-gray-300', border: 'border-gray-200 dark:border-gray-700' };
}
});
const qualityLabel = $derived.by(() => {
switch (result.resultQuality) {
case ResultQuality.VERIFIED: return 'Verified';
case ResultQuality.INFERRED: return 'Inferred';
case ResultQuality.UNCERTAIN: return 'Uncertain';
default: return '';
}
});
const sourceBadge = $derived(resultSourceConfig(result.source));
const LINE_COLLAPSE_THRESHOLD = 20;
function contentLineCount(content: string): number {
return content.split('\n').length;
}
const URL_RE = /(https?:\/\/[^\s<>"')\]]+)/g;
function linkify(text: string): string {
return text.replace(URL_RE, '<a href="$1" target="_blank" rel="noopener noreferrer" class="text-blue-600 dark:text-blue-400 underline hover:text-blue-800 dark:hover:text-blue-300 break-all">$1</a>');
}
let copiedIndex: number | null = $state(null);
async function copyToClipboard(content: string, index: number) {
await navigator.clipboard.writeText(content);
copiedIndex = index;
setTimeout(() => {
if (copiedIndex === index) copiedIndex = null;
}, 2000);
}
</script>
<details class="mx-4 mb-3 rounded-xl border {statusConfig.border} {statusConfig.bg}">
<summary class="flex cursor-pointer items-center gap-2 px-4 py-2.5 select-none">
<svg class="chevron h-4 w-4 shrink-0 text-gray-500 dark:text-gray-400 transition-transform" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" /></svg>
<span class="rounded-full px-2.5 py-0.5 text-xs font-medium {statusConfig.bg} {statusConfig.text}">
{statusConfig.label}
</span>
{#if qualityLabel}
<span class="rounded-full bg-blue-100 dark:bg-blue-900/40 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:text-blue-300">
{qualityLabel}
</span>
{/if}
{#if sourceBadge.label !== 'Unspecified'}
<span class="rounded-full {sourceBadge.bg} px-2.5 py-0.5 text-xs font-medium {sourceBadge.text}">
{sourceBadge.label}
</span>
{/if}
{#if result.summary}
<span class="ml-1 truncate text-xs {statusConfig.text}">{result.summary}</span>
{/if}
</summary>
<div class="max-h-96 overflow-y-auto border-t {statusConfig.border} px-4 pb-4 pt-3">
{#if result.summary}
<p class="text-sm {statusConfig.text}">{@html linkify(result.summary)}</p>
{/if}
{#if result.failureReason}
<p class="mt-2 text-sm text-red-700 dark:text-red-400">Reason: {result.failureReason}</p>
{/if}
{#if result.artifacts.length > 0}
<div class="mt-3 border-t {statusConfig.border} pt-3">
<p class="mb-1.5 text-xs font-medium text-gray-600 dark:text-gray-400">Artifacts</p>
<div class="space-y-3">
{#each result.artifacts as artifact, i (artifact.label + i)}
{#if artifact.artifactType === ArtifactType.CODE}
<!-- Code artifact: syntax-highlighted block with filename header -->
<div class="rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="flex items-center gap-2 bg-gray-100 dark:bg-gray-800 px-3 py-1.5 text-xs">
<span class="text-blue-600 dark:text-blue-400">&#128196;</span>
<span class="font-mono font-medium text-gray-700 dark:text-gray-300">{artifact.label}</span>
<div class="ml-auto flex items-center gap-1.5">
{#if artifact.metadata?.language}
<span class="rounded bg-gray-200 dark:bg-gray-700 px-1.5 py-0.5 text-[10px] text-gray-500 dark:text-gray-400">{artifact.metadata.language}</span>
{/if}
<button
type="button"
onclick={() => copyToClipboard(artifact.content, i)}
class="rounded p-0.5 text-gray-400 hover:bg-gray-200 hover:text-gray-600 dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-300 transition-colors"
title="Copy code"
>
{#if copiedIndex === i}
<svg class="h-3.5 w-3.5 text-green-500" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>
{:else}
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9.75a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184" /></svg>
{/if}
</button>
</div>
</div>
{#if contentLineCount(artifact.content) > LINE_COLLAPSE_THRESHOLD}
<details>
<summary class="cursor-pointer bg-gray-50 dark:bg-gray-900 px-3 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300">
Show {contentLineCount(artifact.content)} lines
</summary>
<pre class="overflow-x-auto bg-gray-50 dark:bg-gray-900 p-3 text-xs leading-relaxed text-gray-800 dark:text-gray-200"><code class="language-{artifact.metadata?.language ?? ''}">{artifact.content}</code></pre>
</details>
{:else}
<pre class="overflow-x-auto bg-gray-50 dark:bg-gray-900 p-3 text-xs leading-relaxed text-gray-800 dark:text-gray-200"><code class="language-{artifact.metadata?.language ?? ''}">{artifact.content}</code></pre>
{/if}
</div>
{:else if artifact.artifactType === ArtifactType.COMMAND_OUTPUT}
<!-- Command output: terminal-style block -->
<div class="rounded-lg border border-gray-700 dark:border-gray-600 overflow-hidden">
<div class="flex items-center gap-2 bg-gray-800 dark:bg-gray-900 px-3 py-1.5 text-xs">
<span class="text-green-400">&#9654;</span>
<span class="font-mono text-gray-300">{artifact.label}</span>
</div>
{#if contentLineCount(artifact.content) > LINE_COLLAPSE_THRESHOLD}
<details>
<summary class="cursor-pointer bg-gray-900 dark:bg-black px-3 py-1 text-xs text-gray-400 hover:text-gray-200">
Show {contentLineCount(artifact.content)} lines
</summary>
<pre class="overflow-x-auto bg-gray-900 dark:bg-black p-3 font-mono text-xs leading-relaxed text-green-300 dark:text-green-400">{artifact.content}</pre>
</details>
{:else}
<pre class="overflow-x-auto bg-gray-900 dark:bg-black p-3 font-mono text-xs leading-relaxed text-green-300 dark:text-green-400">{artifact.content}</pre>
{/if}
</div>
{:else if artifact.artifactType === ArtifactType.SEARCH_RESULT}
<!-- Search result: card with query title -->
<div class="rounded-lg border border-purple-200 dark:border-purple-800 bg-purple-50 dark:bg-purple-900/20 p-3">
<div class="mb-1 flex items-center gap-2 text-xs">
<span class="text-purple-500">&#128269;</span>
<span class="font-medium text-purple-700 dark:text-purple-300">{artifact.label}</span>
</div>
{#if contentLineCount(artifact.content) > LINE_COLLAPSE_THRESHOLD}
<details>
<summary class="cursor-pointer text-xs text-purple-500 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300">
Show full results
</summary>
<p class="mt-1 whitespace-pre-wrap text-xs text-gray-700 dark:text-gray-300">{@html linkify(artifact.content)}</p>
</details>
{:else}
<p class="whitespace-pre-wrap text-xs text-gray-700 dark:text-gray-300">{@html linkify(artifact.content)}</p>
{/if}
</div>
{:else}
<!-- Text / finding artifact: plain text -->
<div class="flex items-start gap-2 text-sm">
<span class="mt-0.5 text-gray-400 dark:text-gray-500">&#128196;</span>
<span class="text-xs text-gray-700 dark:text-gray-300">{@html linkify(artifact.content)}</span>
</div>
{/if}
{/each}
</div>
</div>
{/if}
</div>
</details>
<style>
details[open] > summary .chevron {
transform: rotate(90deg);
}
</style>

View File

@@ -0,0 +1,94 @@
<script lang="ts">
import type { InferenceStats } from '$lib/proto/llm_multiverse/v1/common_pb';
let { stats }: { stats: InferenceStats } = $props();
const utilizationPct = $derived(
stats.contextWindowSize > 0
? Math.min(100, (stats.totalTokens / stats.contextWindowSize) * 100)
: 0
);
const utilizationColor = $derived.by(() => {
if (utilizationPct >= 90) return { bar: 'bg-red-500', text: 'text-red-700 dark:text-red-400' };
if (utilizationPct >= 70) return { bar: 'bg-amber-500', text: 'text-amber-700 dark:text-amber-400' };
return { bar: 'bg-blue-500', text: 'text-blue-700 dark:text-blue-400' };
});
function formatNumber(n: number): string {
return n.toLocaleString();
}
</script>
<details class="mx-4 mb-3 rounded-xl border border-indigo-200 dark:border-indigo-800 bg-indigo-50 dark:bg-indigo-900/20">
<summary class="flex cursor-pointer items-center gap-2 px-4 py-2.5 select-none">
<svg class="chevron h-4 w-4 shrink-0 text-gray-500 dark:text-gray-400 transition-transform" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" /></svg>
<span class="rounded-full bg-indigo-100 dark:bg-indigo-900/40 px-2.5 py-0.5 text-xs font-medium text-indigo-800 dark:text-indigo-300">
Stats
</span>
<span class="truncate text-xs text-indigo-700 dark:text-indigo-400">
{formatNumber(stats.totalTokens)} tokens
{#if stats.tokensPerSecond > 0}
&middot; {stats.tokensPerSecond.toFixed(1)} tok/s
{/if}
{#if stats.contextWindowSize > 0}
&middot; {utilizationPct.toFixed(0)}% context
{/if}
</span>
</summary>
<div class="border-t border-indigo-200 dark:border-indigo-800 px-4 pb-4 pt-3">
<div class="grid grid-cols-2 gap-x-6 gap-y-3 sm:grid-cols-3">
<div>
<p class="text-[10px] font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Prompt</p>
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{formatNumber(stats.promptTokens)}</p>
</div>
<div>
<p class="text-[10px] font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Completion</p>
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{formatNumber(stats.completionTokens)}</p>
</div>
<div>
<p class="text-[10px] font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Total</p>
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{formatNumber(stats.totalTokens)}</p>
</div>
{#if stats.tokensPerSecond > 0}
<div>
<p class="text-[10px] font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Throughput</p>
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{stats.tokensPerSecond.toFixed(1)} tok/s</p>
</div>
{/if}
{#if stats.contextWindowSize > 0}
<div class="col-span-2">
<p class="text-[10px] font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Context Window</p>
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{formatNumber(stats.contextWindowSize)}</p>
</div>
{/if}
</div>
{#if stats.contextWindowSize > 0}
<div class="mt-3 border-t border-indigo-200 dark:border-indigo-800 pt-3">
<div class="flex items-center justify-between text-xs">
<span class="font-medium text-gray-600 dark:text-gray-400">Context Utilization</span>
<span class="font-semibold {utilizationColor.text}">{utilizationPct.toFixed(1)}%</span>
</div>
<div class="mt-1.5 h-2 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
<div
class="h-full rounded-full transition-all duration-300 {utilizationColor.bar}"
style="width: {utilizationPct}%"
role="progressbar"
aria-valuenow={utilizationPct}
aria-valuemin={0}
aria-valuemax={100}
aria-label="Context window utilization"
></div>
</div>
</div>
{/if}
</div>
</details>
<style>
details[open] > summary .chevron {
transform: rotate(90deg);
}
</style>

View File

@@ -0,0 +1,301 @@
<script lang="ts">
import type { LineageNode } from '$lib/types/lineage';
import { agentTypeLabel, agentTypeColor } from '$lib/types/lineage';
import { themeStore } from '$lib/stores/theme.svelte';
let {
nodes,
onSelectNode
}: {
nodes: LineageNode[];
onSelectNode?: (node: LineageNode) => void;
} = $props();
let selectedNodeId: string | null = $state(null);
// Layout constants
const NODE_WIDTH = 180;
const NODE_HEIGHT = 64;
const HORIZONTAL_GAP = 60;
const VERTICAL_GAP = 24;
/**
* Positioned node with computed x/y coordinates for SVG rendering.
*/
interface PositionedNode {
node: LineageNode;
x: number;
y: number;
children: PositionedNode[];
}
/**
* Edge between two positioned nodes.
*/
interface Edge {
fromX: number;
fromY: number;
toX: number;
toY: number;
}
/**
* Computes leaf counts for every node in a single bottom-up pass.
* Stores results in a map keyed by node identity.
*/
function computeLeafCounts(roots: LineageNode[]): Map<LineageNode, number> {
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- internal computation map, not reactive state
const counts = new Map<LineageNode, number>();
function walk(node: LineageNode): number {
if (node.children.length === 0) {
counts.set(node, 1);
return 1;
}
const total = node.children.reduce((sum, child) => sum + walk(child), 0);
counts.set(node, total);
return total;
}
for (const root of roots) walk(root);
return counts;
}
/**
* Computes positioned nodes using a horizontal tree layout.
* Root is on the left; children branch to the right.
*/
function layoutTree(roots: LineageNode[]): {
positioned: PositionedNode[];
totalWidth: number;
totalHeight: number;
} {
const leafCounts = computeLeafCounts(roots);
const totalLeaves = roots.reduce((sum, r) => sum + (leafCounts.get(r) ?? 1), 0);
const totalHeight = totalLeaves * (NODE_HEIGHT + VERTICAL_GAP) - VERTICAL_GAP;
function positionNode(
node: LineageNode,
depth: number,
startY: number,
availableHeight: number
): PositionedNode {
const x = depth * (NODE_WIDTH + HORIZONTAL_GAP);
if (node.children.length === 0) {
const y = startY + availableHeight / 2 - NODE_HEIGHT / 2;
return { node, x, y, children: [] };
}
const childLeafCounts = node.children.map((c) => leafCounts.get(c) ?? 1);
const totalChildLeaves = childLeafCounts.reduce((a, b) => a + b, 0);
let currentY = startY;
const positionedChildren: PositionedNode[] = [];
for (let i = 0; i < node.children.length; i++) {
const childHeight = (childLeafCounts[i] / totalChildLeaves) * availableHeight;
const positioned = positionNode(node.children[i], depth + 1, currentY, childHeight);
positionedChildren.push(positioned);
currentY += childHeight;
}
// Center parent vertically relative to its children
const firstChildCenter = positionedChildren[0].y + NODE_HEIGHT / 2;
const lastChildCenter =
positionedChildren[positionedChildren.length - 1].y + NODE_HEIGHT / 2;
const y = (firstChildCenter + lastChildCenter) / 2 - NODE_HEIGHT / 2;
return { node, x, y, children: positionedChildren };
}
let currentY = 0;
const positioned: PositionedNode[] = [];
for (const root of roots) {
const rootLeaves = leafCounts.get(root) ?? 1;
const rootHeight = (rootLeaves / totalLeaves) * totalHeight;
positioned.push(positionNode(root, 0, currentY, rootHeight));
currentY += rootHeight;
}
// Calculate max depth for width
function maxDepth(pn: PositionedNode): number {
if (pn.children.length === 0) return 0;
return 1 + Math.max(...pn.children.map(maxDepth));
}
const depth = positioned.length > 0 ? Math.max(...positioned.map(maxDepth)) : 0;
const totalWidth = (depth + 1) * (NODE_WIDTH + HORIZONTAL_GAP) - HORIZONTAL_GAP;
return { positioned, totalWidth, totalHeight };
}
/**
* Collects all edges from the positioned tree for SVG rendering.
*/
function collectEdges(positioned: PositionedNode[]): Edge[] {
const edges: Edge[] = [];
function walk(pn: PositionedNode) {
for (const child of pn.children) {
edges.push({
fromX: pn.x + NODE_WIDTH,
fromY: pn.y + NODE_HEIGHT / 2,
toX: child.x,
toY: child.y + NODE_HEIGHT / 2
});
walk(child);
}
}
for (const pn of positioned) {
walk(pn);
}
return edges;
}
/**
* Collects all positioned nodes into a flat list for rendering.
*/
function collectNodes(positioned: PositionedNode[]): PositionedNode[] {
const result: PositionedNode[] = [];
function walk(pn: PositionedNode) {
result.push(pn);
for (const child of pn.children) {
walk(child);
}
}
for (const pn of positioned) {
walk(pn);
}
return result;
}
const layout = $derived(layoutTree(nodes));
const edges = $derived(collectEdges(layout.positioned));
const allNodes = $derived(collectNodes(layout.positioned));
const svgWidth = $derived(layout.totalWidth + 40);
const svgHeight = $derived(layout.totalHeight + 40);
// Dark-mode aware colors for SVG elements
const edgeColor = $derived(themeStore.isDark ? '#4b5563' : '#d1d5db');
const defaultStroke = $derived(themeStore.isDark ? '#4b5563' : '#e5e7eb');
const idTextColor = $derived(themeStore.isDark ? '#9ca3af' : '#6b7280');
const depthTextColor = $derived(themeStore.isDark ? '#6b7280' : '#9ca3af');
function handleNodeClick(node: LineageNode) {
selectedNodeId = selectedNodeId === node.id ? null : node.id;
onSelectNode?.(node);
}
/**
* Generates an SVG path for a smooth bezier curve between two points.
*/
function edgePath(edge: Edge): string {
const midX = (edge.fromX + edge.toX) / 2;
return `M ${edge.fromX} ${edge.fromY} C ${midX} ${edge.fromY}, ${midX} ${edge.toY}, ${edge.toX} ${edge.toY}`;
}
/**
* Truncates an ID string for display in the node.
*/
function truncateId(id: string): string {
return id.length > 16 ? id.slice(0, 14) + '..' : id;
}
</script>
<div class="overflow-auto rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
{#if nodes.length === 0}
<div class="flex items-center justify-center p-12 text-sm text-gray-400 dark:text-gray-500">
No lineage data available
</div>
{:else}
<svg
width={svgWidth}
height={svgHeight}
viewBox="0 0 {svgWidth} {svgHeight}"
class="min-w-full"
>
<g transform="translate(20, 20)">
<!-- Edges -->
{#each edges as edge, i (i)}
<path
d={edgePath(edge)}
fill="none"
stroke={edgeColor}
stroke-width="2"
stroke-linecap="round"
/>
{/each}
<!-- Nodes -->
{#each allNodes as pn (pn.node.id)}
{@const colors = agentTypeColor(pn.node.agentType)}
{@const colorSet = themeStore.isDark ? colors.dark : colors.light}
{@const isSelected = selectedNodeId === pn.node.id}
<g
class="cursor-pointer"
transform="translate({pn.x}, {pn.y})"
onclick={() => handleNodeClick(pn.node)}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') handleNodeClick(pn.node);
}}
role="button"
tabindex="0"
aria-label="Agent {pn.node.id}, type {agentTypeLabel(pn.node.agentType)}, depth {pn.node.spawnDepth}"
>
<!-- Node background -->
<rect
width={NODE_WIDTH}
height={NODE_HEIGHT}
rx="8"
ry="8"
fill={colorSet.fill}
stroke={isSelected ? colorSet.stroke : defaultStroke}
stroke-width={isSelected ? 2.5 : 1.5}
/>
<!-- Agent type label -->
<text
x={NODE_WIDTH / 2}
y="22"
text-anchor="middle"
fill={colorSet.text}
font-size="12"
font-weight="600"
>
{agentTypeLabel(pn.node.agentType)}
</text>
<!-- Agent ID -->
<text
x={NODE_WIDTH / 2}
y="38"
text-anchor="middle"
fill={idTextColor}
font-size="10"
font-family="monospace"
>
{truncateId(pn.node.id)}
</text>
<!-- Spawn depth badge -->
<text
x={NODE_WIDTH / 2}
y="54"
text-anchor="middle"
fill={depthTextColor}
font-size="10"
>
depth: {pn.node.spawnDepth}
</text>
</g>
{/each}
</g>
</svg>
{/if}
</div>

View File

@@ -0,0 +1,40 @@
<script lang="ts">
import type { StoredMemoryCandidate } from '$lib/stores/memory.svelte';
import { resultSourceConfig } from '$lib/types/resultSource';
let { candidate }: { candidate: StoredMemoryCandidate } = $props();
const sourceBadge = $derived(resultSourceConfig(candidate.source));
const confidencePct = $derived(Math.round(candidate.confidence * 100));
const confidenceColor = $derived.by(() => {
if (candidate.confidence >= 0.8) return { bar: 'bg-green-500', text: 'text-green-700 dark:text-green-400' };
if (candidate.confidence >= 0.5) return { bar: 'bg-amber-500', text: 'text-amber-700 dark:text-amber-400' };
return { bar: 'bg-red-500', text: 'text-red-700 dark:text-red-400' };
});
</script>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-4">
<div class="mb-2 flex items-center justify-between gap-2">
<span
class="shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium {sourceBadge.bg} {sourceBadge.text}"
>
{sourceBadge.label}
</span>
<span class="text-xs font-medium {confidenceColor.text}">
{confidencePct}%
</span>
</div>
<p class="mb-3 text-sm text-gray-700 dark:text-gray-300">{candidate.content}</p>
<div class="flex items-center gap-2">
<div class="h-2 flex-1 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
<div
class="h-full rounded-full transition-all {confidenceColor.bar}"
style="width: {confidencePct}%"
></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { ChatMessage } from '$lib/types';
let { message }: { message: ChatMessage } = $props();
const isUser = $derived(message.role === 'user');
</script>
<div class="flex {isUser ? 'justify-end' : 'justify-start'} mb-3">
<div
class="max-w-[85%] md:max-w-[75%] rounded-2xl px-3 py-2 md:px-4 md:py-2.5 {isUser
? 'bg-blue-600 text-white rounded-br-md'
: 'bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-bl-md'}"
>
<p class="text-sm whitespace-pre-wrap">{message.content}</p>
<time class="mt-1 block text-xs {isUser ? 'text-blue-200' : 'text-gray-500 dark:text-gray-400'}">
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</time>
</div>
</div>

View File

@@ -0,0 +1,63 @@
<script lang="ts">
let {
onSend,
disabled = false
}: { onSend: (message: string) => void; disabled?: boolean } = $props();
let input = $state('');
let textarea: HTMLTextAreaElement | undefined = $state();
function handleSubmit() {
const trimmed = input.trim();
if (!trimmed || disabled) return;
onSend(trimmed);
input = '';
resizeTextarea();
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
}
function resizeTextarea() {
if (!textarea) return;
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
}
$effect(() => {
if (!disabled && textarea) {
textarea.focus();
}
});
</script>
<div class="border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-3 md:p-4">
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="flex items-end gap-2">
<textarea
bind:this={textarea}
bind:value={input}
onkeydown={handleKeydown}
oninput={resizeTextarea}
{disabled}
placeholder="Type a message..."
rows="1"
class="flex-1 resize-none rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2.5 text-base md:px-4 md:text-sm text-gray-900 dark:text-gray-100
focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none
disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-400 dark:disabled:text-gray-500
placeholder:text-gray-400 dark:placeholder:text-gray-500"
></textarea>
<button
type="submit"
disabled={disabled || !input.trim()}
class="min-h-[44px] min-w-[44px] rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-medium text-white
hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none
disabled:bg-gray-300 dark:disabled:bg-gray-600 disabled:cursor-not-allowed"
>
Send
</button>
</form>
</div>

View File

@@ -0,0 +1,37 @@
<script lang="ts">
import { tick } from 'svelte';
import type { ChatMessage } from '$lib/types';
import MessageBubble from './MessageBubble.svelte';
let { messages }: { messages: ChatMessage[] } = $props();
let container: Element | undefined = $state();
async function scrollToBottom() {
await tick();
if (container) {
container.scrollTop = container.scrollHeight;
}
}
$effect(() => {
if (messages.length > 0) {
scrollToBottom();
}
});
</script>
<div bind:this={container} class="flex-1 overflow-y-auto p-4 bg-white dark:bg-gray-900">
{#if messages.length === 0}
<div class="flex h-full items-center justify-center">
<div class="text-center text-gray-400 dark:text-gray-500">
<p class="text-lg font-medium">No messages yet</p>
<p class="mt-1 text-sm">Send a message to start a conversation</p>
</div>
</div>
{:else}
{#each messages as message (message.id)}
<MessageBubble {message} />
{/each}
{/if}
</div>

View File

@@ -0,0 +1,90 @@
<script lang="ts">
import { OrchestrationState } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
let { state }: { state: OrchestrationState } = $props();
const phases = [
{ state: OrchestrationState.DECOMPOSING, label: 'Decomposing' },
{ state: OrchestrationState.DISPATCHING, label: 'Dispatching' },
{ state: OrchestrationState.EXECUTING, label: 'Executing' },
{ state: OrchestrationState.COMPACTING, label: 'Compacting' },
{ state: OrchestrationState.COMPLETE, label: 'Complete' }
];
function getStatus(phaseState: OrchestrationState): 'completed' | 'active' | 'pending' {
if (state === OrchestrationState.UNSPECIFIED) return 'pending';
if (phaseState < state) return 'completed';
if (phaseState === state) return 'active';
return 'pending';
}
</script>
<div class="mx-4 mb-2 rounded-xl bg-gray-50 dark:bg-gray-800 px-4 py-3">
<div class="flex items-center justify-between">
{#each phases as phase, i (phase.state)}
{@const status = getStatus(phase.state)}
<div class="flex flex-col items-center gap-1">
<div
class="flex h-7 w-7 items-center justify-center rounded-full text-xs font-medium transition-all duration-300
{status === 'completed'
? 'bg-green-500 text-white'
: status === 'active'
? 'bg-blue-500 text-white ring-2 ring-blue-300 dark:ring-blue-600 ring-offset-1 dark:ring-offset-gray-800 animate-pulse-ring'
: 'bg-gray-200 dark:bg-gray-600 text-gray-500 dark:text-gray-400'}"
>
{#if status === 'completed'}
&#10003;
{:else}
{i + 1}
{/if}
</div>
<span
class="text-xs transition-colors duration-300
{status === 'active' ? 'font-medium text-blue-600 dark:text-blue-400' : 'text-gray-500 dark:text-gray-400'}"
>
{phase.label}
</span>
</div>
{#if i < phases.length - 1}
{@const nextStatus = getStatus(phases[i + 1].state)}
<div class="relative mb-5 h-0.5 flex-1 bg-gray-200 dark:bg-gray-600 overflow-hidden">
{#if nextStatus !== 'pending'}
<!-- Completed: solid green fill -->
<div class="absolute inset-0 bg-green-500"></div>
{:else if status === 'active'}
<!-- Active: animated fill shimmer -->
<div class="absolute inset-0 animate-progress-fill bg-gradient-to-r from-blue-500 via-blue-400 to-blue-500 bg-[length:200%_100%]"></div>
{/if}
</div>
{/if}
{/each}
</div>
</div>
<style>
@keyframes pulse-ring {
0%, 100% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.5);
}
50% {
box-shadow: 0 0 0 6px rgba(59, 130, 246, 0);
}
}
@keyframes progress-fill {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
:global(.animate-pulse-ring) {
animation: pulse-ring 2s ease-in-out infinite;
}
:global(.animate-progress-fill) {
animation: progress-fill 2s linear infinite;
}
</style>

View File

@@ -0,0 +1,38 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
let {
title,
backHref,
backLabel = 'Chat',
children
}: {
title: string;
backHref?: string;
backLabel?: string;
children?: Snippet;
} = $props();
</script>
<header class="flex items-center justify-between border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-4 py-3 md:px-6">
<div class="flex items-center gap-2 md:gap-4">
{#if backHref}
<!-- eslint-disable svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
<a
href={backHref}
class="rounded-lg px-2.5 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100"
>
&larr; {backLabel}
</a>
<!-- eslint-enable svelte/no-navigation-without-resolve -->
{/if}
<h1 class="text-base md:text-lg font-semibold text-gray-900 dark:text-gray-100">{title}</h1>
</div>
<div class="flex items-center gap-2">
{#if children}
{@render children()}
{/if}
<ThemeToggle />
</div>
</header>

View File

@@ -0,0 +1,111 @@
<script lang="ts">
import { sessionStore } from '$lib/stores/sessions.svelte';
import { formatRelativeDate } from '$lib/utils/date';
import Backdrop from '$lib/components/Backdrop.svelte';
let {
onSelectSession,
onNewChat,
open = false,
onClose
}: {
onSelectSession: (id: string) => void;
onNewChat: () => void;
open?: boolean;
onClose?: () => void;
} = $props();
let confirmDeleteId: string | null = $state(null);
const sessions = $derived(sessionStore.getAllSessions());
const activeId = $derived(sessionStore.activeSessionId);
function handleDelete(e: MouseEvent, id: string) {
e.stopPropagation();
if (confirmDeleteId === id) {
sessionStore.deleteSession(id);
confirmDeleteId = null;
if (sessionStore.activeSessionId) {
onSelectSession(sessionStore.activeSessionId);
}
} else {
confirmDeleteId = id;
}
}
function handleSelectAndClose(id: string) {
onSelectSession(id);
onClose?.();
}
function handleNewChatAndClose() {
onNewChat();
onClose?.();
}
</script>
{#if open}
<Backdrop onClose={() => onClose?.()} />
{/if}
<!-- Sidebar: always visible on md+, slide-in drawer on mobile when open -->
<aside
class="flex h-full w-64 flex-col border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800
fixed inset-y-0 left-0 z-50 transition-transform duration-300 ease-in-out
{open ? 'translate-x-0' : '-translate-x-full'}
md:relative md:z-auto md:translate-x-0 md:transition-none"
>
<div class="flex items-center justify-between border-b border-gray-200 dark:border-gray-700 p-3">
<button
type="button"
onclick={handleNewChatAndClose}
class="flex-1 rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
+ New Chat
</button>
<!-- Close button visible only on mobile -->
<button
type="button"
onclick={onClose}
class="ml-2 flex h-9 w-9 items-center justify-center rounded-lg text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 md:hidden"
aria-label="Close sidebar"
>
&#10005;
</button>
</div>
<div class="flex-1 overflow-y-auto">
{#if sessions.length === 0}
<p class="p-4 text-center text-sm text-gray-400 dark:text-gray-500">No sessions yet</p>
{:else}
{#each sessions as session (session.id)}
<div
role="button"
tabindex="0"
onclick={() => handleSelectAndClose(session.id)}
onkeydown={(e) => { if (e.key === 'Enter') handleSelectAndClose(session.id); }}
class="group flex w-full cursor-pointer items-start gap-2 border-b border-gray-100 dark:border-gray-700 px-3 py-3 text-left hover:bg-gray-100 dark:hover:bg-gray-700
{activeId === session.id ? 'bg-blue-50 dark:bg-blue-900/30 border-l-2 border-l-blue-500' : ''}"
>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-gray-900 dark:text-gray-100">{session.title}</p>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">{formatRelativeDate(session.createdAt)}</p>
</div>
<button
type="button"
onclick={(e) => handleDelete(e, session.id)}
class="shrink-0 rounded p-1 text-xs opacity-0 group-hover:opacity-100
{confirmDeleteId === session.id
? 'bg-red-100 dark:bg-red-900/40 text-red-600 dark:text-red-400'
: 'text-gray-400 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-600 hover:text-gray-600 dark:hover:text-gray-300'}"
title={confirmDeleteId === session.id ? 'Click again to confirm' : 'Delete session'}
>
{confirmDeleteId === session.id ? '\u2713' : '\u2715'}
</button>
</div>
{/each}
{/if}
</div>
</aside>

View File

@@ -0,0 +1,37 @@
<script lang="ts">
import { themeStore } from '$lib/stores/theme.svelte';
const labels: Record<string, string> = {
system: 'System',
light: 'Light',
dark: 'Dark'
};
</script>
<button
type="button"
onclick={() => themeStore.cycle()}
class="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-100"
title="Theme: {labels[themeStore.mode]} (click to cycle)"
>
{#if themeStore.mode === 'light'}
<!-- Sun icon -->
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5" />
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
</svg>
{:else if themeStore.mode === 'dark'}
<!-- Moon icon -->
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
{:else}
<!-- Monitor/system icon -->
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
<line x1="8" y1="21" x2="16" y2="21" />
<line x1="12" y1="17" x2="12" y2="21" />
</svg>
{/if}
<span class="text-xs">{labels[themeStore.mode]}</span>
</button>

View File

@@ -0,0 +1,31 @@
<script lang="ts">
let { content }: { content: string } = $props();
let expanded = $state(false);
</script>
{#if content}
<div class="mx-4 mb-2">
<button
type="button"
onclick={() => (expanded = !expanded)}
class="flex w-full items-center gap-2 rounded-lg bg-amber-50 dark:bg-amber-900/30 px-3 py-2 text-left text-sm text-amber-700 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/50 transition-colors"
>
<span
class="inline-block transition-transform duration-200 {expanded
? 'rotate-90'
: 'rotate-0'}"
>
&#9654;
</span>
<span class="font-medium">Thinking...</span>
</button>
{#if expanded}
<div
class="mt-1 rounded-b-lg border border-t-0 border-amber-200 dark:border-amber-700 bg-amber-50/50 dark:bg-amber-900/20 px-4 py-3 text-sm text-amber-800 dark:text-amber-200 whitespace-pre-wrap"
>
{content}
</div>
{/if}
</div>
{/if}

View File

@@ -0,0 +1,86 @@
<script lang="ts">
import { toastStore, type ToastType } from '$lib/stores/toast.svelte';
const typeStyles: Record<ToastType, { bg: string; border: string; icon: string }> = {
error: {
bg: 'bg-red-50 dark:bg-red-900/40',
border: 'border-red-200 dark:border-red-800',
icon: 'text-red-500 dark:text-red-400'
},
warning: {
bg: 'bg-amber-50 dark:bg-amber-900/40',
border: 'border-amber-200 dark:border-amber-800',
icon: 'text-amber-500 dark:text-amber-400'
},
success: {
bg: 'bg-green-50 dark:bg-green-900/40',
border: 'border-green-200 dark:border-green-800',
icon: 'text-green-500 dark:text-green-400'
},
info: {
bg: 'bg-blue-50 dark:bg-blue-900/40',
border: 'border-blue-200 dark:border-blue-800',
icon: 'text-blue-500 dark:text-blue-400'
}
};
const typeLabels: Record<ToastType, string> = {
error: 'Error',
warning: 'Warning',
success: 'Success',
info: 'Info'
};
</script>
{#if toastStore.toasts.length > 0}
<div class="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-sm w-full pointer-events-none">
{#each toastStore.toasts as toast (toast.id)}
{@const style = typeStyles[toast.type]}
<div
class="pointer-events-auto flex items-start gap-3 rounded-lg border px-4 py-3 shadow-lg {style.bg} {style.border}"
role="alert"
>
<!-- Icon -->
<div class="mt-0.5 shrink-0 {style.icon}">
{#if toast.type === 'error'}
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
{:else if toast.type === 'warning'}
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
</svg>
{:else if toast.type === 'success'}
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
{:else}
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>
{/if}
</div>
<!-- Content -->
<div class="min-w-0 flex-1">
<p class="text-xs font-medium text-gray-900 dark:text-gray-100">{typeLabels[toast.type]}</p>
<p class="mt-0.5 text-sm text-gray-700 dark:text-gray-300">{toast.message}</p>
</div>
<!-- Dismiss button -->
{#if toast.dismissable}
<button
type="button"
onclick={() => toastStore.removeToast(toast.id)}
class="shrink-0 rounded-md p-1 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
aria-label="Dismiss"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
{/if}
</div>
{/each}
</div>
{/if}

View File

@@ -0,0 +1,174 @@
import type { ChatMessage } from '$lib/types';
import type { SubagentResult, InferenceStats } from '$lib/proto/llm_multiverse/v1/common_pb';
import type { SessionConfig } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
import { processRequest, OrchestratorError, friendlyMessage } from '$lib/services/orchestrator';
import { OrchestrationState } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
import { sessionStore } from '$lib/stores/sessions.svelte';
import { memoryStore } from '$lib/stores/memory.svelte';
import { auditStore } from '$lib/stores/audit.svelte';
import { logger } from '$lib/utils/logger';
export function createOrchestration() {
let isStreaming = $state(false);
let error: string | null = $state(null);
let lastFailedContent: string | null = $state(null);
let orchestrationState: OrchestrationState = $state(OrchestrationState.UNSPECIFIED);
let intermediateResult: string = $state('');
let finalResult: SubagentResult | null = $state(null);
let inferenceStats: InferenceStats | null = $state(null);
async function send(
sessionId: string,
content: string,
config: SessionConfig,
messages: ChatMessage[]
): Promise<ChatMessage[]> {
error = null;
lastFailedContent = null;
orchestrationState = OrchestrationState.UNSPECIFIED;
intermediateResult = '';
finalResult = null;
inferenceStats = null;
let lastAuditState = OrchestrationState.UNSPECIFIED;
const userMessage: ChatMessage = {
id: crypto.randomUUID(),
role: 'user',
content,
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- plain data, not reactive state
timestamp: new Date()
};
messages.push(userMessage);
const assistantMessage: ChatMessage = {
id: crypto.randomUUID(),
role: 'assistant',
content: '',
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- plain data, not reactive state
timestamp: new Date()
};
messages.push(assistantMessage);
sessionStore.updateMessages(sessionId, messages);
isStreaming = true;
try {
for await (const response of processRequest(sessionId, content, config)) {
orchestrationState = response.state;
if (response.state !== lastAuditState) {
const stateLabel = OrchestrationState[response.state] ?? String(response.state);
logger.debug('useOrchestration', `State: ${stateLabel}`, {
sessionId,
state: response.state
});
auditStore.addEvent(sessionId, {
eventType: 'state_change',
details: response.message || `State changed to ${stateLabel}`,
state: stateLabel
});
lastAuditState = response.state;
}
if (response.intermediateResult) {
intermediateResult = response.intermediateResult;
}
if (response.inferenceStats) {
inferenceStats = response.inferenceStats;
}
if (response.finalResult) {
finalResult = response.finalResult;
if (response.finalResult.newMemoryCandidates.length > 0) {
memoryStore.addCandidates(
sessionId,
response.finalResult.newMemoryCandidates.map((mc) => ({
content: mc.content,
source: mc.source,
confidence: mc.confidence
}))
);
}
}
const idx = messages.length - 1;
messages[idx] = {
...messages[idx],
content: response.message
};
}
} catch (err) {
const isOrcErr = err instanceof OrchestratorError;
const code = isOrcErr ? err.code : 'unknown';
const details = isOrcErr ? err.details : undefined;
const friendlyMsg = isOrcErr
? friendlyMessage(err.code)
: 'An unexpected error occurred';
const displayMsg =
code && code !== 'unknown'
? `${friendlyMsg} (${code})`
: friendlyMsg;
error = displayMsg;
lastFailedContent = content;
logger.error('useOrchestration', 'Request failed', { code, details });
auditStore.addEvent(sessionId, {
eventType: 'error',
details: `${friendlyMsg} | code=${code}${details ? ` | details=${details}` : ''}`
});
const idx = messages.length - 1;
messages[idx] = {
...messages[idx],
content: `\u26A0 ${displayMsg}`
};
} finally {
isStreaming = false;
sessionStore.updateMessages(sessionId, messages);
}
return messages;
}
function retry(
sessionId: string,
config: SessionConfig,
messages: ChatMessage[]
): { messages: ChatMessage[]; sending: boolean } {
if (!lastFailedContent) return { messages, sending: false };
const content = lastFailedContent;
// Remove the failed user+assistant pair before retrying
if (
messages.length >= 2 &&
messages[messages.length - 2].role === 'user' &&
messages[messages.length - 1].role === 'assistant' &&
messages[messages.length - 1].content.startsWith('\u26A0')
) {
messages = messages.slice(0, -2);
}
send(sessionId, content, config, messages);
return { messages, sending: true };
}
function reset() {
error = null;
finalResult = null;
inferenceStats = null;
}
return {
get isStreaming() { return isStreaming; },
get error() { return error; },
get lastFailedContent() { return lastFailedContent; },
get orchestrationState() { return orchestrationState; },
get intermediateResult() { return intermediateResult; },
get finalResult() { return finalResult; },
get inferenceStats() { return inferenceStats; },
send,
retry,
reset
};
}
/**
* Module-level singleton so orchestration state survives tab/route changes.
* The chat page uses this instead of calling createOrchestration() per mount.
*/
export const orchestrationStore = createOrchestration();

1
src/lib/index.ts Normal file
View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@@ -0,0 +1,211 @@
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file llm_multiverse/v1/audit.proto (package llm_multiverse.v1, syntax proto3)
/* eslint-disable */
import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
import type { Timestamp } from "@bufbuild/protobuf/wkt";
import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt";
import type { AgentLineage, AgentType, SessionContext } from "./common_pb";
import { file_llm_multiverse_v1_common } from "./common_pb";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file llm_multiverse/v1/audit.proto.
*/
export const file_llm_multiverse_v1_audit: GenFile = /*@__PURE__*/
fileDesc("Ch1sbG1fbXVsdGl2ZXJzZS92MS9hdWRpdC5wcm90bxIRbGxtX211bHRpdmVyc2UudjEipAMKCkF1ZGl0RW50cnkSLQoJdGltZXN0YW1wGAEgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpzZXNzaW9uX2lkGAIgASgJEhAKCGFnZW50X2lkGAMgASgJEjAKCmFnZW50X3R5cGUYBCABKA4yHC5sbG1fbXVsdGl2ZXJzZS52MS5BZ2VudFR5cGUSMAoHbGluZWFnZRgFIAEoCzIfLmxsbV9tdWx0aXZlcnNlLnYxLkFnZW50TGluZWFnZRIuCgZhY3Rpb24YBiABKA4yHi5sbG1fbXVsdGl2ZXJzZS52MS5BdWRpdEFjdGlvbhIRCgl0b29sX25hbWUYByABKAkSEwoLcGFyYW1zX2hhc2gYCCABKAkSFQoNcmVzdWx0X3N0YXR1cxgJIAEoCRI9CghtZXRhZGF0YRgKIAMoCzIrLmxsbV9tdWx0aXZlcnNlLnYxLkF1ZGl0RW50cnkuTWV0YWRhdGFFbnRyeRovCg1NZXRhZGF0YUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEicQoNQXBwZW5kUmVxdWVzdBIyCgdjb250ZXh0GAEgASgLMiEubGxtX211bHRpdmVyc2UudjEuU2Vzc2lvbkNvbnRleHQSLAoFZW50cnkYAiABKAsyHS5sbG1fbXVsdGl2ZXJzZS52MS5BdWRpdEVudHJ5Ik8KDkFwcGVuZFJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgSGgoNZXJyb3JfbWVzc2FnZRgCIAEoCUgAiAEBQhAKDl9lcnJvcl9tZXNzYWdlKrkCCgtBdWRpdEFjdGlvbhIcChhBVURJVF9BQ1RJT05fVU5TUEVDSUZJRUQQABIgChxBVURJVF9BQ1RJT05fVE9PTF9JTlZPQ0FUSU9OEAESIAocQVVESVRfQUNUSU9OX0JST0tFUl9ERUNJU0lPThACEhwKGEFVRElUX0FDVElPTl9NRU1PUllfUkVBRBADEh0KGUFVRElUX0FDVElPTl9NRU1PUllfV1JJVEUQBBIfChtBVURJVF9BQ1RJT05fU1VCQUdFTlRfU1BBV04QBRImCiJBVURJVF9BQ1RJT05fU0VTU0lPTl9DT05GSUdfQ0hBTkdFEAYSIgoeQVVESVRfQUNUSU9OX0lORkVSRU5DRV9SRVFVRVNUEAcSHgoaQVVESVRfQUNUSU9OX1NFQ1JFVF9BQ0NFU1MQCDJdCgxBdWRpdFNlcnZpY2USTQoGQXBwZW5kEiAubGxtX211bHRpdmVyc2UudjEuQXBwZW5kUmVxdWVzdBohLmxsbV9tdWx0aXZlcnNlLnYxLkFwcGVuZFJlc3BvbnNlYgZwcm90bzM", [file_google_protobuf_timestamp, file_llm_multiverse_v1_common]);
/**
* A single audit log entry.
*
* @generated from message llm_multiverse.v1.AuditEntry
*/
export type AuditEntry = Message<"llm_multiverse.v1.AuditEntry"> & {
/**
* @generated from field: google.protobuf.Timestamp timestamp = 1;
*/
timestamp?: Timestamp;
/**
* @generated from field: string session_id = 2;
*/
sessionId: string;
/**
* @generated from field: string agent_id = 3;
*/
agentId: string;
/**
* @generated from field: llm_multiverse.v1.AgentType agent_type = 4;
*/
agentType: AgentType;
/**
* @generated from field: llm_multiverse.v1.AgentLineage lineage = 5;
*/
lineage?: AgentLineage;
/**
* @generated from field: llm_multiverse.v1.AuditAction action = 6;
*/
action: AuditAction;
/**
* Tool name, if action is tool-related.
*
* @generated from field: string tool_name = 7;
*/
toolName: string;
/**
* Hash of parameters — never raw params to avoid logging credentials.
*
* @generated from field: string params_hash = 8;
*/
paramsHash: string;
/**
* Result of the action (allow/deny/success/failure).
*
* @generated from field: string result_status = 9;
*/
resultStatus: string;
/**
* Additional context as key-value pairs.
*
* @generated from field: map<string, string> metadata = 10;
*/
metadata: { [key: string]: string };
};
/**
* Describes the message llm_multiverse.v1.AuditEntry.
* Use `create(AuditEntrySchema)` to create a new message.
*/
export const AuditEntrySchema: GenMessage<AuditEntry> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_audit, 0);
/**
* @generated from message llm_multiverse.v1.AppendRequest
*/
export type AppendRequest = Message<"llm_multiverse.v1.AppendRequest"> & {
/**
* @generated from field: llm_multiverse.v1.SessionContext context = 1;
*/
context?: SessionContext;
/**
* @generated from field: llm_multiverse.v1.AuditEntry entry = 2;
*/
entry?: AuditEntry;
};
/**
* Describes the message llm_multiverse.v1.AppendRequest.
* Use `create(AppendRequestSchema)` to create a new message.
*/
export const AppendRequestSchema: GenMessage<AppendRequest> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_audit, 1);
/**
* @generated from message llm_multiverse.v1.AppendResponse
*/
export type AppendResponse = Message<"llm_multiverse.v1.AppendResponse"> & {
/**
* @generated from field: bool success = 1;
*/
success: boolean;
/**
* @generated from field: optional string error_message = 2;
*/
errorMessage?: string;
};
/**
* Describes the message llm_multiverse.v1.AppendResponse.
* Use `create(AppendResponseSchema)` to create a new message.
*/
export const AppendResponseSchema: GenMessage<AppendResponse> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_audit, 2);
/**
* Action categories for audit log entries.
*
* @generated from enum llm_multiverse.v1.AuditAction
*/
export enum AuditAction {
/**
* @generated from enum value: AUDIT_ACTION_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* @generated from enum value: AUDIT_ACTION_TOOL_INVOCATION = 1;
*/
TOOL_INVOCATION = 1,
/**
* @generated from enum value: AUDIT_ACTION_BROKER_DECISION = 2;
*/
BROKER_DECISION = 2,
/**
* @generated from enum value: AUDIT_ACTION_MEMORY_READ = 3;
*/
MEMORY_READ = 3,
/**
* @generated from enum value: AUDIT_ACTION_MEMORY_WRITE = 4;
*/
MEMORY_WRITE = 4,
/**
* @generated from enum value: AUDIT_ACTION_SUBAGENT_SPAWN = 5;
*/
SUBAGENT_SPAWN = 5,
/**
* @generated from enum value: AUDIT_ACTION_SESSION_CONFIG_CHANGE = 6;
*/
SESSION_CONFIG_CHANGE = 6,
/**
* @generated from enum value: AUDIT_ACTION_INFERENCE_REQUEST = 7;
*/
INFERENCE_REQUEST = 7,
/**
* @generated from enum value: AUDIT_ACTION_SECRET_ACCESS = 8;
*/
SECRET_ACCESS = 8,
}
/**
* Describes the enum llm_multiverse.v1.AuditAction.
*/
export const AuditActionSchema: GenEnum<AuditAction> = /*@__PURE__*/
enumDesc(file_llm_multiverse_v1_audit, 0);
/**
* Write-only append log for all service actions.
*
* @generated from service llm_multiverse.v1.AuditService
*/
export const AuditService: GenService<{
/**
* Append a single audit log entry. Write-only — no read RPC exposed.
*
* @generated from rpc llm_multiverse.v1.AuditService.Append
*/
append: {
methodKind: "unary";
input: typeof AppendRequestSchema;
output: typeof AppendResponseSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_llm_multiverse_v1_audit, 0);

View File

@@ -0,0 +1,589 @@
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file llm_multiverse/v1/common.proto (package llm_multiverse.v1, syntax proto3)
/* eslint-disable */
import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
import type { Timestamp } from "@bufbuild/protobuf/wkt";
import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file llm_multiverse/v1/common.proto.
*/
export const file_llm_multiverse_v1_common: GenFile = /*@__PURE__*/
fileDesc("Ch5sbG1fbXVsdGl2ZXJzZS92MS9jb21tb24ucHJvdG8SEWxsbV9tdWx0aXZlcnNlLnYxItABCghBcnRpZmFjdBINCgVsYWJlbBgBIAEoCRIPCgdjb250ZW50GAIgASgJEjYKDWFydGlmYWN0X3R5cGUYAyABKA4yHy5sbG1fbXVsdGl2ZXJzZS52MS5BcnRpZmFjdFR5cGUSOwoIbWV0YWRhdGEYBCADKAsyKS5sbG1fbXVsdGl2ZXJzZS52MS5BcnRpZmFjdC5NZXRhZGF0YUVudHJ5Gi8KDU1ldGFkYXRhRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJqCg9BZ2VudElkZW50aWZpZXISEAoIYWdlbnRfaWQYASABKAkSMAoKYWdlbnRfdHlwZRgCIAEoDjIcLmxsbV9tdWx0aXZlcnNlLnYxLkFnZW50VHlwZRITCgtzcGF3bl9kZXB0aBgDIAEoDSJCCgxBZ2VudExpbmVhZ2USMgoGYWdlbnRzGAEgAygLMiIubGxtX211bHRpdmVyc2UudjEuQWdlbnRJZGVudGlmaWVyItcBCg5TZXNzaW9uQ29udGV4dBISCgpzZXNzaW9uX2lkGAEgASgJEg8KB3VzZXJfaWQYAiABKAkSNgoNYWdlbnRfbGluZWFnZRgDIAEoCzIfLmxsbV9tdWx0aXZlcnNlLnYxLkFnZW50TGluZWFnZRI4Cg5vdmVycmlkZV9sZXZlbBgEIAEoDjIgLmxsbV9tdWx0aXZlcnNlLnYxLk92ZXJyaWRlTGV2ZWwSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAinQEKC0Vycm9yRGV0YWlsEgwKBGNvZGUYASABKAkSDwoHbWVzc2FnZRgCIAEoCRI+CghtZXRhZGF0YRgDIAMoCzIsLmxsbV9tdWx0aXZlcnNlLnYxLkVycm9yRGV0YWlsLk1ldGFkYXRhRW50cnkaLwoNTWV0YWRhdGFFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBImcKD01lbW9yeUNhbmRpZGF0ZRIPCgdjb250ZW50GAEgASgJEi8KBnNvdXJjZRgCIAEoDjIfLmxsbV9tdWx0aXZlcnNlLnYxLlJlc3VsdFNvdXJjZRISCgpjb25maWRlbmNlGAMgASgCIpABCg5JbmZlcmVuY2VTdGF0cxIVCg1wcm9tcHRfdG9rZW5zGAEgASgNEhkKEWNvbXBsZXRpb25fdG9rZW5zGAIgASgNEhQKDHRvdGFsX3Rva2VucxgDIAEoDRIbChNjb250ZXh0X3dpbmRvd19zaXplGAQgASgNEhkKEXRva2Vuc19wZXJfc2Vjb25kGAUgASgCIuACCg5TdWJhZ2VudFJlc3VsdBIvCgZzdGF0dXMYASABKA4yHy5sbG1fbXVsdGl2ZXJzZS52MS5SZXN1bHRTdGF0dXMSDwoHc3VtbWFyeRgCIAEoCRIuCglhcnRpZmFjdHMYAyADKAsyGy5sbG1fbXVsdGl2ZXJzZS52MS5BcnRpZmFjdBI4Cg5yZXN1bHRfcXVhbGl0eRgEIAEoDjIgLmxsbV9tdWx0aXZlcnNlLnYxLlJlc3VsdFF1YWxpdHkSLwoGc291cmNlGAUgASgOMh8ubGxtX211bHRpdmVyc2UudjEuUmVzdWx0U291cmNlEkEKFW5ld19tZW1vcnlfY2FuZGlkYXRlcxgGIAMoCzIiLmxsbV9tdWx0aXZlcnNlLnYxLk1lbW9yeUNhbmRpZGF0ZRIbCg5mYWlsdXJlX3JlYXNvbhgHIAEoCUgAiAEBQhEKD19mYWlsdXJlX3JlYXNvbiqoAQoJQWdlbnRUeXBlEhoKFkFHRU5UX1RZUEVfVU5TUEVDSUZJRUQQABIbChdBR0VOVF9UWVBFX09SQ0hFU1RSQVRPUhABEhkKFUFHRU5UX1RZUEVfUkVTRUFSQ0hFUhACEhQKEEFHRU5UX1RZUEVfQ09ERVIQAxIXChNBR0VOVF9UWVBFX1NZU0FETUlOEAQSGAoUQUdFTlRfVFlQRV9BU1NJU1RBTlQQBSr1AQoIVG9vbFR5cGUSGQoVVE9PTF9UWVBFX1VOU1BFQ0lGSUVEEAASGQoVVE9PTF9UWVBFX01FTU9SWV9SRUFEEAESGgoWVE9PTF9UWVBFX01FTU9SWV9XUklURRACEhgKFFRPT0xfVFlQRV9XRUJfU0VBUkNIEAMSFQoRVE9PTF9UWVBFX0ZTX1JFQUQQBBIWChJUT09MX1RZUEVfRlNfV1JJVEUQBRIWChJUT09MX1RZUEVfUlVOX0NPREUQBhIXChNUT09MX1RZUEVfUlVOX1NIRUxMEAcSHQoZVE9PTF9UWVBFX1BBQ0tBR0VfSU5TVEFMTBAIKnoKDU92ZXJyaWRlTGV2ZWwSHgoaT1ZFUlJJREVfTEVWRUxfVU5TUEVDSUZJRUQQABIXChNPVkVSUklERV9MRVZFTF9OT05FEAESGAoUT1ZFUlJJREVfTEVWRUxfUkVMQVgQAhIWChJPVkVSUklERV9MRVZFTF9BTEwQAyp9CgxSZXN1bHRTdGF0dXMSHQoZUkVTVUxUX1NUQVRVU19VTlNQRUNJRklFRBAAEhkKFVJFU1VMVF9TVEFUVVNfU1VDQ0VTUxABEhkKFVJFU1VMVF9TVEFUVVNfUEFSVElBTBACEhgKFFJFU1VMVF9TVEFUVVNfRkFJTEVEEAMqhwEKDVJlc3VsdFF1YWxpdHkSHgoaUkVTVUxUX1FVQUxJVFlfVU5TUEVDSUZJRUQQABIbChdSRVNVTFRfUVVBTElUWV9WRVJJRklFRBABEhsKF1JFU1VMVF9RVUFMSVRZX0lORkVSUkVEEAISHAoYUkVTVUxUX1FVQUxJVFlfVU5DRVJUQUlOEAMqhgEKDFJlc3VsdFNvdXJjZRIdChlSRVNVTFRfU09VUkNFX1VOU1BFQ0lGSUVEEAASHQoZUkVTVUxUX1NPVVJDRV9UT09MX09VVFBVVBABEiEKHVJFU1VMVF9TT1VSQ0VfTU9ERUxfS05PV0xFREdFEAISFQoRUkVTVUxUX1NPVVJDRV9XRUIQAyqgAQoMQXJ0aWZhY3RUeXBlEh0KGUFSVElGQUNUX1RZUEVfVU5TUEVDSUZJRUQQABIWChJBUlRJRkFDVF9UWVBFX0NPREUQARIWChJBUlRJRkFDVF9UWVBFX1RFWFQQAhIgChxBUlRJRkFDVF9UWVBFX0NPTU1BTkRfT1VUUFVUEAMSHwobQVJUSUZBQ1RfVFlQRV9TRUFSQ0hfUkVTVUxUEARiBnByb3RvMw", [file_google_protobuf_timestamp]);
/**
* A concrete output produced by an agent (code, command output, etc.).
*
* @generated from message llm_multiverse.v1.Artifact
*/
export type Artifact = Message<"llm_multiverse.v1.Artifact"> & {
/**
* Display name (filename, query, etc.)
*
* @generated from field: string label = 1;
*/
label: string;
/**
* Full content
*
* @generated from field: string content = 2;
*/
content: string;
/**
* @generated from field: llm_multiverse.v1.ArtifactType artifact_type = 3;
*/
artifactType: ArtifactType;
/**
* language, path, tool_name, exit_code, etc.
*
* @generated from field: map<string, string> metadata = 4;
*/
metadata: { [key: string]: string };
};
/**
* Describes the message llm_multiverse.v1.Artifact.
* Use `create(ArtifactSchema)` to create a new message.
*/
export const ArtifactSchema: GenMessage<Artifact> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_common, 0);
/**
* Identifies a single agent in the lineage chain.
*
* @generated from message llm_multiverse.v1.AgentIdentifier
*/
export type AgentIdentifier = Message<"llm_multiverse.v1.AgentIdentifier"> & {
/**
* @generated from field: string agent_id = 1;
*/
agentId: string;
/**
* @generated from field: llm_multiverse.v1.AgentType agent_type = 2;
*/
agentType: AgentType;
/**
* @generated from field: uint32 spawn_depth = 3;
*/
spawnDepth: number;
};
/**
* Describes the message llm_multiverse.v1.AgentIdentifier.
* Use `create(AgentIdentifierSchema)` to create a new message.
*/
export const AgentIdentifierSchema: GenMessage<AgentIdentifier> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_common, 1);
/**
* Ordered chain of agents from orchestrator (index 0) to current agent.
* Used by the Tool Broker for lineage constraint enforcement.
*
* @generated from message llm_multiverse.v1.AgentLineage
*/
export type AgentLineage = Message<"llm_multiverse.v1.AgentLineage"> & {
/**
* @generated from field: repeated llm_multiverse.v1.AgentIdentifier agents = 1;
*/
agents: AgentIdentifier[];
};
/**
* Describes the message llm_multiverse.v1.AgentLineage.
* Use `create(AgentLineageSchema)` to create a new message.
*/
export const AgentLineageSchema: GenMessage<AgentLineage> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_common, 2);
/**
* Carried in every gRPC request for audit trail and broker enforcement.
*
* @generated from message llm_multiverse.v1.SessionContext
*/
export type SessionContext = Message<"llm_multiverse.v1.SessionContext"> & {
/**
* @generated from field: string session_id = 1;
*/
sessionId: string;
/**
* @generated from field: string user_id = 2;
*/
userId: string;
/**
* @generated from field: llm_multiverse.v1.AgentLineage agent_lineage = 3;
*/
agentLineage?: AgentLineage;
/**
* @generated from field: llm_multiverse.v1.OverrideLevel override_level = 4;
*/
overrideLevel: OverrideLevel;
/**
* @generated from field: google.protobuf.Timestamp created_at = 5;
*/
createdAt?: Timestamp;
};
/**
* Describes the message llm_multiverse.v1.SessionContext.
* Use `create(SessionContextSchema)` to create a new message.
*/
export const SessionContextSchema: GenMessage<SessionContext> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_common, 3);
/**
* Structured error detail for gRPC error responses.
*
* @generated from message llm_multiverse.v1.ErrorDetail
*/
export type ErrorDetail = Message<"llm_multiverse.v1.ErrorDetail"> & {
/**
* @generated from field: string code = 1;
*/
code: string;
/**
* @generated from field: string message = 2;
*/
message: string;
/**
* @generated from field: map<string, string> metadata = 3;
*/
metadata: { [key: string]: string };
};
/**
* Describes the message llm_multiverse.v1.ErrorDetail.
* Use `create(ErrorDetailSchema)` to create a new message.
*/
export const ErrorDetailSchema: GenMessage<ErrorDetail> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_common, 4);
/**
* A candidate memory entry proposed by a subagent for persistence.
*
* @generated from message llm_multiverse.v1.MemoryCandidate
*/
export type MemoryCandidate = Message<"llm_multiverse.v1.MemoryCandidate"> & {
/**
* @generated from field: string content = 1;
*/
content: string;
/**
* @generated from field: llm_multiverse.v1.ResultSource source = 2;
*/
source: ResultSource;
/**
* @generated from field: float confidence = 3;
*/
confidence: number;
};
/**
* Describes the message llm_multiverse.v1.MemoryCandidate.
* Use `create(MemoryCandidateSchema)` to create a new message.
*/
export const MemoryCandidateSchema: GenMessage<MemoryCandidate> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_common, 5);
/**
* Inference statistics surfaced from model-gateway through the orchestrator.
*
* @generated from message llm_multiverse.v1.InferenceStats
*/
export type InferenceStats = Message<"llm_multiverse.v1.InferenceStats"> & {
/**
* Number of tokens in the prompt.
*
* @generated from field: uint32 prompt_tokens = 1;
*/
promptTokens: number;
/**
* Number of tokens generated.
*
* @generated from field: uint32 completion_tokens = 2;
*/
completionTokens: number;
/**
* Sum of prompt + completion tokens.
*
* @generated from field: uint32 total_tokens = 3;
*/
totalTokens: number;
/**
* Model's maximum context length.
*
* @generated from field: uint32 context_window_size = 4;
*/
contextWindowSize: number;
/**
* Generation throughput (tokens per second).
*
* @generated from field: float tokens_per_second = 5;
*/
tokensPerSecond: number;
};
/**
* Describes the message llm_multiverse.v1.InferenceStats.
* Use `create(InferenceStatsSchema)` to create a new message.
*/
export const InferenceStatsSchema: GenMessage<InferenceStats> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_common, 6);
/**
* Standardized return value from any subagent to its parent.
*
* @generated from message llm_multiverse.v1.SubagentResult
*/
export type SubagentResult = Message<"llm_multiverse.v1.SubagentResult"> & {
/**
* @generated from field: llm_multiverse.v1.ResultStatus status = 1;
*/
status: ResultStatus;
/**
* 3-sentence max summary of work performed.
*
* @generated from field: string summary = 2;
*/
summary: string;
/**
* Structured artifacts produced during the agent loop.
*
* @generated from field: repeated llm_multiverse.v1.Artifact artifacts = 3;
*/
artifacts: Artifact[];
/**
* @generated from field: llm_multiverse.v1.ResultQuality result_quality = 4;
*/
resultQuality: ResultQuality;
/**
* @generated from field: llm_multiverse.v1.ResultSource source = 5;
*/
source: ResultSource;
/**
* @generated from field: repeated llm_multiverse.v1.MemoryCandidate new_memory_candidates = 6;
*/
newMemoryCandidates: MemoryCandidate[];
/**
* @generated from field: optional string failure_reason = 7;
*/
failureReason?: string;
};
/**
* Describes the message llm_multiverse.v1.SubagentResult.
* Use `create(SubagentResultSchema)` to create a new message.
*/
export const SubagentResultSchema: GenMessage<SubagentResult> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_common, 7);
/**
* Agent types with distinct tool permission manifests.
*
* @generated from enum llm_multiverse.v1.AgentType
*/
export enum AgentType {
/**
* @generated from enum value: AGENT_TYPE_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* @generated from enum value: AGENT_TYPE_ORCHESTRATOR = 1;
*/
ORCHESTRATOR = 1,
/**
* @generated from enum value: AGENT_TYPE_RESEARCHER = 2;
*/
RESEARCHER = 2,
/**
* @generated from enum value: AGENT_TYPE_CODER = 3;
*/
CODER = 3,
/**
* @generated from enum value: AGENT_TYPE_SYSADMIN = 4;
*/
SYSADMIN = 4,
/**
* @generated from enum value: AGENT_TYPE_ASSISTANT = 5;
*/
ASSISTANT = 5,
}
/**
* Describes the enum llm_multiverse.v1.AgentType.
*/
export const AgentTypeSchema: GenEnum<AgentType> = /*@__PURE__*/
enumDesc(file_llm_multiverse_v1_common, 0);
/**
* Tool categories enforced by the Tool Broker.
*
* @generated from enum llm_multiverse.v1.ToolType
*/
export enum ToolType {
/**
* @generated from enum value: TOOL_TYPE_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* @generated from enum value: TOOL_TYPE_MEMORY_READ = 1;
*/
MEMORY_READ = 1,
/**
* @generated from enum value: TOOL_TYPE_MEMORY_WRITE = 2;
*/
MEMORY_WRITE = 2,
/**
* @generated from enum value: TOOL_TYPE_WEB_SEARCH = 3;
*/
WEB_SEARCH = 3,
/**
* @generated from enum value: TOOL_TYPE_FS_READ = 4;
*/
FS_READ = 4,
/**
* @generated from enum value: TOOL_TYPE_FS_WRITE = 5;
*/
FS_WRITE = 5,
/**
* @generated from enum value: TOOL_TYPE_RUN_CODE = 6;
*/
RUN_CODE = 6,
/**
* @generated from enum value: TOOL_TYPE_RUN_SHELL = 7;
*/
RUN_SHELL = 7,
/**
* @generated from enum value: TOOL_TYPE_PACKAGE_INSTALL = 8;
*/
PACKAGE_INSTALL = 8,
}
/**
* Describes the enum llm_multiverse.v1.ToolType.
*/
export const ToolTypeSchema: GenEnum<ToolType> = /*@__PURE__*/
enumDesc(file_llm_multiverse_v1_common, 1);
/**
* Session-level override for broker enforcement.
*
* @generated from enum llm_multiverse.v1.OverrideLevel
*/
export enum OverrideLevel {
/**
* @generated from enum value: OVERRIDE_LEVEL_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* Full manifest + broker enforcement (default).
*
* @generated from enum value: OVERRIDE_LEVEL_NONE = 1;
*/
NONE = 1,
/**
* High-risk tools unlocked, lineage still enforced.
*
* @generated from enum value: OVERRIDE_LEVEL_RELAX = 2;
*/
RELAX = 2,
/**
* Broker passes everything through.
*
* @generated from enum value: OVERRIDE_LEVEL_ALL = 3;
*/
ALL = 3,
}
/**
* Describes the enum llm_multiverse.v1.OverrideLevel.
*/
export const OverrideLevelSchema: GenEnum<OverrideLevel> = /*@__PURE__*/
enumDesc(file_llm_multiverse_v1_common, 2);
/**
* Status of a subagent's result.
*
* @generated from enum llm_multiverse.v1.ResultStatus
*/
export enum ResultStatus {
/**
* @generated from enum value: RESULT_STATUS_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* @generated from enum value: RESULT_STATUS_SUCCESS = 1;
*/
SUCCESS = 1,
/**
* @generated from enum value: RESULT_STATUS_PARTIAL = 2;
*/
PARTIAL = 2,
/**
* @generated from enum value: RESULT_STATUS_FAILED = 3;
*/
FAILED = 3,
}
/**
* Describes the enum llm_multiverse.v1.ResultStatus.
*/
export const ResultStatusSchema: GenEnum<ResultStatus> = /*@__PURE__*/
enumDesc(file_llm_multiverse_v1_common, 3);
/**
* Confidence level of a result.
*
* @generated from enum llm_multiverse.v1.ResultQuality
*/
export enum ResultQuality {
/**
* @generated from enum value: RESULT_QUALITY_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* @generated from enum value: RESULT_QUALITY_VERIFIED = 1;
*/
VERIFIED = 1,
/**
* @generated from enum value: RESULT_QUALITY_INFERRED = 2;
*/
INFERRED = 2,
/**
* @generated from enum value: RESULT_QUALITY_UNCERTAIN = 3;
*/
UNCERTAIN = 3,
}
/**
* Describes the enum llm_multiverse.v1.ResultQuality.
*/
export const ResultQualitySchema: GenEnum<ResultQuality> = /*@__PURE__*/
enumDesc(file_llm_multiverse_v1_common, 4);
/**
* Provenance of a result.
*
* @generated from enum llm_multiverse.v1.ResultSource
*/
export enum ResultSource {
/**
* @generated from enum value: RESULT_SOURCE_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* @generated from enum value: RESULT_SOURCE_TOOL_OUTPUT = 1;
*/
TOOL_OUTPUT = 1,
/**
* @generated from enum value: RESULT_SOURCE_MODEL_KNOWLEDGE = 2;
*/
MODEL_KNOWLEDGE = 2,
/**
* @generated from enum value: RESULT_SOURCE_WEB = 3;
*/
WEB = 3,
}
/**
* Describes the enum llm_multiverse.v1.ResultSource.
*/
export const ResultSourceSchema: GenEnum<ResultSource> = /*@__PURE__*/
enumDesc(file_llm_multiverse_v1_common, 5);
/**
* Type of artifact produced by an agent during its tool call loop.
*
* @generated from enum llm_multiverse.v1.ArtifactType
*/
export enum ArtifactType {
/**
* @generated from enum value: ARTIFACT_TYPE_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* Code written via fs_write
*
* @generated from enum value: ARTIFACT_TYPE_CODE = 1;
*/
CODE = 1,
/**
* Plain text / file content from fs_read
*
* @generated from enum value: ARTIFACT_TYPE_TEXT = 2;
*/
TEXT = 2,
/**
* Output from run_code / run_shell
*
* @generated from enum value: ARTIFACT_TYPE_COMMAND_OUTPUT = 3;
*/
COMMAND_OUTPUT = 3,
/**
* Web search results
*
* @generated from enum value: ARTIFACT_TYPE_SEARCH_RESULT = 4;
*/
SEARCH_RESULT = 4,
}
/**
* Describes the enum llm_multiverse.v1.ArtifactType.
*/
export const ArtifactTypeSchema: GenEnum<ArtifactType> = /*@__PURE__*/
enumDesc(file_llm_multiverse_v1_common, 6);

View File

@@ -0,0 +1,581 @@
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file llm_multiverse/v1/memory.proto (package llm_multiverse.v1, syntax proto3)
/* eslint-disable */
import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
import type { Timestamp } from "@bufbuild/protobuf/wkt";
import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt";
import type { SessionContext } from "./common_pb";
import { file_llm_multiverse_v1_common } from "./common_pb";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file llm_multiverse/v1/memory.proto.
*/
export const file_llm_multiverse_v1_memory: GenFile = /*@__PURE__*/
fileDesc("Ch5sbG1fbXVsdGl2ZXJzZS92MS9tZW1vcnkucHJvdG8SEWxsbV9tdWx0aXZlcnNlLnYxIu4CChJQcm92ZW5hbmNlTWV0YWRhdGESFwoPc291cmNlX2FnZW50X2lkGAEgASgJEhkKEXNvdXJjZV9zZXNzaW9uX2lkGAIgASgJEhUKDWNyZWF0aW9uX3Rvb2wYAyABKAkSPAoLdHJ1c3RfbGV2ZWwYBCABKA4yJy5sbG1fbXVsdGl2ZXJzZS52MS5Qcm92ZW5hbmNlVHJ1c3RMZXZlbBIZChFwYXJlbnRfbWVtb3J5X2lkcxgFIAMoCRISCgppc19yZXZva2VkGAYgASgIEh4KEXJldm9jYXRpb25fcmVhc29uGAcgASgJSACIAQESMwoKcmV2b2tlZF9hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIAYgBARIXCgpyZXZva2VkX2J5GAkgASgJSAKIAQFCFAoSX3Jldm9jYXRpb25fcmVhc29uQg0KC19yZXZva2VkX2F0Qg0KC19yZXZva2VkX2J5IroDCgtNZW1vcnlFbnRyeRIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEgwKBHRhZ3MYBCADKAkSFwoPY29ycmVsYXRpbmdfaWRzGAUgAygJEg4KBmNvcnB1cxgGIAEoCRIWCg5uYW1lX2VtYmVkZGluZxgHIAEoDBIdChVkZXNjcmlwdGlvbl9lbWJlZGRpbmcYCCABKAwSGAoQY29ycHVzX2VtYmVkZGluZxgJIAEoDBIuCgpjcmVhdGVkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIxCg1sYXN0X2FjY2Vzc2VkGAsgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIUCgxhY2Nlc3NfY291bnQYDCABKA0SNwoKcHJvdmVuYW5jZRgNIAEoDjIjLmxsbV9tdWx0aXZlcnNlLnYxLk1lbW9yeVByb3ZlbmFuY2USQgoTcHJvdmVuYW5jZV9tZXRhZGF0YRgOIAEoCzIlLmxsbV9tdWx0aXZlcnNlLnYxLlByb3ZlbmFuY2VNZXRhZGF0YSLvAQoSUXVlcnlNZW1vcnlSZXF1ZXN0EjIKB2NvbnRleHQYASABKAsyIS5sbG1fbXVsdGl2ZXJzZS52MS5TZXNzaW9uQ29udGV4dBINCgVxdWVyeRgCIAEoCRITCgttZW1vcnlfdHlwZRgDIAEoCRINCgVsaW1pdBgEIAEoDRIXCg9za2lwX2V4dHJhY3Rpb24YBSABKAgSQAoPbWluX3RydXN0X2xldmVsGAYgASgOMicubGxtX211bHRpdmVyc2UudjEuUHJvdmVuYW5jZVRydXN0TGV2ZWwSFwoPaW5jbHVkZV9yZXZva2VkGAcgASgIIoICChNRdWVyeU1lbW9yeVJlc3BvbnNlEgwKBHJhbmsYASABKA0SLQoFZW50cnkYAiABKAsyHi5sbG1fbXVsdGl2ZXJzZS52MS5NZW1vcnlFbnRyeRIZChFjb3NpbmVfc2ltaWxhcml0eRgDIAEoAhIRCglpc19jYWNoZWQYBCABKAgSJQoYY2FjaGVkX2V4dHJhY3RlZF9zZWdtZW50GAUgASgJSACIAQESIgoVZXh0cmFjdGlvbl9jb25maWRlbmNlGAYgASgCSAGIAQFCGwoZX2NhY2hlZF9leHRyYWN0ZWRfc2VnbWVudEIYChZfZXh0cmFjdGlvbl9jb25maWRlbmNlIrABChJXcml0ZU1lbW9yeVJlcXVlc3QSMgoHY29udGV4dBgBIAEoCzIhLmxsbV9tdWx0aXZlcnNlLnYxLlNlc3Npb25Db250ZXh0Ei0KBWVudHJ5GAIgASgLMh4ubGxtX211bHRpdmVyc2UudjEuTWVtb3J5RW50cnkSNwoKcHJvdmVuYW5jZRgDIAEoDjIjLmxsbV9tdWx0aXZlcnNlLnYxLk1lbW9yeVByb3ZlbmFuY2UiZwoTV3JpdGVNZW1vcnlSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEhEKCW1lbW9yeV9pZBgCIAEoCRIaCg1lcnJvcl9tZXNzYWdlGAMgASgJSACIAQFCEAoOX2Vycm9yX21lc3NhZ2UidgoUR2V0Q29ycmVsYXRlZFJlcXVlc3QSMgoHY29udGV4dBgBIAEoCzIhLmxsbV9tdWx0aXZlcnNlLnYxLlNlc3Npb25Db250ZXh0EhEKCW1lbW9yeV9pZBgCIAEoCRIXCg9jb3JyZWxhdGluZ19pZHMYAyADKAkivgEKFUdldENvcnJlbGF0ZWRSZXNwb25zZRJQCgxjb3JyZWxhdGlvbnMYASADKAsyOi5sbG1fbXVsdGl2ZXJzZS52MS5HZXRDb3JyZWxhdGVkUmVzcG9uc2UuQ29ycmVsYXRpb25zRW50cnkaUwoRQ29ycmVsYXRpb25zRW50cnkSCwoDa2V5GAEgASgJEi0KBXZhbHVlGAIgASgLMh4ubGxtX211bHRpdmVyc2UudjEuTWVtb3J5RW50cnk6AjgBIm0KE1Jldm9rZU1lbW9yeVJlcXVlc3QSMgoHY29udGV4dBgBIAEoCzIhLmxsbV9tdWx0aXZlcnNlLnYxLlNlc3Npb25Db250ZXh0EhIKCm1lbW9yeV9pZHMYAiADKAkSDgoGcmVhc29uGAMgASgJImwKFFJldm9rZU1lbW9yeVJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgSFQoNcmV2b2tlZF9jb3VudBgCIAEoDRIaCg1lcnJvcl9tZXNzYWdlGAMgASgJSACIAQFCEAoOX2Vycm9yX21lc3NhZ2UqdQoQTWVtb3J5UHJvdmVuYW5jZRIhCh1NRU1PUllfUFJPVkVOQU5DRV9VTlNQRUNJRklFRBAAEh4KGk1FTU9SWV9QUk9WRU5BTkNFX0lOVEVSTkFMEAESHgoaTUVNT1JZX1BST1ZFTkFOQ0VfRVhURVJOQUwQAirSAQoUUHJvdmVuYW5jZVRydXN0TGV2ZWwSJgoiUFJPVkVOQU5DRV9UUlVTVF9MRVZFTF9VTlNQRUNJRklFRBAAEiIKHlBST1ZFTkFOQ0VfVFJVU1RfTEVWRUxfVFJVU1RFRBABEiQKIFBST1ZFTkFOQ0VfVFJVU1RfTEVWRUxfU0FOSVRJWkVEEAISJAogUFJPVkVOQU5DRV9UUlVTVF9MRVZFTF9VTlRSVVNURUQQAxIiCh5QUk9WRU5BTkNFX1RSVVNUX0xFVkVMX1JFVk9LRUQQBDKSAwoNTWVtb3J5U2VydmljZRJeCgtRdWVyeU1lbW9yeRIlLmxsbV9tdWx0aXZlcnNlLnYxLlF1ZXJ5TWVtb3J5UmVxdWVzdBomLmxsbV9tdWx0aXZlcnNlLnYxLlF1ZXJ5TWVtb3J5UmVzcG9uc2UwARJcCgtXcml0ZU1lbW9yeRIlLmxsbV9tdWx0aXZlcnNlLnYxLldyaXRlTWVtb3J5UmVxdWVzdBomLmxsbV9tdWx0aXZlcnNlLnYxLldyaXRlTWVtb3J5UmVzcG9uc2USYgoNR2V0Q29ycmVsYXRlZBInLmxsbV9tdWx0aXZlcnNlLnYxLkdldENvcnJlbGF0ZWRSZXF1ZXN0GigubGxtX211bHRpdmVyc2UudjEuR2V0Q29ycmVsYXRlZFJlc3BvbnNlEl8KDFJldm9rZU1lbW9yeRImLmxsbV9tdWx0aXZlcnNlLnYxLlJldm9rZU1lbW9yeVJlcXVlc3QaJy5sbG1fbXVsdGl2ZXJzZS52MS5SZXZva2VNZW1vcnlSZXNwb25zZWIGcHJvdG8z", [file_google_protobuf_timestamp, file_llm_multiverse_v1_common]);
/**
* Detailed provenance metadata for a memory entry.
*
* @generated from message llm_multiverse.v1.ProvenanceMetadata
*/
export type ProvenanceMetadata = Message<"llm_multiverse.v1.ProvenanceMetadata"> & {
/**
* The agent ID that created this memory.
*
* @generated from field: string source_agent_id = 1;
*/
sourceAgentId: string;
/**
* The session ID during which this memory was created.
*
* @generated from field: string source_session_id = 2;
*/
sourceSessionId: string;
/**
* The tool that produced the content (e.g., "web_search", "fs_read", "model_inference").
*
* @generated from field: string creation_tool = 3;
*/
creationTool: string;
/**
* Trust level of this memory.
*
* @generated from field: llm_multiverse.v1.ProvenanceTrustLevel trust_level = 4;
*/
trustLevel: ProvenanceTrustLevel;
/**
* For derived memories: IDs of parent memories this was derived from.
*
* @generated from field: repeated string parent_memory_ids = 5;
*/
parentMemoryIds: string[];
/**
* Whether this memory has been administratively tainted/revoked.
*
* @generated from field: bool is_revoked = 6;
*/
isRevoked: boolean;
/**
* Reason for revocation (set by admin).
*
* @generated from field: optional string revocation_reason = 7;
*/
revocationReason?: string;
/**
* Timestamp when the memory was revoked.
*
* @generated from field: optional google.protobuf.Timestamp revoked_at = 8;
*/
revokedAt?: Timestamp;
/**
* The agent ID that revoked this memory (admin action).
*
* @generated from field: optional string revoked_by = 9;
*/
revokedBy?: string;
};
/**
* Describes the message llm_multiverse.v1.ProvenanceMetadata.
* Use `create(ProvenanceMetadataSchema)` to create a new message.
*/
export const ProvenanceMetadataSchema: GenMessage<ProvenanceMetadata> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_memory, 0);
/**
* A single memory entry stored in DuckDB with VSS.
*
* @generated from message llm_multiverse.v1.MemoryEntry
*/
export type MemoryEntry = Message<"llm_multiverse.v1.MemoryEntry"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
/**
* @generated from field: string name = 2;
*/
name: string;
/**
* @generated from field: string description = 3;
*/
description: string;
/**
* @generated from field: repeated string tags = 4;
*/
tags: string[];
/**
* @generated from field: repeated string correlating_ids = 5;
*/
correlatingIds: string[];
/**
* Full text content.
*
* @generated from field: string corpus = 6;
*/
corpus: string;
/**
* Embedding vectors (from nomic-embed-text).
*
* @generated from field: bytes name_embedding = 7;
*/
nameEmbedding: Uint8Array;
/**
* @generated from field: bytes description_embedding = 8;
*/
descriptionEmbedding: Uint8Array;
/**
* @generated from field: bytes corpus_embedding = 9;
*/
corpusEmbedding: Uint8Array;
/**
* @generated from field: google.protobuf.Timestamp created_at = 10;
*/
createdAt?: Timestamp;
/**
* @generated from field: google.protobuf.Timestamp last_accessed = 11;
*/
lastAccessed?: Timestamp;
/**
* @generated from field: uint32 access_count = 12;
*/
accessCount: number;
/**
* @generated from field: llm_multiverse.v1.MemoryProvenance provenance = 13;
*/
provenance: MemoryProvenance;
/**
* Detailed provenance metadata (source agent, trust level, revocation status).
*
* @generated from field: llm_multiverse.v1.ProvenanceMetadata provenance_metadata = 14;
*/
provenanceMetadata?: ProvenanceMetadata;
};
/**
* Describes the message llm_multiverse.v1.MemoryEntry.
* Use `create(MemoryEntrySchema)` to create a new message.
*/
export const MemoryEntrySchema: GenMessage<MemoryEntry> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_memory, 1);
/**
* @generated from message llm_multiverse.v1.QueryMemoryRequest
*/
export type QueryMemoryRequest = Message<"llm_multiverse.v1.QueryMemoryRequest"> & {
/**
* @generated from field: llm_multiverse.v1.SessionContext context = 1;
*/
context?: SessionContext;
/**
* @generated from field: string query = 2;
*/
query: string;
/**
* Optional filter by memory type/tag.
*
* @generated from field: string memory_type = 3;
*/
memoryType: string;
/**
* Max results to return (default 5).
*
* @generated from field: uint32 limit = 4;
*/
limit: number;
/**
* When true, skip the extraction step for lower latency.
*
* @generated from field: bool skip_extraction = 5;
*/
skipExtraction: boolean;
/**
* Filter results by provenance trust level (only return entries at or above this level).
*
* @generated from field: llm_multiverse.v1.ProvenanceTrustLevel min_trust_level = 6;
*/
minTrustLevel: ProvenanceTrustLevel;
/**
* When true, include revoked memories in results (default: false, meaning revoked memories are excluded).
*
* @generated from field: bool include_revoked = 7;
*/
includeRevoked: boolean;
};
/**
* Describes the message llm_multiverse.v1.QueryMemoryRequest.
* Use `create(QueryMemoryRequestSchema)` to create a new message.
*/
export const QueryMemoryRequestSchema: GenMessage<QueryMemoryRequest> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_memory, 2);
/**
* Streamed result for each matching memory entry.
*
* @generated from message llm_multiverse.v1.QueryMemoryResponse
*/
export type QueryMemoryResponse = Message<"llm_multiverse.v1.QueryMemoryResponse"> & {
/**
* @generated from field: uint32 rank = 1;
*/
rank: number;
/**
* @generated from field: llm_multiverse.v1.MemoryEntry entry = 2;
*/
entry?: MemoryEntry;
/**
* @generated from field: float cosine_similarity = 3;
*/
cosineSimilarity: number;
/**
* @generated from field: bool is_cached = 4;
*/
isCached: boolean;
/**
* Extracted relevant segment if cache hit.
*
* @generated from field: optional string cached_extracted_segment = 5;
*/
cachedExtractedSegment?: string;
/**
* Confidence of the extraction (0.0-1.0). Only set when extraction was performed.
*
* @generated from field: optional float extraction_confidence = 6;
*/
extractionConfidence?: number;
};
/**
* Describes the message llm_multiverse.v1.QueryMemoryResponse.
* Use `create(QueryMemoryResponseSchema)` to create a new message.
*/
export const QueryMemoryResponseSchema: GenMessage<QueryMemoryResponse> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_memory, 3);
/**
* @generated from message llm_multiverse.v1.WriteMemoryRequest
*/
export type WriteMemoryRequest = Message<"llm_multiverse.v1.WriteMemoryRequest"> & {
/**
* @generated from field: llm_multiverse.v1.SessionContext context = 1;
*/
context?: SessionContext;
/**
* @generated from field: llm_multiverse.v1.MemoryEntry entry = 2;
*/
entry?: MemoryEntry;
/**
* @generated from field: llm_multiverse.v1.MemoryProvenance provenance = 3;
*/
provenance: MemoryProvenance;
};
/**
* Describes the message llm_multiverse.v1.WriteMemoryRequest.
* Use `create(WriteMemoryRequestSchema)` to create a new message.
*/
export const WriteMemoryRequestSchema: GenMessage<WriteMemoryRequest> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_memory, 4);
/**
* @generated from message llm_multiverse.v1.WriteMemoryResponse
*/
export type WriteMemoryResponse = Message<"llm_multiverse.v1.WriteMemoryResponse"> & {
/**
* @generated from field: bool success = 1;
*/
success: boolean;
/**
* ID of the created or updated memory entry.
*
* @generated from field: string memory_id = 2;
*/
memoryId: string;
/**
* @generated from field: optional string error_message = 3;
*/
errorMessage?: string;
};
/**
* Describes the message llm_multiverse.v1.WriteMemoryResponse.
* Use `create(WriteMemoryResponseSchema)` to create a new message.
*/
export const WriteMemoryResponseSchema: GenMessage<WriteMemoryResponse> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_memory, 5);
/**
* @generated from message llm_multiverse.v1.GetCorrelatedRequest
*/
export type GetCorrelatedRequest = Message<"llm_multiverse.v1.GetCorrelatedRequest"> & {
/**
* @generated from field: llm_multiverse.v1.SessionContext context = 1;
*/
context?: SessionContext;
/**
* @generated from field: string memory_id = 2;
*/
memoryId: string;
/**
* @generated from field: repeated string correlating_ids = 3;
*/
correlatingIds: string[];
};
/**
* Describes the message llm_multiverse.v1.GetCorrelatedRequest.
* Use `create(GetCorrelatedRequestSchema)` to create a new message.
*/
export const GetCorrelatedRequestSchema: GenMessage<GetCorrelatedRequest> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_memory, 6);
/**
* @generated from message llm_multiverse.v1.GetCorrelatedResponse
*/
export type GetCorrelatedResponse = Message<"llm_multiverse.v1.GetCorrelatedResponse"> & {
/**
* Map of memory ID to entry (descriptions only, not full corpus).
*
* @generated from field: map<string, llm_multiverse.v1.MemoryEntry> correlations = 1;
*/
correlations: { [key: string]: MemoryEntry };
};
/**
* Describes the message llm_multiverse.v1.GetCorrelatedResponse.
* Use `create(GetCorrelatedResponseSchema)` to create a new message.
*/
export const GetCorrelatedResponseSchema: GenMessage<GetCorrelatedResponse> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_memory, 7);
/**
* Request to mark memories as tainted/revoked (admin operation).
*
* @generated from message llm_multiverse.v1.RevokeMemoryRequest
*/
export type RevokeMemoryRequest = Message<"llm_multiverse.v1.RevokeMemoryRequest"> & {
/**
* @generated from field: llm_multiverse.v1.SessionContext context = 1;
*/
context?: SessionContext;
/**
* Memory IDs to revoke.
*
* @generated from field: repeated string memory_ids = 2;
*/
memoryIds: string[];
/**
* Reason for revocation.
*
* @generated from field: string reason = 3;
*/
reason: string;
};
/**
* Describes the message llm_multiverse.v1.RevokeMemoryRequest.
* Use `create(RevokeMemoryRequestSchema)` to create a new message.
*/
export const RevokeMemoryRequestSchema: GenMessage<RevokeMemoryRequest> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_memory, 8);
/**
* @generated from message llm_multiverse.v1.RevokeMemoryResponse
*/
export type RevokeMemoryResponse = Message<"llm_multiverse.v1.RevokeMemoryResponse"> & {
/**
* @generated from field: bool success = 1;
*/
success: boolean;
/**
* Number of memories revoked.
*
* @generated from field: uint32 revoked_count = 2;
*/
revokedCount: number;
/**
* @generated from field: optional string error_message = 3;
*/
errorMessage?: string;
};
/**
* Describes the message llm_multiverse.v1.RevokeMemoryResponse.
* Use `create(RevokeMemoryResponseSchema)` to create a new message.
*/
export const RevokeMemoryResponseSchema: GenMessage<RevokeMemoryResponse> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_memory, 9);
/**
* Provenance of a memory entry.
*
* @generated from enum llm_multiverse.v1.MemoryProvenance
*/
export enum MemoryProvenance {
/**
* @generated from enum value: MEMORY_PROVENANCE_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* Generated internally by agents.
*
* @generated from enum value: MEMORY_PROVENANCE_INTERNAL = 1;
*/
INTERNAL = 1,
/**
* Sourced from external content (web, files). Tagged for poisoning protection.
*
* @generated from enum value: MEMORY_PROVENANCE_EXTERNAL = 2;
*/
EXTERNAL = 2,
}
/**
* Describes the enum llm_multiverse.v1.MemoryProvenance.
*/
export const MemoryProvenanceSchema: GenEnum<MemoryProvenance> = /*@__PURE__*/
enumDesc(file_llm_multiverse_v1_memory, 0);
/**
* Trust level for provenance-based filtering.
*
* @generated from enum llm_multiverse.v1.ProvenanceTrustLevel
*/
export enum ProvenanceTrustLevel {
/**
* @generated from enum value: PROVENANCE_TRUST_LEVEL_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* Fully trusted (internal agent-generated content).
*
* @generated from enum value: PROVENANCE_TRUST_LEVEL_TRUSTED = 1;
*/
TRUSTED = 1,
/**
* External content that has been sanitized.
*
* @generated from enum value: PROVENANCE_TRUST_LEVEL_SANITIZED = 2;
*/
SANITIZED = 2,
/**
* External content, not yet sanitized.
*
* @generated from enum value: PROVENANCE_TRUST_LEVEL_UNTRUSTED = 3;
*/
UNTRUSTED = 3,
/**
* Administratively revoked/tainted.
*
* @generated from enum value: PROVENANCE_TRUST_LEVEL_REVOKED = 4;
*/
REVOKED = 4,
}
/**
* Describes the enum llm_multiverse.v1.ProvenanceTrustLevel.
*/
export const ProvenanceTrustLevelSchema: GenEnum<ProvenanceTrustLevel> = /*@__PURE__*/
enumDesc(file_llm_multiverse_v1_memory, 1);
/**
* Vector-backed memory storage with staged retrieval.
*
* @generated from service llm_multiverse.v1.MemoryService
*/
export const MemoryService: GenService<{
/**
* Query memory with staged coarse-to-fine retrieval. Server-streaming.
*
* @generated from rpc llm_multiverse.v1.MemoryService.QueryMemory
*/
queryMemory: {
methodKind: "server_streaming";
input: typeof QueryMemoryRequestSchema;
output: typeof QueryMemoryResponseSchema;
},
/**
* Write or update a memory entry.
*
* @generated from rpc llm_multiverse.v1.MemoryService.WriteMemory
*/
writeMemory: {
methodKind: "unary";
input: typeof WriteMemoryRequestSchema;
output: typeof WriteMemoryResponseSchema;
},
/**
* Get correlated memory entries by ID.
*
* @generated from rpc llm_multiverse.v1.MemoryService.GetCorrelated
*/
getCorrelated: {
methodKind: "unary";
input: typeof GetCorrelatedRequestSchema;
output: typeof GetCorrelatedResponseSchema;
},
/**
* Mark memories as tainted/revoked (admin operation).
*
* @generated from rpc llm_multiverse.v1.MemoryService.RevokeMemory
*/
revokeMemory: {
methodKind: "unary";
input: typeof RevokeMemoryRequestSchema;
output: typeof RevokeMemoryResponseSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_llm_multiverse_v1_memory, 0);

View File

@@ -0,0 +1,339 @@
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file llm_multiverse/v1/model_gateway.proto (package llm_multiverse.v1, syntax proto3)
/* eslint-disable */
import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
import type { SessionContext } from "./common_pb";
import { file_llm_multiverse_v1_common } from "./common_pb";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file llm_multiverse/v1/model_gateway.proto.
*/
export const file_llm_multiverse_v1_model_gateway: GenFile = /*@__PURE__*/
fileDesc("CiVsbG1fbXVsdGl2ZXJzZS92MS9tb2RlbF9nYXRld2F5LnByb3RvEhFsbG1fbXVsdGl2ZXJzZS52MSKtAgoPSW5mZXJlbmNlUGFyYW1zEjIKB2NvbnRleHQYASABKAsyIS5sbG1fbXVsdGl2ZXJzZS52MS5TZXNzaW9uQ29udGV4dBIOCgZwcm9tcHQYAiABKAkSOgoPdGFza19jb21wbGV4aXR5GAMgASgOMiEubGxtX211bHRpdmVyc2UudjEuVGFza0NvbXBsZXhpdHkSEgoKbWF4X3Rva2VucxgEIAEoDRIYCgt0ZW1wZXJhdHVyZRgFIAEoAkgAiAEBEhIKBXRvcF9wGAYgASgCSAGIAQESFgoOc3RvcF9zZXF1ZW5jZXMYByADKAkSFwoKbW9kZWxfaGludBgIIAEoCUgCiAEBQg4KDF90ZW1wZXJhdHVyZUIICgZfdG9wX3BCDQoLX21vZGVsX2hpbnQiTAoWU3RyZWFtSW5mZXJlbmNlUmVxdWVzdBIyCgZwYXJhbXMYASABKAsyIi5sbG1fbXVsdGl2ZXJzZS52MS5JbmZlcmVuY2VQYXJhbXMiRgoQSW5mZXJlbmNlUmVxdWVzdBIyCgZwYXJhbXMYASABKAsyIi5sbG1fbXVsdGl2ZXJzZS52MS5JbmZlcmVuY2VQYXJhbXMiVgoXU3RyZWFtSW5mZXJlbmNlUmVzcG9uc2USDQoFdG9rZW4YASABKAkSGgoNZmluaXNoX3JlYXNvbhgCIAEoCUgAiAEBQhAKDl9maW5pc2hfcmVhc29uIk0KEUluZmVyZW5jZVJlc3BvbnNlEgwKBHRleHQYASABKAkSFQoNZmluaXNoX3JlYXNvbhgCIAEoCRITCgt0b2tlbnNfdXNlZBgDIAEoDSJ6ChhHZW5lcmF0ZUVtYmVkZGluZ1JlcXVlc3QSMgoHY29udGV4dBgBIAEoCzIhLmxsbV9tdWx0aXZlcnNlLnYxLlNlc3Npb25Db250ZXh0EgwKBHRleHQYAiABKAkSEgoFbW9kZWwYAyABKAlIAIgBAUIICgZfbW9kZWwiQgoZR2VuZXJhdGVFbWJlZGRpbmdSZXNwb25zZRIRCgllbWJlZGRpbmcYASADKAISEgoKZGltZW5zaW9ucxgCIAEoDSI9ChNJc01vZGVsUmVhZHlSZXF1ZXN0EhcKCm1vZGVsX25hbWUYASABKAlIAIgBAUINCgtfbW9kZWxfbmFtZSJtChRJc01vZGVsUmVhZHlSZXNwb25zZRINCgVyZWFkeRgBIAEoCBIYChBhdmFpbGFibGVfbW9kZWxzGAIgAygJEhoKDWVycm9yX21lc3NhZ2UYAyABKAlIAIgBAUIQCg5fZXJyb3JfbWVzc2FnZSpqCg5UYXNrQ29tcGxleGl0eRIfChtUQVNLX0NPTVBMRVhJVFlfVU5TUEVDSUZJRUQQABIaChZUQVNLX0NPTVBMRVhJVFlfU0lNUExFEAESGwoXVEFTS19DT01QTEVYSVRZX0NPTVBMRVgQAjKqAwoTTW9kZWxHYXRld2F5U2VydmljZRJqCg9TdHJlYW1JbmZlcmVuY2USKS5sbG1fbXVsdGl2ZXJzZS52MS5TdHJlYW1JbmZlcmVuY2VSZXF1ZXN0GioubGxtX211bHRpdmVyc2UudjEuU3RyZWFtSW5mZXJlbmNlUmVzcG9uc2UwARJWCglJbmZlcmVuY2USIy5sbG1fbXVsdGl2ZXJzZS52MS5JbmZlcmVuY2VSZXF1ZXN0GiQubGxtX211bHRpdmVyc2UudjEuSW5mZXJlbmNlUmVzcG9uc2USbgoRR2VuZXJhdGVFbWJlZGRpbmcSKy5sbG1fbXVsdGl2ZXJzZS52MS5HZW5lcmF0ZUVtYmVkZGluZ1JlcXVlc3QaLC5sbG1fbXVsdGl2ZXJzZS52MS5HZW5lcmF0ZUVtYmVkZGluZ1Jlc3BvbnNlEl8KDElzTW9kZWxSZWFkeRImLmxsbV9tdWx0aXZlcnNlLnYxLklzTW9kZWxSZWFkeVJlcXVlc3QaJy5sbG1fbXVsdGl2ZXJzZS52MS5Jc01vZGVsUmVhZHlSZXNwb25zZWIGcHJvdG8z", [file_llm_multiverse_v1_common]);
/**
* Shared inference parameters used by both streaming and unary RPCs.
*
* @generated from message llm_multiverse.v1.InferenceParams
*/
export type InferenceParams = Message<"llm_multiverse.v1.InferenceParams"> & {
/**
* @generated from field: llm_multiverse.v1.SessionContext context = 1;
*/
context?: SessionContext;
/**
* @generated from field: string prompt = 2;
*/
prompt: string;
/**
* @generated from field: llm_multiverse.v1.TaskComplexity task_complexity = 3;
*/
taskComplexity: TaskComplexity;
/**
* @generated from field: uint32 max_tokens = 4;
*/
maxTokens: number;
/**
* @generated from field: optional float temperature = 5;
*/
temperature?: number;
/**
* @generated from field: optional float top_p = 6;
*/
topP?: number;
/**
* @generated from field: repeated string stop_sequences = 7;
*/
stopSequences: string[];
/**
* Explicit model name or alias override. If set, bypasses task_complexity routing.
*
* @generated from field: optional string model_hint = 8;
*/
modelHint?: string;
};
/**
* Describes the message llm_multiverse.v1.InferenceParams.
* Use `create(InferenceParamsSchema)` to create a new message.
*/
export const InferenceParamsSchema: GenMessage<InferenceParams> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_model_gateway, 0);
/**
* @generated from message llm_multiverse.v1.StreamInferenceRequest
*/
export type StreamInferenceRequest = Message<"llm_multiverse.v1.StreamInferenceRequest"> & {
/**
* @generated from field: llm_multiverse.v1.InferenceParams params = 1;
*/
params?: InferenceParams;
};
/**
* Describes the message llm_multiverse.v1.StreamInferenceRequest.
* Use `create(StreamInferenceRequestSchema)` to create a new message.
*/
export const StreamInferenceRequestSchema: GenMessage<StreamInferenceRequest> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_model_gateway, 1);
/**
* @generated from message llm_multiverse.v1.InferenceRequest
*/
export type InferenceRequest = Message<"llm_multiverse.v1.InferenceRequest"> & {
/**
* @generated from field: llm_multiverse.v1.InferenceParams params = 1;
*/
params?: InferenceParams;
};
/**
* Describes the message llm_multiverse.v1.InferenceRequest.
* Use `create(InferenceRequestSchema)` to create a new message.
*/
export const InferenceRequestSchema: GenMessage<InferenceRequest> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_model_gateway, 2);
/**
* Single token in a streaming response.
*
* @generated from message llm_multiverse.v1.StreamInferenceResponse
*/
export type StreamInferenceResponse = Message<"llm_multiverse.v1.StreamInferenceResponse"> & {
/**
* @generated from field: string token = 1;
*/
token: string;
/**
* @generated from field: optional string finish_reason = 2;
*/
finishReason?: string;
};
/**
* Describes the message llm_multiverse.v1.StreamInferenceResponse.
* Use `create(StreamInferenceResponseSchema)` to create a new message.
*/
export const StreamInferenceResponseSchema: GenMessage<StreamInferenceResponse> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_model_gateway, 3);
/**
* Complete inference result.
*
* @generated from message llm_multiverse.v1.InferenceResponse
*/
export type InferenceResponse = Message<"llm_multiverse.v1.InferenceResponse"> & {
/**
* @generated from field: string text = 1;
*/
text: string;
/**
* @generated from field: string finish_reason = 2;
*/
finishReason: string;
/**
* @generated from field: uint32 tokens_used = 3;
*/
tokensUsed: number;
};
/**
* Describes the message llm_multiverse.v1.InferenceResponse.
* Use `create(InferenceResponseSchema)` to create a new message.
*/
export const InferenceResponseSchema: GenMessage<InferenceResponse> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_model_gateway, 4);
/**
* @generated from message llm_multiverse.v1.GenerateEmbeddingRequest
*/
export type GenerateEmbeddingRequest = Message<"llm_multiverse.v1.GenerateEmbeddingRequest"> & {
/**
* @generated from field: llm_multiverse.v1.SessionContext context = 1;
*/
context?: SessionContext;
/**
* @generated from field: string text = 2;
*/
text: string;
/**
* Model to use (defaults to nomic-embed-text).
*
* @generated from field: optional string model = 3;
*/
model?: string;
};
/**
* Describes the message llm_multiverse.v1.GenerateEmbeddingRequest.
* Use `create(GenerateEmbeddingRequestSchema)` to create a new message.
*/
export const GenerateEmbeddingRequestSchema: GenMessage<GenerateEmbeddingRequest> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_model_gateway, 5);
/**
* @generated from message llm_multiverse.v1.GenerateEmbeddingResponse
*/
export type GenerateEmbeddingResponse = Message<"llm_multiverse.v1.GenerateEmbeddingResponse"> & {
/**
* Raw embedding vector.
*
* @generated from field: repeated float embedding = 1;
*/
embedding: number[];
/**
* @generated from field: uint32 dimensions = 2;
*/
dimensions: number;
};
/**
* Describes the message llm_multiverse.v1.GenerateEmbeddingResponse.
* Use `create(GenerateEmbeddingResponseSchema)` to create a new message.
*/
export const GenerateEmbeddingResponseSchema: GenMessage<GenerateEmbeddingResponse> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_model_gateway, 6);
/**
* @generated from message llm_multiverse.v1.IsModelReadyRequest
*/
export type IsModelReadyRequest = Message<"llm_multiverse.v1.IsModelReadyRequest"> & {
/**
* Specific model to check. If empty, checks all configured models.
*
* @generated from field: optional string model_name = 1;
*/
modelName?: string;
};
/**
* Describes the message llm_multiverse.v1.IsModelReadyRequest.
* Use `create(IsModelReadyRequestSchema)` to create a new message.
*/
export const IsModelReadyRequestSchema: GenMessage<IsModelReadyRequest> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_model_gateway, 7);
/**
* @generated from message llm_multiverse.v1.IsModelReadyResponse
*/
export type IsModelReadyResponse = Message<"llm_multiverse.v1.IsModelReadyResponse"> & {
/**
* @generated from field: bool ready = 1;
*/
ready: boolean;
/**
* @generated from field: repeated string available_models = 2;
*/
availableModels: string[];
/**
* @generated from field: optional string error_message = 3;
*/
errorMessage?: string;
};
/**
* Describes the message llm_multiverse.v1.IsModelReadyResponse.
* Use `create(IsModelReadyResponseSchema)` to create a new message.
*/
export const IsModelReadyResponseSchema: GenMessage<IsModelReadyResponse> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_model_gateway, 8);
/**
* Hint for model routing based on task complexity.
*
* @generated from enum llm_multiverse.v1.TaskComplexity
*/
export enum TaskComplexity {
/**
* @generated from enum value: TASK_COMPLEXITY_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* Route to smaller models (3B/7B).
*
* @generated from enum value: TASK_COMPLEXITY_SIMPLE = 1;
*/
SIMPLE = 1,
/**
* Route to larger models (14B) for reasoning/code.
*
* @generated from enum value: TASK_COMPLEXITY_COMPLEX = 2;
*/
COMPLEX = 2,
}
/**
* Describes the enum llm_multiverse.v1.TaskComplexity.
*/
export const TaskComplexitySchema: GenEnum<TaskComplexity> = /*@__PURE__*/
enumDesc(file_llm_multiverse_v1_model_gateway, 0);
/**
* Wraps Ollama HTTP API, exposes inference via gRPC.
*
* @generated from service llm_multiverse.v1.ModelGatewayService
*/
export const ModelGatewayService: GenService<{
/**
* Streaming token-by-token inference.
*
* @generated from rpc llm_multiverse.v1.ModelGatewayService.StreamInference
*/
streamInference: {
methodKind: "server_streaming";
input: typeof StreamInferenceRequestSchema;
output: typeof StreamInferenceResponseSchema;
},
/**
* Synchronous full-text inference.
*
* @generated from rpc llm_multiverse.v1.ModelGatewayService.Inference
*/
inference: {
methodKind: "unary";
input: typeof InferenceRequestSchema;
output: typeof InferenceResponseSchema;
},
/**
* Generate embedding vector for text input.
*
* @generated from rpc llm_multiverse.v1.ModelGatewayService.GenerateEmbedding
*/
generateEmbedding: {
methodKind: "unary";
input: typeof GenerateEmbeddingRequestSchema;
output: typeof GenerateEmbeddingResponseSchema;
},
/**
* Check model availability.
*
* @generated from rpc llm_multiverse.v1.ModelGatewayService.IsModelReady
*/
isModelReady: {
methodKind: "unary";
input: typeof IsModelReadyRequestSchema;
output: typeof IsModelReadyResponseSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_llm_multiverse_v1_model_gateway, 0);

View File

@@ -0,0 +1,279 @@
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file llm_multiverse/v1/orchestrator.proto (package llm_multiverse.v1, syntax proto3)
/* eslint-disable */
import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
import type { AgentType, InferenceStats, OverrideLevel, SessionContext, SubagentResult, ToolType } from "./common_pb";
import { file_llm_multiverse_v1_common } from "./common_pb";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file llm_multiverse/v1/orchestrator.proto.
*/
export const file_llm_multiverse_v1_orchestrator: GenFile = /*@__PURE__*/
fileDesc("CiRsbG1fbXVsdGl2ZXJzZS92MS9vcmNoZXN0cmF0b3IucHJvdG8SEWxsbV9tdWx0aXZlcnNlLnYxIn4KDVNlc3Npb25Db25maWcSOAoOb3ZlcnJpZGVfbGV2ZWwYASABKA4yIC5sbG1fbXVsdGl2ZXJzZS52MS5PdmVycmlkZUxldmVsEhYKDmRpc2FibGVkX3Rvb2xzGAIgAygJEhsKE2dyYW50ZWRfcGVybWlzc2lvbnMYAyADKAkirwEKEVN1YnRhc2tEZWZpbml0aW9uEgoKAmlkGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEjAKCmFnZW50X3R5cGUYAyABKA4yHC5sbG1fbXVsdGl2ZXJzZS52MS5BZ2VudFR5cGUSEgoKZGVwZW5kc19vbhgEIAMoCRIzCg50b29sc19yZXF1aXJlZBgFIAMoDjIbLmxsbV9tdWx0aXZlcnNlLnYxLlRvb2xUeXBlIoYCCg9TdWJhZ2VudFJlcXVlc3QSMgoHY29udGV4dBgBIAEoCzIhLmxsbV9tdWx0aXZlcnNlLnYxLlNlc3Npb25Db250ZXh0EhAKCGFnZW50X2lkGAIgASgJEjAKCmFnZW50X3R5cGUYAyABKA4yHC5sbG1fbXVsdGl2ZXJzZS52MS5BZ2VudFR5cGUSDAoEdGFzaxgEIAEoCRIfChdyZWxldmFudF9tZW1vcnlfY29udGV4dBgFIAMoCRISCgptYXhfdG9rZW5zGAYgASgNEjgKDnNlc3Npb25fY29uZmlnGAcgASgLMiAubGxtX211bHRpdmVyc2UudjEuU2Vzc2lvbkNvbmZpZyKTAQoVUHJvY2Vzc1JlcXVlc3RSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSFAoMdXNlcl9tZXNzYWdlGAIgASgJEj0KDnNlc3Npb25fY29uZmlnGAMgASgLMiAubGxtX211bHRpdmVyc2UudjEuU2Vzc2lvbkNvbmZpZ0gAiAEBQhEKD19zZXNzaW9uX2NvbmZpZyK9AgoWUHJvY2Vzc1JlcXVlc3RSZXNwb25zZRI0CgVzdGF0ZRgBIAEoDjIlLmxsbV9tdWx0aXZlcnNlLnYxLk9yY2hlc3RyYXRpb25TdGF0ZRIPCgdtZXNzYWdlGAIgASgJEiAKE2ludGVybWVkaWF0ZV9yZXN1bHQYAyABKAlIAIgBARI8CgxmaW5hbF9yZXN1bHQYBCABKAsyIS5sbG1fbXVsdGl2ZXJzZS52MS5TdWJhZ2VudFJlc3VsdEgBiAEBEj8KD2luZmVyZW5jZV9zdGF0cxgFIAEoCzIhLmxsbV9tdWx0aXZlcnNlLnYxLkluZmVyZW5jZVN0YXRzSAKIAQFCFgoUX2ludGVybWVkaWF0ZV9yZXN1bHRCDwoNX2ZpbmFsX3Jlc3VsdEISChBfaW5mZXJlbmNlX3N0YXRzKuwBChJPcmNoZXN0cmF0aW9uU3RhdGUSIwofT1JDSEVTVFJBVElPTl9TVEFURV9VTlNQRUNJRklFRBAAEiMKH09SQ0hFU1RSQVRJT05fU1RBVEVfREVDT01QT1NJTkcQARIjCh9PUkNIRVNUUkFUSU9OX1NUQVRFX0RJU1BBVENISU5HEAISIQodT1JDSEVTVFJBVElPTl9TVEFURV9FWEVDVVRJTkcQAxIiCh5PUkNIRVNUUkFUSU9OX1NUQVRFX0NPTVBBQ1RJTkcQBBIgChxPUkNIRVNUUkFUSU9OX1NUQVRFX0NPTVBMRVRFEAUyfgoTT3JjaGVzdHJhdG9yU2VydmljZRJnCg5Qcm9jZXNzUmVxdWVzdBIoLmxsbV9tdWx0aXZlcnNlLnYxLlByb2Nlc3NSZXF1ZXN0UmVxdWVzdBopLmxsbV9tdWx0aXZlcnNlLnYxLlByb2Nlc3NSZXF1ZXN0UmVzcG9uc2UwAWIGcHJvdG8z", [file_llm_multiverse_v1_common]);
/**
* Per-session configuration for override control.
*
* @generated from message llm_multiverse.v1.SessionConfig
*/
export type SessionConfig = Message<"llm_multiverse.v1.SessionConfig"> & {
/**
* @generated from field: llm_multiverse.v1.OverrideLevel override_level = 1;
*/
overrideLevel: OverrideLevel;
/**
* Tools explicitly disabled for this session.
*
* @generated from field: repeated string disabled_tools = 2;
*/
disabledTools: string[];
/**
* Explicit grants in "agent_type:tool" format.
*
* @generated from field: repeated string granted_permissions = 3;
*/
grantedPermissions: string[];
};
/**
* Describes the message llm_multiverse.v1.SessionConfig.
* Use `create(SessionConfigSchema)` to create a new message.
*/
export const SessionConfigSchema: GenMessage<SessionConfig> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_orchestrator, 0);
/**
* A subtask decomposed from a user request.
*
* @generated from message llm_multiverse.v1.SubtaskDefinition
*/
export type SubtaskDefinition = Message<"llm_multiverse.v1.SubtaskDefinition"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
/**
* @generated from field: string description = 2;
*/
description: string;
/**
* @generated from field: llm_multiverse.v1.AgentType agent_type = 3;
*/
agentType: AgentType;
/**
* IDs of subtasks this depends on.
*
* @generated from field: repeated string depends_on = 4;
*/
dependsOn: string[];
/**
* @generated from field: repeated llm_multiverse.v1.ToolType tools_required = 5;
*/
toolsRequired: ToolType[];
};
/**
* Describes the message llm_multiverse.v1.SubtaskDefinition.
* Use `create(SubtaskDefinitionSchema)` to create a new message.
*/
export const SubtaskDefinitionSchema: GenMessage<SubtaskDefinition> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_orchestrator, 1);
/**
* Internal request dispatched to a subagent.
*
* @generated from message llm_multiverse.v1.SubagentRequest
*/
export type SubagentRequest = Message<"llm_multiverse.v1.SubagentRequest"> & {
/**
* @generated from field: llm_multiverse.v1.SessionContext context = 1;
*/
context?: SessionContext;
/**
* @generated from field: string agent_id = 2;
*/
agentId: string;
/**
* @generated from field: llm_multiverse.v1.AgentType agent_type = 3;
*/
agentType: AgentType;
/**
* @generated from field: string task = 4;
*/
task: string;
/**
* Relevant memory summaries (max extracted segments only).
*
* @generated from field: repeated string relevant_memory_context = 5;
*/
relevantMemoryContext: string[];
/**
* @generated from field: uint32 max_tokens = 6;
*/
maxTokens: number;
/**
* @generated from field: llm_multiverse.v1.SessionConfig session_config = 7;
*/
sessionConfig?: SessionConfig;
};
/**
* Describes the message llm_multiverse.v1.SubagentRequest.
* Use `create(SubagentRequestSchema)` to create a new message.
*/
export const SubagentRequestSchema: GenMessage<SubagentRequest> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_orchestrator, 2);
/**
* User-facing request to the orchestrator.
*
* @generated from message llm_multiverse.v1.ProcessRequestRequest
*/
export type ProcessRequestRequest = Message<"llm_multiverse.v1.ProcessRequestRequest"> & {
/**
* @generated from field: string session_id = 1;
*/
sessionId: string;
/**
* @generated from field: string user_message = 2;
*/
userMessage: string;
/**
* @generated from field: optional llm_multiverse.v1.SessionConfig session_config = 3;
*/
sessionConfig?: SessionConfig;
};
/**
* Describes the message llm_multiverse.v1.ProcessRequestRequest.
* Use `create(ProcessRequestRequestSchema)` to create a new message.
*/
export const ProcessRequestRequestSchema: GenMessage<ProcessRequestRequest> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_orchestrator, 3);
/**
* Streamed orchestration progress.
*
* @generated from message llm_multiverse.v1.ProcessRequestResponse
*/
export type ProcessRequestResponse = Message<"llm_multiverse.v1.ProcessRequestResponse"> & {
/**
* @generated from field: llm_multiverse.v1.OrchestrationState state = 1;
*/
state: OrchestrationState;
/**
* User-facing status message or summary.
*
* @generated from field: string message = 2;
*/
message: string;
/**
* Intermediate result for UX feedback.
*
* @generated from field: optional string intermediate_result = 3;
*/
intermediateResult?: string;
/**
* Final subagent result when state is COMPLETE.
*
* @generated from field: optional llm_multiverse.v1.SubagentResult final_result = 4;
*/
finalResult?: SubagentResult;
/**
* Inference statistics from the model-gateway (on the final streamed message).
*
* @generated from field: optional llm_multiverse.v1.InferenceStats inference_stats = 5;
*/
inferenceStats?: InferenceStats;
};
/**
* Describes the message llm_multiverse.v1.ProcessRequestResponse.
* Use `create(ProcessRequestResponseSchema)` to create a new message.
*/
export const ProcessRequestResponseSchema: GenMessage<ProcessRequestResponse> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_orchestrator, 4);
/**
* Orchestration lifecycle states.
*
* @generated from enum llm_multiverse.v1.OrchestrationState
*/
export enum OrchestrationState {
/**
* @generated from enum value: ORCHESTRATION_STATE_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* @generated from enum value: ORCHESTRATION_STATE_DECOMPOSING = 1;
*/
DECOMPOSING = 1,
/**
* @generated from enum value: ORCHESTRATION_STATE_DISPATCHING = 2;
*/
DISPATCHING = 2,
/**
* @generated from enum value: ORCHESTRATION_STATE_EXECUTING = 3;
*/
EXECUTING = 3,
/**
* @generated from enum value: ORCHESTRATION_STATE_COMPACTING = 4;
*/
COMPACTING = 4,
/**
* @generated from enum value: ORCHESTRATION_STATE_COMPLETE = 5;
*/
COMPLETE = 5,
}
/**
* Describes the enum llm_multiverse.v1.OrchestrationState.
*/
export const OrchestrationStateSchema: GenEnum<OrchestrationState> = /*@__PURE__*/
enumDesc(file_llm_multiverse_v1_orchestrator, 0);
/**
* Entry point for user requests. Decomposes tasks and dispatches to subagents.
*
* @generated from service llm_multiverse.v1.OrchestratorService
*/
export const OrchestratorService: GenService<{
/**
* Process a user request. Server-streaming for progressive status updates.
*
* @generated from rpc llm_multiverse.v1.OrchestratorService.ProcessRequest
*/
processRequest: {
methodKind: "server_streaming";
input: typeof ProcessRequestRequestSchema;
output: typeof ProcessRequestResponseSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_llm_multiverse_v1_orchestrator, 0);

View File

@@ -0,0 +1,133 @@
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file llm_multiverse/v1/search.proto (package llm_multiverse.v1, syntax proto3)
/* eslint-disable */
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
import type { SessionContext } from "./common_pb";
import { file_llm_multiverse_v1_common } from "./common_pb";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file llm_multiverse/v1/search.proto.
*/
export const file_llm_multiverse_v1_search: GenFile = /*@__PURE__*/
fileDesc("Ch5sbG1fbXVsdGl2ZXJzZS92MS9zZWFyY2gucHJvdG8SEWxsbV9tdWx0aXZlcnNlLnYxImcKDVNlYXJjaFJlcXVlc3QSMgoHY29udGV4dBgBIAEoCzIhLmxsbV9tdWx0aXZlcnNlLnYxLlNlc3Npb25Db250ZXh0Eg0KBXF1ZXJ5GAIgASgJEhMKC251bV9yZXN1bHRzGAMgASgNImQKDFNlYXJjaFJlc3VsdBINCgVjbGFpbRgBIAEoCRISCgpzb3VyY2VfdXJsGAIgASgJEhIKCmNvbmZpZGVuY2UYAyABKAISDAoEZGF0ZRgEIAEoCRIPCgdzdW1tYXJ5GAUgASgJInAKDlNlYXJjaFJlc3BvbnNlEjAKB3Jlc3VsdHMYASADKAsyHy5sbG1fbXVsdGl2ZXJzZS52MS5TZWFyY2hSZXN1bHQSGgoNZXJyb3JfbWVzc2FnZRgCIAEoCUgAiAEBQhAKDl9lcnJvcl9tZXNzYWdlMl4KDVNlYXJjaFNlcnZpY2USTQoGU2VhcmNoEiAubGxtX211bHRpdmVyc2UudjEuU2VhcmNoUmVxdWVzdBohLmxsbV9tdWx0aXZlcnNlLnYxLlNlYXJjaFJlc3BvbnNlYgZwcm90bzM", [file_llm_multiverse_v1_common]);
/**
* @generated from message llm_multiverse.v1.SearchRequest
*/
export type SearchRequest = Message<"llm_multiverse.v1.SearchRequest"> & {
/**
* @generated from field: llm_multiverse.v1.SessionContext context = 1;
*/
context?: SessionContext;
/**
* @generated from field: string query = 2;
*/
query: string;
/**
* Number of results to return (default 5).
*
* @generated from field: uint32 num_results = 3;
*/
numResults: number;
};
/**
* Describes the message llm_multiverse.v1.SearchRequest.
* Use `create(SearchRequestSchema)` to create a new message.
*/
export const SearchRequestSchema: GenMessage<SearchRequest> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_search, 0);
/**
* A single search result, summarized and structured.
*
* @generated from message llm_multiverse.v1.SearchResult
*/
export type SearchResult = Message<"llm_multiverse.v1.SearchResult"> & {
/**
* Key assertion extracted from the content.
*
* @generated from field: string claim = 1;
*/
claim: string;
/**
* @generated from field: string source_url = 2;
*/
sourceUrl: string;
/**
* Confidence score (0.0-1.0).
*
* @generated from field: float confidence = 3;
*/
confidence: number;
/**
* Publication or extraction date (ISO 8601).
*
* @generated from field: string date = 4;
*/
date: string;
/**
* Summarized content — never raw web content.
*
* @generated from field: string summary = 5;
*/
summary: string;
};
/**
* Describes the message llm_multiverse.v1.SearchResult.
* Use `create(SearchResultSchema)` to create a new message.
*/
export const SearchResultSchema: GenMessage<SearchResult> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_search, 1);
/**
* @generated from message llm_multiverse.v1.SearchResponse
*/
export type SearchResponse = Message<"llm_multiverse.v1.SearchResponse"> & {
/**
* @generated from field: repeated llm_multiverse.v1.SearchResult results = 1;
*/
results: SearchResult[];
/**
* @generated from field: optional string error_message = 2;
*/
errorMessage?: string;
};
/**
* Describes the message llm_multiverse.v1.SearchResponse.
* Use `create(SearchResponseSchema)` to create a new message.
*/
export const SearchResponseSchema: GenMessage<SearchResponse> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_search, 2);
/**
* Web search via local SearXNG instance with summarization pipeline.
*
* @generated from service llm_multiverse.v1.SearchService
*/
export const SearchService: GenService<{
/**
* Search the web. Returns summarized results — never raw web content.
*
* @generated from rpc llm_multiverse.v1.SearchService.Search
*/
search: {
methodKind: "unary";
input: typeof SearchRequestSchema;
output: typeof SearchResponseSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_llm_multiverse_v1_search, 0);

View File

@@ -0,0 +1,84 @@
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file llm_multiverse/v1/secrets.proto (package llm_multiverse.v1, syntax proto3)
/* eslint-disable */
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
import type { SessionContext } from "./common_pb";
import { file_llm_multiverse_v1_common } from "./common_pb";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file llm_multiverse/v1/secrets.proto.
*/
export const file_llm_multiverse_v1_secrets: GenFile = /*@__PURE__*/
fileDesc("Ch9sbG1fbXVsdGl2ZXJzZS92MS9zZWNyZXRzLnByb3RvEhFsbG1fbXVsdGl2ZXJzZS52MSJbChBHZXRTZWNyZXRSZXF1ZXN0EjIKB2NvbnRleHQYASABKAsyIS5sbG1fbXVsdGl2ZXJzZS52MS5TZXNzaW9uQ29udGV4dBITCgtzZWNyZXRfbmFtZRgCIAEoCSJQChFHZXRTZWNyZXRSZXNwb25zZRINCgV2YWx1ZRgBIAEoCRIaCg1lcnJvcl9tZXNzYWdlGAIgASgJSACIAQFCEAoOX2Vycm9yX21lc3NhZ2UyaAoOU2VjcmV0c1NlcnZpY2USVgoJR2V0U2VjcmV0EiMubGxtX211bHRpdmVyc2UudjEuR2V0U2VjcmV0UmVxdWVzdBokLmxsbV9tdWx0aXZlcnNlLnYxLkdldFNlY3JldFJlc3BvbnNlYgZwcm90bzM", [file_llm_multiverse_v1_common]);
/**
* @generated from message llm_multiverse.v1.GetSecretRequest
*/
export type GetSecretRequest = Message<"llm_multiverse.v1.GetSecretRequest"> & {
/**
* @generated from field: llm_multiverse.v1.SessionContext context = 1;
*/
context?: SessionContext;
/**
* Secret name as declared in tool definition placeholder.
*
* @generated from field: string secret_name = 2;
*/
secretName: string;
};
/**
* Describes the message llm_multiverse.v1.GetSecretRequest.
* Use `create(GetSecretRequestSchema)` to create a new message.
*/
export const GetSecretRequestSchema: GenMessage<GetSecretRequest> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_secrets, 0);
/**
* @generated from message llm_multiverse.v1.GetSecretResponse
*/
export type GetSecretResponse = Message<"llm_multiverse.v1.GetSecretResponse"> & {
/**
* The actual credential value. Never exposed in logs or agent context.
*
* @generated from field: string value = 1;
*/
value: string;
/**
* @generated from field: optional string error_message = 2;
*/
errorMessage?: string;
};
/**
* Describes the message llm_multiverse.v1.GetSecretResponse.
* Use `create(GetSecretResponseSchema)` to create a new message.
*/
export const GetSecretResponseSchema: GenMessage<GetSecretResponse> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_secrets, 1);
/**
* Credential retrieval via Linux Secret Service API.
* Only the Tool Broker is authorized to call this service.
*
* @generated from service llm_multiverse.v1.SecretsService
*/
export const SecretsService: GenService<{
/**
* Retrieve a secret by name. No list or write RPCs exposed.
*
* @generated from rpc llm_multiverse.v1.SecretsService.GetSecret
*/
getSecret: {
methodKind: "unary";
input: typeof GetSecretRequestSchema;
output: typeof GetSecretResponseSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_llm_multiverse_v1_secrets, 0);

View File

@@ -0,0 +1,376 @@
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
// @generated from file llm_multiverse/v1/tool_broker.proto (package llm_multiverse.v1, syntax proto3)
/* eslint-disable */
import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
import type { AgentType, SessionContext } from "./common_pb";
import { file_llm_multiverse_v1_common } from "./common_pb";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file llm_multiverse/v1/tool_broker.proto.
*/
export const file_llm_multiverse_v1_tool_broker: GenFile = /*@__PURE__*/
fileDesc("CiNsbG1fbXVsdGl2ZXJzZS92MS90b29sX2Jyb2tlci5wcm90bxIRbGxtX211bHRpdmVyc2UudjEiYgoPUGFyYW1ldGVyU2NoZW1hEgwKBHR5cGUYASABKAkSEwoLZGVzY3JpcHRpb24YAiABKAkSGgoNZGVmYXVsdF92YWx1ZRgDIAEoCUgAiAEBQhAKDl9kZWZhdWx0X3ZhbHVlIqQCCg5Ub29sRGVmaW5pdGlvbhIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEkUKCnBhcmFtZXRlcnMYAyADKAsyMS5sbG1fbXVsdGl2ZXJzZS52MS5Ub29sRGVmaW5pdGlvbi5QYXJhbWV0ZXJzRW50cnkSFwoPcmVxdWlyZWRfcGFyYW1zGAQgAygJEiAKE3JlcXVpcmVzX2NyZWRlbnRpYWwYBSABKAlIAIgBARpVCg9QYXJhbWV0ZXJzRW50cnkSCwoDa2V5GAEgASgJEjEKBXZhbHVlGAIgASgLMiIubGxtX211bHRpdmVyc2UudjEuUGFyYW1ldGVyU2NoZW1hOgI4AUIWChRfcmVxdWlyZXNfY3JlZGVudGlhbCKWAQoURGlzY292ZXJUb29sc1JlcXVlc3QSMgoHY29udGV4dBgBIAEoCzIhLmxsbV9tdWx0aXZlcnNlLnYxLlNlc3Npb25Db250ZXh0EjAKCmFnZW50X3R5cGUYAiABKA4yHC5sbG1fbXVsdGl2ZXJzZS52MS5BZ2VudFR5cGUSGAoQdGFza19kZXNjcmlwdGlvbhgDIAEoCSKBAQoVRGlzY292ZXJUb29sc1Jlc3BvbnNlEjoKD2F2YWlsYWJsZV90b29scxgBIAMoCzIhLmxsbV9tdWx0aXZlcnNlLnYxLlRvb2xEZWZpbml0aW9uEhoKDWVycm9yX21lc3NhZ2UYAiABKAlIAIgBAUIQCg5fZXJyb3JfbWVzc2FnZSKLAgoSRXhlY3V0ZVRvb2xSZXF1ZXN0EjIKB2NvbnRleHQYASABKAsyIS5sbG1fbXVsdGl2ZXJzZS52MS5TZXNzaW9uQ29udGV4dBIwCgphZ2VudF90eXBlGAIgASgOMhwubGxtX211bHRpdmVyc2UudjEuQWdlbnRUeXBlEhEKCXRvb2xfbmFtZRgDIAEoCRJJCgpwYXJhbWV0ZXJzGAQgAygLMjUubGxtX211bHRpdmVyc2UudjEuRXhlY3V0ZVRvb2xSZXF1ZXN0LlBhcmFtZXRlcnNFbnRyeRoxCg9QYXJhbWV0ZXJzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKHAQoTRXhlY3V0ZVRvb2xSZXNwb25zZRIOCgZvdXRwdXQYASABKAkSMgoGc3RhdHVzGAIgASgOMiIubGxtX211bHRpdmVyc2UudjEuRXhlY3V0aW9uU3RhdHVzEhoKDWVycm9yX21lc3NhZ2UYAyABKAlIAIgBAUIQCg5fZXJyb3JfbWVzc2FnZSKNAgoTVmFsaWRhdGVDYWxsUmVxdWVzdBIyCgdjb250ZXh0GAEgASgLMiEubGxtX211bHRpdmVyc2UudjEuU2Vzc2lvbkNvbnRleHQSMAoKYWdlbnRfdHlwZRgCIAEoDjIcLmxsbV9tdWx0aXZlcnNlLnYxLkFnZW50VHlwZRIRCgl0b29sX25hbWUYAyABKAkSSgoKcGFyYW1ldGVycxgEIAMoCzI2LmxsbV9tdWx0aXZlcnNlLnYxLlZhbGlkYXRlQ2FsbFJlcXVlc3QuUGFyYW1ldGVyc0VudHJ5GjEKD1BhcmFtZXRlcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIpgBChRWYWxpZGF0ZUNhbGxSZXNwb25zZRISCgppc19hbGxvd2VkGAEgASgIEhoKDWRlbmlhbF9yZWFzb24YAiABKAlIAIgBARI+ChFlbmZvcmNlbWVudF9sYXllchgDIAEoDjIjLmxsbV9tdWx0aXZlcnNlLnYxLkVuZm9yY2VtZW50TGF5ZXJCEAoOX2RlbmlhbF9yZWFzb24qjQEKD0V4ZWN1dGlvblN0YXR1cxIgChxFWEVDVVRJT05fU1RBVFVTX1VOU1BFQ0lGSUVEEAASHAoYRVhFQ1VUSU9OX1NUQVRVU19SVU5OSU5HEAESHAoYRVhFQ1VUSU9OX1NUQVRVU19TVUNDRVNTEAISHAoYRVhFQ1VUSU9OX1NUQVRVU19GQUlMVVJFEAMq+QEKEEVuZm9yY2VtZW50TGF5ZXISIQodRU5GT1JDRU1FTlRfTEFZRVJfVU5TUEVDSUZJRUQQABImCiJFTkZPUkNFTUVOVF9MQVlFUl9TRVNTSU9OX09WRVJSSURFEAESJAogRU5GT1JDRU1FTlRfTEFZRVJfQUdFTlRfTUFOSUZFU1QQAhIoCiRFTkZPUkNFTUVOVF9MQVlFUl9MSU5FQUdFX0NPTlNUUkFJTlQQAxIkCiBFTkZPUkNFTUVOVF9MQVlFUl9QQVRIX0FMTE9XTElTVBAEEiQKIEVORk9SQ0VNRU5UX0xBWUVSX05FVFdPUktfRUdSRVNTEAUyuAIKEVRvb2xCcm9rZXJTZXJ2aWNlEmIKDURpc2NvdmVyVG9vbHMSJy5sbG1fbXVsdGl2ZXJzZS52MS5EaXNjb3ZlclRvb2xzUmVxdWVzdBooLmxsbV9tdWx0aXZlcnNlLnYxLkRpc2NvdmVyVG9vbHNSZXNwb25zZRJeCgtFeGVjdXRlVG9vbBIlLmxsbV9tdWx0aXZlcnNlLnYxLkV4ZWN1dGVUb29sUmVxdWVzdBomLmxsbV9tdWx0aXZlcnNlLnYxLkV4ZWN1dGVUb29sUmVzcG9uc2UwARJfCgxWYWxpZGF0ZUNhbGwSJi5sbG1fbXVsdGl2ZXJzZS52MS5WYWxpZGF0ZUNhbGxSZXF1ZXN0GicubGxtX211bHRpdmVyc2UudjEuVmFsaWRhdGVDYWxsUmVzcG9uc2ViBnByb3RvMw", [file_llm_multiverse_v1_common]);
/**
* Schema for a tool parameter.
*
* @generated from message llm_multiverse.v1.ParameterSchema
*/
export type ParameterSchema = Message<"llm_multiverse.v1.ParameterSchema"> & {
/**
* @generated from field: string type = 1;
*/
type: string;
/**
* @generated from field: string description = 2;
*/
description: string;
/**
* @generated from field: optional string default_value = 3;
*/
defaultValue?: string;
};
/**
* Describes the message llm_multiverse.v1.ParameterSchema.
* Use `create(ParameterSchemaSchema)` to create a new message.
*/
export const ParameterSchemaSchema: GenMessage<ParameterSchema> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_tool_broker, 0);
/**
* Definition of a tool available through the broker.
*
* @generated from message llm_multiverse.v1.ToolDefinition
*/
export type ToolDefinition = Message<"llm_multiverse.v1.ToolDefinition"> & {
/**
* @generated from field: string name = 1;
*/
name: string;
/**
* @generated from field: string description = 2;
*/
description: string;
/**
* @generated from field: map<string, llm_multiverse.v1.ParameterSchema> parameters = 3;
*/
parameters: { [key: string]: ParameterSchema };
/**
* @generated from field: repeated string required_params = 4;
*/
requiredParams: string[];
/**
* Credential placeholder name, if tool requires a secret.
*
* @generated from field: optional string requires_credential = 5;
*/
requiresCredential?: string;
};
/**
* Describes the message llm_multiverse.v1.ToolDefinition.
* Use `create(ToolDefinitionSchema)` to create a new message.
*/
export const ToolDefinitionSchema: GenMessage<ToolDefinition> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_tool_broker, 1);
/**
* @generated from message llm_multiverse.v1.DiscoverToolsRequest
*/
export type DiscoverToolsRequest = Message<"llm_multiverse.v1.DiscoverToolsRequest"> & {
/**
* @generated from field: llm_multiverse.v1.SessionContext context = 1;
*/
context?: SessionContext;
/**
* @generated from field: llm_multiverse.v1.AgentType agent_type = 2;
*/
agentType: AgentType;
/**
* Brief task description for tool selector model call.
*
* @generated from field: string task_description = 3;
*/
taskDescription: string;
};
/**
* Describes the message llm_multiverse.v1.DiscoverToolsRequest.
* Use `create(DiscoverToolsRequestSchema)` to create a new message.
*/
export const DiscoverToolsRequestSchema: GenMessage<DiscoverToolsRequest> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_tool_broker, 2);
/**
* @generated from message llm_multiverse.v1.DiscoverToolsResponse
*/
export type DiscoverToolsResponse = Message<"llm_multiverse.v1.DiscoverToolsResponse"> & {
/**
* Filtered tools (typically 2-4) relevant to the agent and task.
*
* @generated from field: repeated llm_multiverse.v1.ToolDefinition available_tools = 1;
*/
availableTools: ToolDefinition[];
/**
* @generated from field: optional string error_message = 2;
*/
errorMessage?: string;
};
/**
* Describes the message llm_multiverse.v1.DiscoverToolsResponse.
* Use `create(DiscoverToolsResponseSchema)` to create a new message.
*/
export const DiscoverToolsResponseSchema: GenMessage<DiscoverToolsResponse> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_tool_broker, 3);
/**
* @generated from message llm_multiverse.v1.ExecuteToolRequest
*/
export type ExecuteToolRequest = Message<"llm_multiverse.v1.ExecuteToolRequest"> & {
/**
* @generated from field: llm_multiverse.v1.SessionContext context = 1;
*/
context?: SessionContext;
/**
* @generated from field: llm_multiverse.v1.AgentType agent_type = 2;
*/
agentType: AgentType;
/**
* @generated from field: string tool_name = 3;
*/
toolName: string;
/**
* @generated from field: map<string, string> parameters = 4;
*/
parameters: { [key: string]: string };
};
/**
* Describes the message llm_multiverse.v1.ExecuteToolRequest.
* Use `create(ExecuteToolRequestSchema)` to create a new message.
*/
export const ExecuteToolRequestSchema: GenMessage<ExecuteToolRequest> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_tool_broker, 4);
/**
* Streamed tool execution output. All results tagged [TOOL_RESULT: UNTRUSTED].
*
* @generated from message llm_multiverse.v1.ExecuteToolResponse
*/
export type ExecuteToolResponse = Message<"llm_multiverse.v1.ExecuteToolResponse"> & {
/**
* @generated from field: string output = 1;
*/
output: string;
/**
* @generated from field: llm_multiverse.v1.ExecutionStatus status = 2;
*/
status: ExecutionStatus;
/**
* @generated from field: optional string error_message = 3;
*/
errorMessage?: string;
};
/**
* Describes the message llm_multiverse.v1.ExecuteToolResponse.
* Use `create(ExecuteToolResponseSchema)` to create a new message.
*/
export const ExecuteToolResponseSchema: GenMessage<ExecuteToolResponse> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_tool_broker, 5);
/**
* @generated from message llm_multiverse.v1.ValidateCallRequest
*/
export type ValidateCallRequest = Message<"llm_multiverse.v1.ValidateCallRequest"> & {
/**
* @generated from field: llm_multiverse.v1.SessionContext context = 1;
*/
context?: SessionContext;
/**
* @generated from field: llm_multiverse.v1.AgentType agent_type = 2;
*/
agentType: AgentType;
/**
* @generated from field: string tool_name = 3;
*/
toolName: string;
/**
* @generated from field: map<string, string> parameters = 4;
*/
parameters: { [key: string]: string };
};
/**
* Describes the message llm_multiverse.v1.ValidateCallRequest.
* Use `create(ValidateCallRequestSchema)` to create a new message.
*/
export const ValidateCallRequestSchema: GenMessage<ValidateCallRequest> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_tool_broker, 6);
/**
* @generated from message llm_multiverse.v1.ValidateCallResponse
*/
export type ValidateCallResponse = Message<"llm_multiverse.v1.ValidateCallResponse"> & {
/**
* @generated from field: bool is_allowed = 1;
*/
isAllowed: boolean;
/**
* @generated from field: optional string denial_reason = 2;
*/
denialReason?: string;
/**
* Which enforcement layer would deny (for audit).
*
* @generated from field: llm_multiverse.v1.EnforcementLayer enforcement_layer = 3;
*/
enforcementLayer: EnforcementLayer;
};
/**
* Describes the message llm_multiverse.v1.ValidateCallResponse.
* Use `create(ValidateCallResponseSchema)` to create a new message.
*/
export const ValidateCallResponseSchema: GenMessage<ValidateCallResponse> = /*@__PURE__*/
messageDesc(file_llm_multiverse_v1_tool_broker, 7);
/**
* Execution status of a tool call.
*
* @generated from enum llm_multiverse.v1.ExecutionStatus
*/
export enum ExecutionStatus {
/**
* @generated from enum value: EXECUTION_STATUS_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* @generated from enum value: EXECUTION_STATUS_RUNNING = 1;
*/
RUNNING = 1,
/**
* @generated from enum value: EXECUTION_STATUS_SUCCESS = 2;
*/
SUCCESS = 2,
/**
* @generated from enum value: EXECUTION_STATUS_FAILURE = 3;
*/
FAILURE = 3,
}
/**
* Describes the enum llm_multiverse.v1.ExecutionStatus.
*/
export const ExecutionStatusSchema: GenEnum<ExecutionStatus> = /*@__PURE__*/
enumDesc(file_llm_multiverse_v1_tool_broker, 0);
/**
* Which enforcement layer denied a tool call.
*
* @generated from enum llm_multiverse.v1.EnforcementLayer
*/
export enum EnforcementLayer {
/**
* @generated from enum value: ENFORCEMENT_LAYER_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* @generated from enum value: ENFORCEMENT_LAYER_SESSION_OVERRIDE = 1;
*/
SESSION_OVERRIDE = 1,
/**
* @generated from enum value: ENFORCEMENT_LAYER_AGENT_MANIFEST = 2;
*/
AGENT_MANIFEST = 2,
/**
* @generated from enum value: ENFORCEMENT_LAYER_LINEAGE_CONSTRAINT = 3;
*/
LINEAGE_CONSTRAINT = 3,
/**
* @generated from enum value: ENFORCEMENT_LAYER_PATH_ALLOWLIST = 4;
*/
PATH_ALLOWLIST = 4,
/**
* @generated from enum value: ENFORCEMENT_LAYER_NETWORK_EGRESS = 5;
*/
NETWORK_EGRESS = 5,
}
/**
* Describes the enum llm_multiverse.v1.EnforcementLayer.
*/
export const EnforcementLayerSchema: GenEnum<EnforcementLayer> = /*@__PURE__*/
enumDesc(file_llm_multiverse_v1_tool_broker, 1);
/**
* Single enforcement point for all tool calls.
*
* @generated from service llm_multiverse.v1.ToolBrokerService
*/
export const ToolBrokerService: GenService<{
/**
* Discover available tools for an agent based on type, lineage, and task.
*
* @generated from rpc llm_multiverse.v1.ToolBrokerService.DiscoverTools
*/
discoverTools: {
methodKind: "unary";
input: typeof DiscoverToolsRequestSchema;
output: typeof DiscoverToolsResponseSchema;
},
/**
* Execute a tool through all enforcement layers. Server-streaming for progressive output.
*
* @generated from rpc llm_multiverse.v1.ToolBrokerService.ExecuteTool
*/
executeTool: {
methodKind: "server_streaming";
input: typeof ExecuteToolRequestSchema;
output: typeof ExecuteToolResponseSchema;
},
/**
* Dry-run validation of a tool call without execution.
*
* @generated from rpc llm_multiverse.v1.ToolBrokerService.ValidateCall
*/
validateCall: {
methodKind: "unary";
input: typeof ValidateCallRequestSchema;
output: typeof ValidateCallResponseSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_llm_multiverse_v1_tool_broker, 0);

View File

@@ -0,0 +1,262 @@
import { createClient, ConnectError, Code } from '@connectrpc/connect';
import { createGrpcWebTransport } from '@connectrpc/connect-web';
import { OrchestratorService } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
import type {
ProcessRequestResponse,
SessionConfig
} from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
import { create } from '@bufbuild/protobuf';
import { ProcessRequestRequestSchema } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
import { connectionStore } from '$lib/stores/connection.svelte';
import { toastStore } from '$lib/stores/toast.svelte';
import { logger } from '$lib/utils/logger';
/**
* Application-level error wrapping gRPC status codes.
*/
export class OrchestratorError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly details?: string
) {
super(message);
this.name = 'OrchestratorError';
}
}
/**
* Map gRPC status codes to user-friendly messages.
*/
const GRPC_USER_MESSAGES: Record<string, string> = {
unavailable: 'The server is currently unavailable. Please try again later.',
deadline_exceeded: 'The request timed out. Please try again.',
cancelled: 'The request was cancelled.',
not_found: 'The requested resource was not found.',
already_exists: 'The resource already exists.',
permission_denied: 'You do not have permission to perform this action.',
resource_exhausted: 'Rate limit reached. Please wait a moment and try again.',
failed_precondition: 'The operation cannot be performed in the current state.',
aborted: 'The operation was aborted. Please try again.',
unimplemented: 'This feature is not yet available.',
internal: 'An internal server error occurred. Please try again.',
unauthenticated: 'Authentication required. Please log in.',
data_loss: 'Data loss detected. Please contact support.',
unknown: 'An unexpected error occurred. Please try again.'
};
/**
* Codes considered transient / retriable.
*/
const TRANSIENT_CODES = new Set(['unavailable', 'deadline_exceeded', 'aborted', 'internal']);
/**
* Return a user-friendly message for a gRPC error code.
*/
export function friendlyMessage(code: string): string {
return GRPC_USER_MESSAGES[code.toLowerCase()] ?? GRPC_USER_MESSAGES['unknown'];
}
/**
* Whether the given error code is transient and should be retried.
*/
function isTransient(code: string): boolean {
return TRANSIENT_CODES.has(code.toLowerCase());
}
const DEFAULT_ENDPOINT = '/';
let transport: ReturnType<typeof createGrpcWebTransport> | null = null;
function getTransport(endpoint?: string) {
if (!transport) {
transport = createGrpcWebTransport({
baseUrl: endpoint ?? DEFAULT_ENDPOINT
});
}
return transport;
}
/**
* Reset the transport (useful for reconfiguring the endpoint).
*/
export function resetTransport(newEndpoint?: string): void {
transport = null;
if (newEndpoint !== undefined) {
transport = createGrpcWebTransport({ baseUrl: newEndpoint });
}
}
/**
* Create a configured orchestrator client.
*/
function getClient() {
return createClient(OrchestratorService, getTransport());
}
/**
* Sleep for a given number of milliseconds.
*/
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Calculate exponential backoff delay with jitter.
* Base delay doubles each attempt: 1s, 2s, 4s (capped at 8s).
*/
function backoffDelay(attempt: number): number {
const base = Math.min(1000 * Math.pow(2, attempt), 8000);
const jitter = Math.random() * base * 0.25;
return base + jitter;
}
const MAX_RETRIES = 3;
/**
* Map numeric Code enum values to lowercase string names matching GRPC_USER_MESSAGES keys.
*/
const CODE_TO_STRING: Record<number, string> = {
[Code.Canceled]: 'cancelled',
[Code.Unknown]: 'unknown',
[Code.InvalidArgument]: 'failed_precondition',
[Code.DeadlineExceeded]: 'deadline_exceeded',
[Code.NotFound]: 'not_found',
[Code.AlreadyExists]: 'already_exists',
[Code.PermissionDenied]: 'permission_denied',
[Code.ResourceExhausted]: 'resource_exhausted',
[Code.FailedPrecondition]: 'failed_precondition',
[Code.Aborted]: 'aborted',
[Code.OutOfRange]: 'failed_precondition',
[Code.Unimplemented]: 'unimplemented',
[Code.Internal]: 'internal',
[Code.Unavailable]: 'unavailable',
[Code.DataLoss]: 'data_loss',
[Code.Unauthenticated]: 'unauthenticated'
};
/**
* Extract gRPC error code from an error, normalising to lowercase string.
*/
function extractCode(err: unknown): string {
if (err instanceof ConnectError) {
return CODE_TO_STRING[err.code] ?? 'unknown';
}
if (err instanceof Error && 'code' in err) {
const raw = (err as { code: unknown }).code;
if (typeof raw === 'string') return raw.toLowerCase();
if (typeof raw === 'number') return String(raw);
}
return 'unknown';
}
/**
* Wrap an error into an OrchestratorError with a friendly message.
*/
function toOrchestratorError(err: unknown): OrchestratorError {
if (err instanceof OrchestratorError) return err;
if (err instanceof ConnectError) {
const code = extractCode(err);
const details = [
err.rawMessage,
err.cause ? String(err.cause) : ''
].filter(Boolean).join('; ');
return new OrchestratorError(friendlyMessage(code), code, details || undefined);
}
if (err instanceof Error) {
const code = extractCode(err);
return new OrchestratorError(friendlyMessage(code), code, err.message);
}
return new OrchestratorError(friendlyMessage('unknown'), 'unknown');
}
/**
* Append a diagnostic code suffix to a message, e.g. "(code: unavailable)".
*/
function diagnosticSuffix(err: OrchestratorError): string {
return err.code && err.code !== 'unknown' ? ` (code: ${err.code})` : '';
}
/**
* Send a request to the orchestrator and yield streaming responses.
*
* Includes automatic retry with exponential backoff for transient failures.
* Updates the connection status store on success or failure and fires
* toast notifications on errors.
*
* Returns an async iterator of `ProcessRequestResponse` messages,
* each containing the current orchestration state, status message,
* and optionally intermediate or final results.
*/
export async function* processRequest(
sessionId: string,
userMessage: string,
sessionConfig?: SessionConfig
): AsyncGenerator<ProcessRequestResponse> {
const request = create(ProcessRequestRequestSchema, {
sessionId,
userMessage,
sessionConfig
});
logger.debug('orchestrator', 'processRequest', {
sessionId,
messageLength: userMessage.length
});
let lastError: OrchestratorError | null = null;
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
// Skip retries if already known disconnected
if (connectionStore.status === 'disconnected' && attempt > 0) {
break;
}
if (attempt > 0) {
connectionStore.setReconnecting();
const delay = backoffDelay(attempt - 1);
logger.warn('orchestrator', `Retry attempt ${attempt}/${MAX_RETRIES}`, {
sessionId,
previousCode: lastError?.code,
delay
});
await sleep(delay);
}
try {
const client = getClient();
for await (const response of client.processRequest(request)) {
connectionStore.reportSuccess();
yield response;
}
logger.debug('orchestrator', 'Stream completed', { sessionId });
// Completed successfully — no retry needed
return;
} catch (err: unknown) {
logger.grpcError('orchestrator', `Request failed (attempt ${attempt + 1}/${MAX_RETRIES + 1})`, err);
lastError = toOrchestratorError(err);
const code = lastError.code;
if (isTransient(code) && attempt < MAX_RETRIES) {
// Will retry — continue loop
connectionStore.reportFailure();
continue;
}
// Non-transient or exhausted retries
connectionStore.reportFailure();
const suffix = diagnosticSuffix(lastError);
logger.error('orchestrator', 'Request failed permanently', {
code: lastError.code,
details: lastError.details
});
toastStore.addToast({ message: lastError.message + suffix, type: 'error' });
throw lastError;
}
}
// Should not reach here, but guard against it
if (lastError) {
throw lastError;
}
}

View File

@@ -0,0 +1,88 @@
import { SvelteMap } from 'svelte/reactivity';
export type AuditEventType = 'state_change' | 'tool_invocation' | 'error' | 'message';
export interface AuditEvent {
id: string;
sessionId: string;
timestamp: Date;
eventType: AuditEventType;
details: string;
state?: string;
}
export interface SessionAuditLog {
sessionId: string;
events: AuditEvent[];
}
const STORAGE_KEY = 'llm-multiverse-audit-events';
function loadEvents(): SvelteMap<string, AuditEvent[]> {
if (typeof localStorage === 'undefined') return new SvelteMap();
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return new SvelteMap();
const arr: [string, AuditEvent[]][] = JSON.parse(raw);
return new SvelteMap(
arr.map(([id, events]) => [
id,
events.map((e) => ({ ...e, timestamp: new Date(e.timestamp) }))
])
);
} catch {
return new SvelteMap();
}
}
function saveEvents(events: SvelteMap<string, AuditEvent[]>) {
if (typeof localStorage === 'undefined') return;
localStorage.setItem(STORAGE_KEY, JSON.stringify([...events.entries()]));
}
function createAuditStore() {
const events = $state<SvelteMap<string, AuditEvent[]>>(loadEvents());
function addEvent(
sessionId: string,
event: Omit<AuditEvent, 'id' | 'sessionId' | 'timestamp'>
) {
const auditEvent: AuditEvent = {
id: crypto.randomUUID(),
sessionId,
timestamp: new Date(),
...event
};
const existing = events.get(sessionId) ?? [];
events.set(sessionId, [...existing, auditEvent]);
saveEvents(events);
}
function getEventsBySession(sessionId: string): AuditEvent[] {
return events.get(sessionId) ?? [];
}
function getAllSessions(): SessionAuditLog[] {
return [...events.entries()]
.map(([sessionId, evts]) => ({ sessionId, events: evts }))
.sort((a, b) => {
const aLatest = a.events.length > 0 ? a.events[a.events.length - 1].timestamp.getTime() : 0;
const bLatest = b.events.length > 0 ? b.events[b.events.length - 1].timestamp.getTime() : 0;
return bLatest - aLatest;
});
}
function clearSession(sessionId: string) {
events.delete(sessionId);
saveEvents(events);
}
return {
addEvent,
getEventsBySession,
getAllSessions,
clearSession
};
}
export const auditStore = createAuditStore();

View File

@@ -0,0 +1,38 @@
export type ConnectionStatus = 'connected' | 'reconnecting' | 'disconnected';
function createConnectionStore() {
let status: ConnectionStatus = $state('connected');
let consecutiveFailures = $state(0);
function reportSuccess() {
status = 'connected';
consecutiveFailures = 0;
}
function reportFailure() {
consecutiveFailures += 1;
if (consecutiveFailures >= 3) {
status = 'disconnected';
} else {
status = 'reconnecting';
}
}
function setReconnecting() {
status = 'reconnecting';
}
return {
get status() {
return status;
},
get consecutiveFailures() {
return consecutiveFailures;
},
reportSuccess,
reportFailure,
setReconnecting
};
}
export const connectionStore = createConnectionStore();

View File

@@ -0,0 +1,87 @@
import { SvelteMap } from 'svelte/reactivity';
import { ResultSource } from '$lib/proto/llm_multiverse/v1/common_pb';
export interface StoredMemoryCandidate {
content: string;
source: ResultSource;
confidence: number;
}
export interface SessionMemory {
sessionId: string;
candidates: StoredMemoryCandidate[];
}
const STORAGE_KEY = 'llm-multiverse-memory-candidates';
const SAMPLE_CLEANUP_KEY = 'llm-multiverse-sample-data-cleaned';
function loadMemory(): SvelteMap<string, StoredMemoryCandidate[]> {
if (typeof localStorage === 'undefined') return new SvelteMap();
_cleanupSampleData();
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return new SvelteMap();
const arr: [string, StoredMemoryCandidate[]][] = JSON.parse(raw);
return new SvelteMap(arr);
} catch {
return new SvelteMap();
}
}
/** One-time removal of previously seeded sample sessions. */
function _cleanupSampleData() {
if (typeof localStorage === 'undefined') return;
if (localStorage.getItem(SAMPLE_CLEANUP_KEY)) return;
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const arr: [string, unknown[]][] = JSON.parse(raw);
const filtered = arr.filter(([id]) => !id.startsWith('session-demo-'));
localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered));
}
// Also clean audit store
const auditKey = 'llm-multiverse-audit-events';
const auditRaw = localStorage.getItem(auditKey);
if (auditRaw) {
const arr: [string, unknown[]][] = JSON.parse(auditRaw);
const filtered = arr.filter(([id]) => !id.startsWith('session-demo-'));
localStorage.setItem(auditKey, JSON.stringify(filtered));
}
} catch { /* ignore */ }
localStorage.setItem(SAMPLE_CLEANUP_KEY, '1');
}
function saveMemory(memory: SvelteMap<string, StoredMemoryCandidate[]>) {
if (typeof localStorage === 'undefined') return;
localStorage.setItem(STORAGE_KEY, JSON.stringify([...memory.entries()]));
}
function createMemoryStore() {
const memory = $state<SvelteMap<string, StoredMemoryCandidate[]>>(loadMemory());
function addCandidates(sessionId: string, candidates: StoredMemoryCandidate[]) {
if (candidates.length === 0) return;
const existing = memory.get(sessionId) ?? [];
memory.set(sessionId, [...existing, ...candidates]);
saveMemory(memory);
}
function getAllBySession(): SessionMemory[] {
return [...memory.entries()]
.map(([sessionId, candidates]) => ({ sessionId, candidates }))
.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
}
function clearSession(sessionId: string) {
memory.delete(sessionId);
saveMemory(memory);
}
return {
addCandidates,
getAllBySession,
clearSession
};
}
export const memoryStore = createMemoryStore();

View File

@@ -0,0 +1,116 @@
import { OverrideLevel } from '$lib/proto/llm_multiverse/v1/common_pb';
export interface PresetConfig {
overrideLevel: OverrideLevel;
disabledTools: string[];
grantedPermissions: string[];
}
export interface Preset {
name: string;
config: PresetConfig;
builtIn: boolean;
}
const STORAGE_KEY = 'llm-multiverse-presets';
const builtInPresets: Preset[] = [
{
name: 'Strict mode',
config: {
overrideLevel: OverrideLevel.NONE,
disabledTools: ['FS Write', 'Run Shell', 'Run Code', 'Package Install'],
grantedPermissions: []
},
builtIn: true
},
{
name: 'Research only',
config: {
overrideLevel: OverrideLevel.NONE,
disabledTools: ['Memory Write', 'FS Write', 'Run Code', 'Run Shell', 'Package Install'],
grantedPermissions: []
},
builtIn: true
},
{
name: 'Full access',
config: {
overrideLevel: OverrideLevel.ALL,
disabledTools: [],
grantedPermissions: []
},
builtIn: true
}
];
function loadCustomPresets(): Preset[] {
if (typeof localStorage === 'undefined') return [];
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
const parsed: Preset[] = JSON.parse(raw);
return parsed.map((p) => ({ ...p, builtIn: false }));
} catch {
return [];
}
}
function saveCustomPresets(presets: Preset[]) {
if (typeof localStorage === 'undefined') return;
const custom = presets.filter((p) => !p.builtIn);
localStorage.setItem(STORAGE_KEY, JSON.stringify(custom));
}
function createPresetStore() {
const customPresets = $state<Preset[]>(loadCustomPresets());
function getAllPresets(): Preset[] {
return [...builtInPresets, ...customPresets];
}
function savePreset(name: string, config: PresetConfig): Preset {
const trimmed = name.trim();
if (!trimmed) throw new Error('Preset name cannot be empty');
if (builtInPresets.some((p) => p.name === trimmed)) {
throw new Error('Cannot overwrite a built-in preset');
}
const existing = customPresets.findIndex((p) => p.name === trimmed);
const preset: Preset = { name: trimmed, config, builtIn: false };
if (existing >= 0) {
customPresets[existing] = preset;
} else {
customPresets.push(preset);
}
saveCustomPresets(customPresets);
return preset;
}
function loadPreset(name: string): PresetConfig | null {
const all = getAllPresets();
const preset = all.find((p) => p.name === name);
return preset?.config ?? null;
}
function deletePreset(name: string): boolean {
if (builtInPresets.some((p) => p.name === name)) return false;
const idx = customPresets.findIndex((p) => p.name === name);
if (idx < 0) return false;
customPresets.splice(idx, 1);
saveCustomPresets(customPresets);
return true;
}
return {
getAllPresets,
savePreset,
loadPreset,
deletePreset
};
}
export const presetStore = createPresetStore();

View File

@@ -0,0 +1,125 @@
import { SvelteMap } from 'svelte/reactivity';
import type { ChatMessage } from '$lib/types';
export interface Session {
id: string;
title: string;
messages: ChatMessage[];
createdAt: Date;
}
const STORAGE_KEY = 'llm-multiverse-sessions';
const ACTIVE_SESSION_KEY = 'llm-multiverse-active-session';
function loadSessions(): SvelteMap<string, Session> {
if (typeof localStorage === 'undefined') return new SvelteMap();
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return new SvelteMap();
const arr: [string, Session][] = JSON.parse(raw);
return new SvelteMap(
arr.map(([id, s]) => [
id,
{ ...s, createdAt: new Date(s.createdAt), messages: s.messages.map(m => ({ ...m, timestamp: new Date(m.timestamp) })) }
])
);
} catch {
return new SvelteMap();
}
}
function saveSessions(sessions: SvelteMap<string, Session>) {
if (typeof localStorage === 'undefined') return;
localStorage.setItem(STORAGE_KEY, JSON.stringify([...sessions.entries()]));
}
function loadActiveSessionId(): string | null {
if (typeof localStorage === 'undefined') return null;
return localStorage.getItem(ACTIVE_SESSION_KEY);
}
function saveActiveSessionId(id: string) {
if (typeof localStorage === 'undefined') return;
localStorage.setItem(ACTIVE_SESSION_KEY, id);
}
function createSessionStore() {
const sessions = $state<SvelteMap<string, Session>>(loadSessions());
let activeSessionId = $state<string | null>(loadActiveSessionId());
function createSession(id?: string): Session {
const session: Session = {
id: id ?? crypto.randomUUID(),
title: 'New Chat',
messages: [],
createdAt: new Date()
};
sessions.set(session.id, session);
activeSessionId = session.id;
saveSessions(sessions);
saveActiveSessionId(session.id);
return session;
}
function getOrCreateSession(id?: string): Session {
if (id && sessions.has(id)) {
activeSessionId = id;
saveActiveSessionId(id);
return sessions.get(id)!;
}
// No specific ID — prefer existing active session
if (!id && activeSessionId && sessions.has(activeSessionId)) {
return sessions.get(activeSessionId)!;
}
return createSession(id);
}
function updateMessages(sessionId: string, messages: ChatMessage[]) {
const session = sessions.get(sessionId);
if (!session) return;
session.messages = messages;
// Update title from first user message if still default
if (session.title === 'New Chat') {
const firstUser = messages.find(m => m.role === 'user');
if (firstUser) {
session.title = firstUser.content.slice(0, 50) + (firstUser.content.length > 50 ? '...' : '');
}
}
sessions.set(sessionId, session);
saveSessions(sessions);
}
function switchSession(id: string) {
if (sessions.has(id)) {
activeSessionId = id;
saveActiveSessionId(id);
}
}
function deleteSession(id: string) {
sessions.delete(id);
if (activeSessionId === id) {
const remaining = getAllSessions();
activeSessionId = remaining.length > 0 ? remaining[0].id : null;
if (activeSessionId) saveActiveSessionId(activeSessionId);
}
saveSessions(sessions);
}
function getAllSessions(): Session[] {
return [...sessions.values()].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
}
return {
get activeSessionId() { return activeSessionId; },
get activeSession() { return activeSessionId ? sessions.get(activeSessionId) ?? null : null; },
createSession,
getOrCreateSession,
updateMessages,
switchSession,
deleteSession,
getAllSessions
};
}
export const sessionStore = createSessionStore();

View File

@@ -0,0 +1,99 @@
export type ThemeMode = 'light' | 'dark' | 'system';
const STORAGE_KEY = 'llm-multiverse-theme';
function createThemeStore() {
let mode: ThemeMode = $state('system');
let resolvedDark = $state(false);
let initialized = false;
let mediaQuery: MediaQueryList | null = null;
let mediaListener: ((e: MediaQueryListEvent) => void) | null = null;
function applyTheme(isDark: boolean) {
if (typeof document === 'undefined') return;
const root = document.documentElement;
// Add transition class for smooth switching
root.classList.add('theme-transition');
if (isDark) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
// Remove transition class after animation completes
setTimeout(() => {
root.classList.remove('theme-transition');
}, 300);
}
function getSystemPreference(): boolean {
if (typeof window === 'undefined') return false;
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
function init(): (() => void) | undefined {
if (typeof window === 'undefined') return;
if (initialized) return;
initialized = true;
// Load saved preference
const saved = localStorage.getItem(STORAGE_KEY) as ThemeMode | null;
if (saved === 'light' || saved === 'dark' || saved === 'system') {
mode = saved;
} else {
mode = 'system';
}
// Listen for system preference changes
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaListener = (e: MediaQueryListEvent) => {
if (mode === 'system') {
resolvedDark = e.matches;
applyTheme(resolvedDark);
}
};
mediaQuery.addEventListener('change', mediaListener);
// Apply initial theme
resolvedDark =
mode === 'dark' ? true : mode === 'light' ? false : getSystemPreference();
applyTheme(resolvedDark);
return () => {
if (mediaQuery && mediaListener) {
mediaQuery.removeEventListener('change', mediaListener);
}
initialized = false;
};
}
function setMode(newMode: ThemeMode) {
mode = newMode;
if (typeof window !== 'undefined') {
localStorage.setItem(STORAGE_KEY, newMode);
}
resolvedDark =
newMode === 'dark' ? true : newMode === 'light' ? false : getSystemPreference();
applyTheme(resolvedDark);
}
function cycle() {
const order: ThemeMode[] = ['system', 'light', 'dark'];
const idx = order.indexOf(mode);
const next = order[(idx + 1) % order.length];
setMode(next);
}
return {
get mode() {
return mode;
},
get isDark() {
return resolvedDark;
},
init,
setMode,
cycle
};
}
export const themeStore = createThemeStore();

View File

@@ -0,0 +1,82 @@
export type ToastType = 'error' | 'warning' | 'success' | 'info';
export interface Toast {
id: string;
message: string;
type: ToastType;
duration: number;
dismissable: boolean;
}
const DEFAULT_DURATIONS: Record<ToastType, number> = {
success: 5000,
info: 5000,
warning: 8000,
error: 10000
};
function createToastStore() {
let toasts: Toast[] = $state([]);
// Internal bookkeeping only — not reactive state, so Map is intentional.
// eslint-disable-next-line svelte/prefer-svelte-reactivity
const timers = new Map<string, ReturnType<typeof setTimeout>>();
function addToast(
options: Omit<Toast, 'id' | 'duration' | 'dismissable'> & {
id?: string;
duration?: number;
dismissable?: boolean;
}
): string {
const id = options.id ?? crypto.randomUUID();
const duration = options.duration ?? DEFAULT_DURATIONS[options.type];
const dismissable = options.dismissable ?? true;
const toast: Toast = {
id,
message: options.message,
type: options.type,
duration,
dismissable
};
toasts = [...toasts, toast];
if (duration > 0) {
const timer = setTimeout(() => {
removeToast(id);
}, duration);
timers.set(id, timer);
}
return id;
}
function removeToast(id: string) {
const timer = timers.get(id);
if (timer) {
clearTimeout(timer);
timers.delete(id);
}
toasts = toasts.filter((t) => t.id !== id);
}
function clear() {
for (const timer of timers.values()) {
clearTimeout(timer);
}
timers.clear();
toasts = [];
}
return {
get toasts() {
return toasts;
},
addToast,
removeToast,
clear
};
}
export const toastStore = createToastStore();

6
src/lib/types.ts Normal file
View File

@@ -0,0 +1,6 @@
export interface ChatMessage {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
}

153
src/lib/types/lineage.ts Normal file
View File

@@ -0,0 +1,153 @@
import { AgentType } from '$lib/proto/llm_multiverse/v1/common_pb';
/**
* A node in the agent lineage tree, enriched with children references
* for tree rendering.
*/
export interface LineageNode {
id: string;
agentType: AgentType;
spawnDepth: number;
children: LineageNode[];
}
/**
* Flat agent identifier matching the proto shape but without protobuf Message dependency.
* Used for sample data and as input to tree building.
*/
export interface SimpleAgentIdentifier {
agentId: string;
agentType: AgentType;
spawnDepth: number;
parentId?: string;
}
/**
* Maps AgentType enum values to human-readable labels.
*/
export function agentTypeLabel(type: AgentType): string {
switch (type) {
case AgentType.ORCHESTRATOR:
return 'Orchestrator';
case AgentType.RESEARCHER:
return 'Researcher';
case AgentType.CODER:
return 'Coder';
case AgentType.SYSADMIN:
return 'SysAdmin';
case AgentType.ASSISTANT:
return 'Assistant';
default:
return 'Unspecified';
}
}
export interface AgentColorSet {
fill: string;
stroke: string;
text: string;
}
export interface AgentColors {
light: AgentColorSet;
dark: AgentColorSet;
badge: string;
}
/**
* Maps AgentType enum values to light/dark color tokens for SVG rendering
* and Tailwind badge classes.
*/
export function agentTypeColor(type: AgentType): AgentColors {
switch (type) {
case AgentType.ORCHESTRATOR:
return {
light: { fill: '#dbeafe', stroke: '#3b82f6', text: '#1e40af' },
dark: { fill: '#1e3a5f', stroke: '#60a5fa', text: '#93c5fd' },
badge: 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300'
};
case AgentType.RESEARCHER:
return {
light: { fill: '#dcfce7', stroke: '#22c55e', text: '#166534' },
dark: { fill: '#14532d', stroke: '#4ade80', text: '#86efac' },
badge: 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300'
};
case AgentType.CODER:
return {
light: { fill: '#f3e8ff', stroke: '#a855f7', text: '#6b21a8' },
dark: { fill: '#3b0764', stroke: '#c084fc', text: '#d8b4fe' },
badge: 'bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300'
};
case AgentType.SYSADMIN:
return {
light: { fill: '#ffedd5', stroke: '#f97316', text: '#9a3412' },
dark: { fill: '#431407', stroke: '#fb923c', text: '#fdba74' },
badge: 'bg-orange-100 dark:bg-orange-900/40 text-orange-700 dark:text-orange-300'
};
case AgentType.ASSISTANT:
return {
light: { fill: '#ccfbf1', stroke: '#14b8a6', text: '#115e59' },
dark: { fill: '#134e4a', stroke: '#2dd4bf', text: '#5eead4' },
badge: 'bg-teal-100 dark:bg-teal-900/40 text-teal-700 dark:text-teal-300'
};
default:
return {
light: { fill: '#f3f4f6', stroke: '#9ca3af', text: '#374151' },
dark: { fill: '#1f2937', stroke: '#6b7280', text: '#9ca3af' },
badge: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300'
};
}
}
/**
* Builds a tree of LineageNode from a flat list of SimpleAgentIdentifier.
*
* The algorithm groups agents by spawnDepth. Depth-0 agents become root nodes.
* Each agent at depth N is attached as a child of the last agent at depth N-1
* (based on insertion order), which models a sequential spawn chain.
*
* If parentId is provided, it is used for explicit parent-child linking.
*/
export function buildLineageTree(agents: SimpleAgentIdentifier[]): LineageNode[] {
if (agents.length === 0) return [];
const nodeMap = new Map<string, LineageNode>();
// Create all nodes first
for (const agent of agents) {
nodeMap.set(agent.agentId, {
id: agent.agentId,
agentType: agent.agentType,
spawnDepth: agent.spawnDepth,
children: []
});
}
const roots: LineageNode[] = [];
const depthLastNode = new Map<number, LineageNode>();
for (const agent of agents) {
const node = nodeMap.get(agent.agentId)!;
if (agent.parentId && nodeMap.has(agent.parentId)) {
// Explicit parent link
nodeMap.get(agent.parentId)!.children.push(node);
} else if (agent.spawnDepth === 0) {
roots.push(node);
} else {
// Attach to the last node at depth - 1
const parent = depthLastNode.get(agent.spawnDepth - 1);
if (parent) {
parent.children.push(node);
} else {
// Fallback: treat as root if no parent found
roots.push(node);
}
}
depthLastNode.set(agent.spawnDepth, node);
}
return roots;
}

View File

@@ -0,0 +1,23 @@
import { ResultSource } from '$lib/proto/llm_multiverse/v1/common_pb';
export interface ResultSourceStyle {
label: string;
bg: string;
text: string;
}
/**
* Returns label and badge classes for a ResultSource value.
*/
export function resultSourceConfig(source: ResultSource): ResultSourceStyle {
switch (source) {
case ResultSource.TOOL_OUTPUT:
return { label: 'Tool Output', bg: 'bg-blue-100 dark:bg-blue-900/40', text: 'text-blue-800 dark:text-blue-300' };
case ResultSource.MODEL_KNOWLEDGE:
return { label: 'Model Knowledge', bg: 'bg-purple-100 dark:bg-purple-900/40', text: 'text-purple-800 dark:text-purple-300' };
case ResultSource.WEB:
return { label: 'Web', bg: 'bg-green-100 dark:bg-green-900/40', text: 'text-green-800 dark:text-green-300' };
default:
return { label: 'Unspecified', bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-800 dark:text-gray-300' };
}
}

19
src/lib/utils/date.ts Normal file
View File

@@ -0,0 +1,19 @@
/**
* Formats a date as a relative string: "Today", "Yesterday", "N days ago", or locale date.
*/
export function formatRelativeDate(date: Date): string {
const now = new Date();
const diff = now.getTime() - date.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) return 'Today';
if (days === 1) return 'Yesterday';
if (days < 7) return `${days} days ago`;
return date.toLocaleDateString();
}
/**
* Formats a date as a short string like "Mar 12, 2026".
*/
export function formatShortDate(date: Date): string {
return date.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' });
}

60
src/lib/utils/logger.ts Normal file
View File

@@ -0,0 +1,60 @@
/**
* Lightweight dev-mode logging utility.
* All diagnostic logging flows through this module.
*/
import { ConnectError } from '@connectrpc/connect';
declare global {
var __LLM_DEBUG: boolean | undefined;
}
function isDebugEnabled(): boolean {
return import.meta.env.DEV || globalThis.__LLM_DEBUG === true;
}
function fmt(tag: string, message: string): string {
return `[${tag}] ${message}`;
}
export const logger = {
debug(tag: string, message: string, ...data: unknown[]): void {
if (isDebugEnabled()) {
console.debug(fmt(tag, message), ...data);
}
},
info(tag: string, message: string, ...data: unknown[]): void {
if (isDebugEnabled()) {
console.info(fmt(tag, message), ...data);
}
},
warn(tag: string, message: string, ...data: unknown[]): void {
if (isDebugEnabled()) {
console.warn(fmt(tag, message), ...data);
}
},
error(tag: string, message: string, ...data: unknown[]): void {
if (isDebugEnabled()) {
console.error(fmt(tag, message), ...data);
}
},
/** Always logs regardless of debug toggle. Destructures ConnectError fields. */
grpcError(tag: string, label: string, err: unknown): void {
if (err instanceof ConnectError) {
console.error(fmt(tag, label), {
code: err.code,
rawMessage: err.rawMessage,
cause: err.cause,
metadata: Object.fromEntries(err.metadata.entries())
});
} else if (err instanceof Error) {
console.error(fmt(tag, label), { message: err.message });
} else {
console.error(fmt(tag, label), err);
}
}
};

View File

@@ -0,0 +1,13 @@
import { OverrideLevel } from '$lib/proto/llm_multiverse/v1/common_pb';
import type { SessionConfig } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
/**
* Returns true if the session config differs from defaults.
*/
export function isNonDefaultConfig(config: SessionConfig): boolean {
return (
config.overrideLevel !== OverrideLevel.NONE ||
config.disabledTools.length > 0 ||
config.grantedPermissions.length > 0
);
}

54
src/routes/+error.svelte Normal file
View File

@@ -0,0 +1,54 @@
<script lang="ts">
import { page } from '$app/stores';
import { resolveRoute } from '$app/paths';
const homeHref = resolveRoute('/');
</script>
<div class="flex min-h-screen items-center justify-center bg-white px-4 dark:bg-gray-900">
<div class="w-full max-w-md text-center">
<!-- Error icon -->
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/40">
<svg class="h-8 w-8 text-red-500 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
</div>
<!-- Status code -->
<p class="text-5xl font-bold text-gray-900 dark:text-gray-100">{$page.status}</p>
<!-- Message -->
<h1 class="mt-3 text-lg font-semibold text-gray-900 dark:text-gray-100">
Something went wrong
</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{$page.error?.message ?? 'An unexpected error occurred. Please try again.'}
</p>
<!-- Actions -->
<div class="mt-8 flex flex-col gap-3 sm:flex-row sm:justify-center">
<button
type="button"
onclick={() => history.back()}
class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Go back
</button>
<button
type="button"
onclick={() => location.reload()}
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
Try again
</button>
<!-- eslint-disable svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
<a
href={homeHref}
class="inline-flex items-center justify-center rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Home
</a>
<!-- eslint-enable svelte/no-navigation-without-resolve -->
</div>
</div>
</div>

31
src/routes/+layout.svelte Normal file
View File

@@ -0,0 +1,31 @@
<script lang="ts">
import favicon from '$lib/assets/favicon.svg';
import '../app.css';
import { onMount } from 'svelte';
import { themeStore } from '$lib/stores/theme.svelte';
import ToastContainer from '$lib/components/ToastContainer.svelte';
let { children } = $props();
onMount(() => {
const cleanup = themeStore.init();
return cleanup;
});
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
{@render children()}
<ToastContainer />
<style>
:global(.theme-transition),
:global(.theme-transition *) {
transition:
background-color 0.3s ease,
border-color 0.3s ease,
color 0.3s ease !important;
}
</style>

27
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,27 @@
<script lang="ts">
import { resolveRoute } from '$app/paths';
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
const chatHref = resolveRoute('/chat');
const lineageHref = resolveRoute('/lineage');
const memoryHref = resolveRoute('/memory');
const auditHref = resolveRoute('/audit');
</script>
<div class="flex min-h-screen flex-col items-center bg-white dark:bg-gray-900 px-4">
<div class="absolute right-4 top-4">
<ThemeToggle />
</div>
<h1 class="text-2xl md:text-3xl font-bold text-center mt-8 text-gray-900 dark:text-gray-100">LLM Multiverse UI</h1>
<p class="text-center text-gray-600 dark:text-gray-400 mt-2">Orchestration interface</p>
<nav class="flex flex-col sm:flex-row justify-center gap-3 sm:gap-4 mt-6 w-full max-w-md sm:max-w-none sm:w-auto">
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
<a href={chatHref} class="rounded-lg bg-blue-600 px-4 py-3 sm:py-2 text-center text-sm font-medium text-white hover:bg-blue-700">Chat</a>
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
<a href={lineageHref} class="rounded-lg bg-gray-100 dark:bg-gray-700 px-4 py-3 sm:py-2 text-center text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600">Agent Lineage</a>
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
<a href={memoryHref} class="rounded-lg bg-gray-100 dark:bg-gray-700 px-4 py-3 sm:py-2 text-center text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600">Memory Candidates</a>
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
<a href={auditHref} class="rounded-lg bg-gray-100 dark:bg-gray-700 px-4 py-3 sm:py-2 text-center text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600">Audit Log</a>
</nav>
</div>

2
src/routes/+page.ts Normal file
View File

@@ -0,0 +1,2 @@
export const prerender = false;
export const ssr = false;

Some files were not shown because too many files have changed in this diff Show More