diff --git a/README.md b/README.md index 0003222..cd86e64 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,6 @@ src/ lib/ components/ Svelte components (Backdrop, PageHeader, MessageInput, ...) composables/ Reactive composables (useOrchestration) - data/ Sample/demo data generators proto/ Generated protobuf TypeScript services/ gRPC client (orchestrator) stores/ Svelte 5 reactive stores (sessions, audit, memory, theme, ...) diff --git a/src/lib/components/FinalResult.svelte b/src/lib/components/FinalResult.svelte index 83a8c99..d2c5694 100644 --- a/src/lib/components/FinalResult.svelte +++ b/src/lib/components/FinalResult.svelte @@ -1,6 +1,6 @@ -
-
+
+ + {statusConfig.label} @@ -45,27 +68,115 @@ {sourceBadge.label} {/if} + {#if result.summary} + {result.summary} + {/if} + + +
+ {#if result.summary} +

{@html linkify(result.summary)}

+ {/if} + + {#if result.failureReason} +

Reason: {result.failureReason}

+ {/if} + + {#if result.artifacts.length > 0} +
+

Artifacts

+
+ {#each result.artifacts as artifact, i (artifact.label + i)} + {#if artifact.artifactType === ArtifactType.CODE} + +
+
+ 📄 + {artifact.label} +
+ {#if artifact.metadata?.language} + {artifact.metadata.language} + {/if} + +
+
+ {#if contentLineCount(artifact.content) > LINE_COLLAPSE_THRESHOLD} +
+ + Show {contentLineCount(artifact.content)} lines + +
{artifact.content}
+
+ {:else} +
{artifact.content}
+ {/if} +
+ + {:else if artifact.artifactType === ArtifactType.COMMAND_OUTPUT} + +
+
+ + {artifact.label} +
+ {#if contentLineCount(artifact.content) > LINE_COLLAPSE_THRESHOLD} +
+ + Show {contentLineCount(artifact.content)} lines + +
{artifact.content}
+
+ {:else} +
{artifact.content}
+ {/if} +
+ + {:else if artifact.artifactType === ArtifactType.SEARCH_RESULT} + +
+
+ 🔍 + {artifact.label} +
+ {#if contentLineCount(artifact.content) > LINE_COLLAPSE_THRESHOLD} +
+ + Show full results + +

{@html linkify(artifact.content)}

+
+ {:else} +

{@html linkify(artifact.content)}

+ {/if} +
+ + {:else} + +
+ 📄 + {@html linkify(artifact.content)} +
+ {/if} + {/each} +
+
+ {/if}
+
- {#if result.summary} -

{result.summary}

- {/if} - - {#if result.failureReason} -

Reason: {result.failureReason}

- {/if} - - {#if result.artifacts.length > 0} -
-

Artifacts

-
    - {#each result.artifacts as artifact (artifact)} -
  • - 📄 - {artifact} -
  • - {/each} -
-
- {/if} -
+ diff --git a/src/lib/components/OrchestrationProgress.svelte b/src/lib/components/OrchestrationProgress.svelte index 1a7ab6d..f3d0c26 100644 --- a/src/lib/components/OrchestrationProgress.svelte +++ b/src/lib/components/OrchestrationProgress.svelte @@ -29,7 +29,7 @@ {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' + ? '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'} @@ -46,11 +46,45 @@
{#if i < phases.length - 1} -
+ {@const nextStatus = getStatus(phases[i + 1].state)} +
+ {#if nextStatus !== 'pending'} + +
+ {:else if status === 'active'} + +
+ {/if} +
{/if} {/each} + + diff --git a/src/lib/components/PageHeader.svelte b/src/lib/components/PageHeader.svelte index 7ae81fe..c14c9de 100644 --- a/src/lib/components/PageHeader.svelte +++ b/src/lib/components/PageHeader.svelte @@ -6,13 +6,11 @@ title, backHref, backLabel = 'Chat', - showSampleBadge = false, children }: { title: string; backHref?: string; backLabel?: string; - showSampleBadge?: boolean; children?: Snippet; } = $props(); @@ -36,10 +34,5 @@ {@render children()} {/if} - {#if showSampleBadge} - - {/if} diff --git a/src/lib/composables/useOrchestration.svelte.ts b/src/lib/composables/useOrchestration.svelte.ts index 922af70..e8bfd6c 100644 --- a/src/lib/composables/useOrchestration.svelte.ts +++ b/src/lib/composables/useOrchestration.svelte.ts @@ -6,7 +6,7 @@ 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 { toastStore } from '$lib/stores/toast.svelte'; +import { logger } from '$lib/utils/logger'; export function createOrchestration() { let isStreaming = $state(false); @@ -57,6 +57,10 @@ export function createOrchestration() { 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}`, @@ -88,21 +92,27 @@ export function createOrchestration() { }; } } catch (err) { - const friendlyMsg = - err instanceof OrchestratorError - ? friendlyMessage(err.code) - : 'An unexpected error occurred'; - error = friendlyMsg; + 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; - toastStore.addToast({ message: friendlyMsg, type: 'error' }); + logger.error('useOrchestration', 'Request failed', { code, details }); auditStore.addEvent(sessionId, { eventType: 'error', - details: friendlyMsg + details: `${friendlyMsg} | code=${code}${details ? ` | details=${details}` : ''}` }); const idx = messages.length - 1; messages[idx] = { ...messages[idx], - content: `\u26A0 ${friendlyMsg}` + content: `\u26A0 ${displayMsg}` }; } finally { isStreaming = false; @@ -149,3 +159,9 @@ export function createOrchestration() { 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(); diff --git a/src/lib/data/sampleData.ts b/src/lib/data/sampleData.ts deleted file mode 100644 index 2e1ce6b..0000000 --- a/src/lib/data/sampleData.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { SvelteMap } from 'svelte/reactivity'; -import { ResultSource } from '$lib/proto/llm_multiverse/v1/common_pb'; -import type { AuditEvent } from '$lib/stores/audit.svelte'; -import type { StoredMemoryCandidate } from '$lib/stores/memory.svelte'; - -export function getSampleAuditData(): SvelteMap { - const samples = new SvelteMap(); - const now = Date.now(); - - samples.set('session-demo-alpha', [ - { - id: 'evt-001', - sessionId: 'session-demo-alpha', - timestamp: new Date(now - 300_000), - eventType: 'state_change', - details: 'Orchestration started — decomposing user request', - state: 'DECOMPOSING' - }, - { - id: 'evt-002', - sessionId: 'session-demo-alpha', - timestamp: new Date(now - 280_000), - eventType: 'tool_invocation', - details: 'Invoked web_search tool for "SvelteKit best practices"', - state: 'DECOMPOSING' - }, - { - id: 'evt-003', - sessionId: 'session-demo-alpha', - timestamp: new Date(now - 260_000), - eventType: 'state_change', - details: 'Dispatching subtasks to subagents', - state: 'DISPATCHING' - }, - { - id: 'evt-004', - sessionId: 'session-demo-alpha', - timestamp: new Date(now - 240_000), - eventType: 'message', - details: 'Researcher agent assigned to subtask "Gather TypeScript patterns"' - }, - { - id: 'evt-005', - sessionId: 'session-demo-alpha', - timestamp: new Date(now - 220_000), - eventType: 'state_change', - details: 'Subagents executing tasks', - state: 'EXECUTING' - }, - { - id: 'evt-006', - sessionId: 'session-demo-alpha', - timestamp: new Date(now - 200_000), - eventType: 'tool_invocation', - details: 'Invoked code_analysis tool on project source files' - }, - { - id: 'evt-007', - sessionId: 'session-demo-alpha', - timestamp: new Date(now - 180_000), - eventType: 'error', - details: 'Timeout waiting for code_analysis response (retrying)' - }, - { - id: 'evt-008', - sessionId: 'session-demo-alpha', - timestamp: new Date(now - 160_000), - eventType: 'tool_invocation', - details: 'Retry: code_analysis tool succeeded' - }, - { - id: 'evt-009', - sessionId: 'session-demo-alpha', - timestamp: new Date(now - 140_000), - eventType: 'state_change', - details: 'Compacting results from subagents', - state: 'COMPACTING' - }, - { - id: 'evt-010', - sessionId: 'session-demo-alpha', - timestamp: new Date(now - 120_000), - eventType: 'state_change', - details: 'Orchestration complete', - state: 'COMPLETE' - } - ]); - - samples.set('session-demo-beta', [ - { - id: 'evt-011', - sessionId: 'session-demo-beta', - timestamp: new Date(now - 600_000), - eventType: 'state_change', - details: 'Orchestration started — decomposing deployment request', - state: 'DECOMPOSING' - }, - { - id: 'evt-012', - sessionId: 'session-demo-beta', - timestamp: new Date(now - 580_000), - eventType: 'state_change', - details: 'Dispatching to sysadmin agent', - state: 'DISPATCHING' - }, - { - id: 'evt-013', - sessionId: 'session-demo-beta', - timestamp: new Date(now - 560_000), - eventType: 'state_change', - details: 'Executing deployment analysis', - state: 'EXECUTING' - }, - { - id: 'evt-014', - sessionId: 'session-demo-beta', - timestamp: new Date(now - 540_000), - eventType: 'tool_invocation', - details: 'Invoked kubectl_check tool for cluster status' - }, - { - id: 'evt-015', - sessionId: 'session-demo-beta', - timestamp: new Date(now - 520_000), - eventType: 'error', - details: 'Permission denied: kubectl access not granted for this session' - }, - { - id: 'evt-016', - sessionId: 'session-demo-beta', - timestamp: new Date(now - 500_000), - eventType: 'message', - details: 'Agent requested elevated permissions for cluster access' - }, - { - id: 'evt-017', - sessionId: 'session-demo-beta', - timestamp: new Date(now - 480_000), - eventType: 'state_change', - details: 'Compacting partial results', - state: 'COMPACTING' - }, - { - id: 'evt-018', - sessionId: 'session-demo-beta', - timestamp: new Date(now - 460_000), - eventType: 'state_change', - details: 'Orchestration complete with warnings', - state: 'COMPLETE' - } - ]); - - return samples; -} - -export function getSampleMemoryData(): SvelteMap { - const samples = new SvelteMap(); - samples.set('session-demo-alpha', [ - { - content: 'The user prefers TypeScript strict mode with no implicit any.', - source: ResultSource.MODEL_KNOWLEDGE, - confidence: 0.92 - }, - { - content: 'Project uses SvelteKit with Tailwind CSS v4 and Svelte 5 runes.', - source: ResultSource.TOOL_OUTPUT, - confidence: 0.98 - }, - { - content: 'Preferred testing framework is Vitest with Playwright for e2e.', - source: ResultSource.MODEL_KNOWLEDGE, - confidence: 0.75 - }, - { - content: 'The gRPC backend runs on port 50051 behind a Caddy reverse proxy.', - source: ResultSource.TOOL_OUTPUT, - confidence: 0.85 - } - ]); - samples.set('session-demo-beta', [ - { - content: 'User asked about deploying to Kubernetes with Helm charts.', - source: ResultSource.WEB, - confidence: 0.67 - }, - { - content: 'The container registry is at registry.example.com.', - source: ResultSource.TOOL_OUTPUT, - confidence: 0.91 - }, - { - content: 'Deployment target is a 3-node k3s cluster running on ARM64.', - source: ResultSource.WEB, - confidence: 0.58 - } - ]); - samples.set('session-demo-gamma', [ - { - content: 'Database schema uses PostgreSQL with pgvector extension for embeddings.', - source: ResultSource.TOOL_OUTPUT, - confidence: 0.95 - }, - { - content: 'The embedding model is all-MiniLM-L6-v2 with 384 dimensions.', - source: ResultSource.MODEL_KNOWLEDGE, - confidence: 0.82 - } - ]); - return samples; -} diff --git a/src/lib/proto/llm_multiverse/v1/common_pb.ts b/src/lib/proto/llm_multiverse/v1/common_pb.ts index faea233..e74a4ac 100644 --- a/src/lib/proto/llm_multiverse/v1/common_pb.ts +++ b/src/lib/proto/llm_multiverse/v1/common_pb.ts @@ -12,7 +12,47 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file llm_multiverse/v1/common.proto. */ export const file_llm_multiverse_v1_common: GenFile = /*@__PURE__*/ - fileDesc("Ch5sbG1fbXVsdGl2ZXJzZS92MS9jb21tb24ucHJvdG8SEWxsbV9tdWx0aXZlcnNlLnYxImoKD0FnZW50SWRlbnRpZmllchIQCghhZ2VudF9pZBgBIAEoCRIwCgphZ2VudF90eXBlGAIgASgOMhwubGxtX211bHRpdmVyc2UudjEuQWdlbnRUeXBlEhMKC3NwYXduX2RlcHRoGAMgASgNIkIKDEFnZW50TGluZWFnZRIyCgZhZ2VudHMYASADKAsyIi5sbG1fbXVsdGl2ZXJzZS52MS5BZ2VudElkZW50aWZpZXIi1wEKDlNlc3Npb25Db250ZXh0EhIKCnNlc3Npb25faWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRI2Cg1hZ2VudF9saW5lYWdlGAMgASgLMh8ubGxtX211bHRpdmVyc2UudjEuQWdlbnRMaW5lYWdlEjgKDm92ZXJyaWRlX2xldmVsGAQgASgOMiAubGxtX211bHRpdmVyc2UudjEuT3ZlcnJpZGVMZXZlbBIuCgpjcmVhdGVkX2F0GAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKdAQoLRXJyb3JEZXRhaWwSDAoEY29kZRgBIAEoCRIPCgdtZXNzYWdlGAIgASgJEj4KCG1ldGFkYXRhGAMgAygLMiwubGxtX211bHRpdmVyc2UudjEuRXJyb3JEZXRhaWwuTWV0YWRhdGFFbnRyeRovCg1NZXRhZGF0YUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZwoPTWVtb3J5Q2FuZGlkYXRlEg8KB2NvbnRlbnQYASABKAkSLwoGc291cmNlGAIgASgOMh8ubGxtX211bHRpdmVyc2UudjEuUmVzdWx0U291cmNlEhIKCmNvbmZpZGVuY2UYAyABKAIiwwIKDlN1YmFnZW50UmVzdWx0Ei8KBnN0YXR1cxgBIAEoDjIfLmxsbV9tdWx0aXZlcnNlLnYxLlJlc3VsdFN0YXR1cxIPCgdzdW1tYXJ5GAIgASgJEhEKCWFydGlmYWN0cxgDIAMoCRI4Cg5yZXN1bHRfcXVhbGl0eRgEIAEoDjIgLmxsbV9tdWx0aXZlcnNlLnYxLlJlc3VsdFF1YWxpdHkSLwoGc291cmNlGAUgASgOMh8ubGxtX211bHRpdmVyc2UudjEuUmVzdWx0U291cmNlEkEKFW5ld19tZW1vcnlfY2FuZGlkYXRlcxgGIAMoCzIiLmxsbV9tdWx0aXZlcnNlLnYxLk1lbW9yeUNhbmRpZGF0ZRIbCg5mYWlsdXJlX3JlYXNvbhgHIAEoCUgAiAEBQhEKD19mYWlsdXJlX3JlYXNvbiqoAQoJQWdlbnRUeXBlEhoKFkFHRU5UX1RZUEVfVU5TUEVDSUZJRUQQABIbChdBR0VOVF9UWVBFX09SQ0hFU1RSQVRPUhABEhkKFUFHRU5UX1RZUEVfUkVTRUFSQ0hFUhACEhQKEEFHRU5UX1RZUEVfQ09ERVIQAxIXChNBR0VOVF9UWVBFX1NZU0FETUlOEAQSGAoUQUdFTlRfVFlQRV9BU1NJU1RBTlQQBSr1AQoIVG9vbFR5cGUSGQoVVE9PTF9UWVBFX1VOU1BFQ0lGSUVEEAASGQoVVE9PTF9UWVBFX01FTU9SWV9SRUFEEAESGgoWVE9PTF9UWVBFX01FTU9SWV9XUklURRACEhgKFFRPT0xfVFlQRV9XRUJfU0VBUkNIEAMSFQoRVE9PTF9UWVBFX0ZTX1JFQUQQBBIWChJUT09MX1RZUEVfRlNfV1JJVEUQBRIWChJUT09MX1RZUEVfUlVOX0NPREUQBhIXChNUT09MX1RZUEVfUlVOX1NIRUxMEAcSHQoZVE9PTF9UWVBFX1BBQ0tBR0VfSU5TVEFMTBAIKnoKDU92ZXJyaWRlTGV2ZWwSHgoaT1ZFUlJJREVfTEVWRUxfVU5TUEVDSUZJRUQQABIXChNPVkVSUklERV9MRVZFTF9OT05FEAESGAoUT1ZFUlJJREVfTEVWRUxfUkVMQVgQAhIWChJPVkVSUklERV9MRVZFTF9BTEwQAyp9CgxSZXN1bHRTdGF0dXMSHQoZUkVTVUxUX1NUQVRVU19VTlNQRUNJRklFRBAAEhkKFVJFU1VMVF9TVEFUVVNfU1VDQ0VTUxABEhkKFVJFU1VMVF9TVEFUVVNfUEFSVElBTBACEhgKFFJFU1VMVF9TVEFUVVNfRkFJTEVEEAMqhwEKDVJlc3VsdFF1YWxpdHkSHgoaUkVTVUxUX1FVQUxJVFlfVU5TUEVDSUZJRUQQABIbChdSRVNVTFRfUVVBTElUWV9WRVJJRklFRBABEhsKF1JFU1VMVF9RVUFMSVRZX0lORkVSUkVEEAISHAoYUkVTVUxUX1FVQUxJVFlfVU5DRVJUQUlOEAMqhgEKDFJlc3VsdFNvdXJjZRIdChlSRVNVTFRfU09VUkNFX1VOU1BFQ0lGSUVEEAASHQoZUkVTVUxUX1NPVVJDRV9UT09MX09VVFBVVBABEiEKHVJFU1VMVF9TT1VSQ0VfTU9ERUxfS05PV0xFREdFEAISFQoRUkVTVUxUX1NPVVJDRV9XRUIQA2IGcHJvdG8z", [file_google_protobuf_timestamp]); + fileDesc("Ch5sbG1fbXVsdGl2ZXJzZS92MS9jb21tb24ucHJvdG8SEWxsbV9tdWx0aXZlcnNlLnYxItABCghBcnRpZmFjdBINCgVsYWJlbBgBIAEoCRIPCgdjb250ZW50GAIgASgJEjYKDWFydGlmYWN0X3R5cGUYAyABKA4yHy5sbG1fbXVsdGl2ZXJzZS52MS5BcnRpZmFjdFR5cGUSOwoIbWV0YWRhdGEYBCADKAsyKS5sbG1fbXVsdGl2ZXJzZS52MS5BcnRpZmFjdC5NZXRhZGF0YUVudHJ5Gi8KDU1ldGFkYXRhRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJqCg9BZ2VudElkZW50aWZpZXISEAoIYWdlbnRfaWQYASABKAkSMAoKYWdlbnRfdHlwZRgCIAEoDjIcLmxsbV9tdWx0aXZlcnNlLnYxLkFnZW50VHlwZRITCgtzcGF3bl9kZXB0aBgDIAEoDSJCCgxBZ2VudExpbmVhZ2USMgoGYWdlbnRzGAEgAygLMiIubGxtX211bHRpdmVyc2UudjEuQWdlbnRJZGVudGlmaWVyItcBCg5TZXNzaW9uQ29udGV4dBISCgpzZXNzaW9uX2lkGAEgASgJEg8KB3VzZXJfaWQYAiABKAkSNgoNYWdlbnRfbGluZWFnZRgDIAEoCzIfLmxsbV9tdWx0aXZlcnNlLnYxLkFnZW50TGluZWFnZRI4Cg5vdmVycmlkZV9sZXZlbBgEIAEoDjIgLmxsbV9tdWx0aXZlcnNlLnYxLk92ZXJyaWRlTGV2ZWwSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAinQEKC0Vycm9yRGV0YWlsEgwKBGNvZGUYASABKAkSDwoHbWVzc2FnZRgCIAEoCRI+CghtZXRhZGF0YRgDIAMoCzIsLmxsbV9tdWx0aXZlcnNlLnYxLkVycm9yRGV0YWlsLk1ldGFkYXRhRW50cnkaLwoNTWV0YWRhdGFFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBImcKD01lbW9yeUNhbmRpZGF0ZRIPCgdjb250ZW50GAEgASgJEi8KBnNvdXJjZRgCIAEoDjIfLmxsbV9tdWx0aXZlcnNlLnYxLlJlc3VsdFNvdXJjZRISCgpjb25maWRlbmNlGAMgASgCIuACCg5TdWJhZ2VudFJlc3VsdBIvCgZzdGF0dXMYASABKA4yHy5sbG1fbXVsdGl2ZXJzZS52MS5SZXN1bHRTdGF0dXMSDwoHc3VtbWFyeRgCIAEoCRIuCglhcnRpZmFjdHMYAyADKAsyGy5sbG1fbXVsdGl2ZXJzZS52MS5BcnRpZmFjdBI4Cg5yZXN1bHRfcXVhbGl0eRgEIAEoDjIgLmxsbV9tdWx0aXZlcnNlLnYxLlJlc3VsdFF1YWxpdHkSLwoGc291cmNlGAUgASgOMh8ubGxtX211bHRpdmVyc2UudjEuUmVzdWx0U291cmNlEkEKFW5ld19tZW1vcnlfY2FuZGlkYXRlcxgGIAMoCzIiLmxsbV9tdWx0aXZlcnNlLnYxLk1lbW9yeUNhbmRpZGF0ZRIbCg5mYWlsdXJlX3JlYXNvbhgHIAEoCUgAiAEBQhEKD19mYWlsdXJlX3JlYXNvbiqoAQoJQWdlbnRUeXBlEhoKFkFHRU5UX1RZUEVfVU5TUEVDSUZJRUQQABIbChdBR0VOVF9UWVBFX09SQ0hFU1RSQVRPUhABEhkKFUFHRU5UX1RZUEVfUkVTRUFSQ0hFUhACEhQKEEFHRU5UX1RZUEVfQ09ERVIQAxIXChNBR0VOVF9UWVBFX1NZU0FETUlOEAQSGAoUQUdFTlRfVFlQRV9BU1NJU1RBTlQQBSr1AQoIVG9vbFR5cGUSGQoVVE9PTF9UWVBFX1VOU1BFQ0lGSUVEEAASGQoVVE9PTF9UWVBFX01FTU9SWV9SRUFEEAESGgoWVE9PTF9UWVBFX01FTU9SWV9XUklURRACEhgKFFRPT0xfVFlQRV9XRUJfU0VBUkNIEAMSFQoRVE9PTF9UWVBFX0ZTX1JFQUQQBBIWChJUT09MX1RZUEVfRlNfV1JJVEUQBRIWChJUT09MX1RZUEVfUlVOX0NPREUQBhIXChNUT09MX1RZUEVfUlVOX1NIRUxMEAcSHQoZVE9PTF9UWVBFX1BBQ0tBR0VfSU5TVEFMTBAIKnoKDU92ZXJyaWRlTGV2ZWwSHgoaT1ZFUlJJREVfTEVWRUxfVU5TUEVDSUZJRUQQABIXChNPVkVSUklERV9MRVZFTF9OT05FEAESGAoUT1ZFUlJJREVfTEVWRUxfUkVMQVgQAhIWChJPVkVSUklERV9MRVZFTF9BTEwQAyp9CgxSZXN1bHRTdGF0dXMSHQoZUkVTVUxUX1NUQVRVU19VTlNQRUNJRklFRBAAEhkKFVJFU1VMVF9TVEFUVVNfU1VDQ0VTUxABEhkKFVJFU1VMVF9TVEFUVVNfUEFSVElBTBACEhgKFFJFU1VMVF9TVEFUVVNfRkFJTEVEEAMqhwEKDVJlc3VsdFF1YWxpdHkSHgoaUkVTVUxUX1FVQUxJVFlfVU5TUEVDSUZJRUQQABIbChdSRVNVTFRfUVVBTElUWV9WRVJJRklFRBABEhsKF1JFU1VMVF9RVUFMSVRZX0lORkVSUkVEEAISHAoYUkVTVUxUX1FVQUxJVFlfVU5DRVJUQUlOEAMqhgEKDFJlc3VsdFNvdXJjZRIdChlSRVNVTFRfU09VUkNFX1VOU1BFQ0lGSUVEEAASHQoZUkVTVUxUX1NPVVJDRV9UT09MX09VVFBVVBABEiEKHVJFU1VMVF9TT1VSQ0VfTU9ERUxfS05PV0xFREdFEAISFQoRUkVTVUxUX1NPVVJDRV9XRUIQAyqgAQoMQXJ0aWZhY3RUeXBlEh0KGUFSVElGQUNUX1RZUEVfVU5TUEVDSUZJRUQQABIWChJBUlRJRkFDVF9UWVBFX0NPREUQARIWChJBUlRJRkFDVF9UWVBFX1RFWFQQAhIgChxBUlRJRkFDVF9UWVBFX0NPTU1BTkRfT1VUUFVUEAMSHwobQVJUSUZBQ1RfVFlQRV9TRUFSQ0hfUkVTVUxUEARiBnByb3RvMw", [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 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 = /*@__PURE__*/ + messageDesc(file_llm_multiverse_v1_common, 0); /** * Identifies a single agent in the lineage chain. @@ -41,7 +81,7 @@ export type AgentIdentifier = Message<"llm_multiverse.v1.AgentIdentifier"> & { * Use `create(AgentIdentifierSchema)` to create a new message. */ export const AgentIdentifierSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_llm_multiverse_v1_common, 0); + messageDesc(file_llm_multiverse_v1_common, 1); /** * Ordered chain of agents from orchestrator (index 0) to current agent. @@ -61,7 +101,7 @@ export type AgentLineage = Message<"llm_multiverse.v1.AgentLineage"> & { * Use `create(AgentLineageSchema)` to create a new message. */ export const AgentLineageSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_llm_multiverse_v1_common, 1); + messageDesc(file_llm_multiverse_v1_common, 2); /** * Carried in every gRPC request for audit trail and broker enforcement. @@ -100,7 +140,7 @@ export type SessionContext = Message<"llm_multiverse.v1.SessionContext"> & { * Use `create(SessionContextSchema)` to create a new message. */ export const SessionContextSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_llm_multiverse_v1_common, 2); + messageDesc(file_llm_multiverse_v1_common, 3); /** * Structured error detail for gRPC error responses. @@ -129,7 +169,7 @@ export type ErrorDetail = Message<"llm_multiverse.v1.ErrorDetail"> & { * Use `create(ErrorDetailSchema)` to create a new message. */ export const ErrorDetailSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_llm_multiverse_v1_common, 3); + messageDesc(file_llm_multiverse_v1_common, 4); /** * A candidate memory entry proposed by a subagent for persistence. @@ -158,7 +198,7 @@ export type MemoryCandidate = Message<"llm_multiverse.v1.MemoryCandidate"> & { * Use `create(MemoryCandidateSchema)` to create a new message. */ export const MemoryCandidateSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_llm_multiverse_v1_common, 4); + messageDesc(file_llm_multiverse_v1_common, 5); /** * Standardized return value from any subagent to its parent. @@ -179,9 +219,11 @@ export type SubagentResult = Message<"llm_multiverse.v1.SubagentResult"> & { summary: string; /** - * @generated from field: repeated string artifacts = 3; + * Structured artifacts produced during the agent loop. + * + * @generated from field: repeated llm_multiverse.v1.Artifact artifacts = 3; */ - artifacts: string[]; + artifacts: Artifact[]; /** * @generated from field: llm_multiverse.v1.ResultQuality result_quality = 4; @@ -209,7 +251,7 @@ export type SubagentResult = Message<"llm_multiverse.v1.SubagentResult"> & { * Use `create(SubagentResultSchema)` to create a new message. */ export const SubagentResultSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_llm_multiverse_v1_common, 5); + messageDesc(file_llm_multiverse_v1_common, 6); /** * Agent types with distinct tool permission manifests. @@ -450,3 +492,49 @@ export enum ResultSource { export const ResultSourceSchema: GenEnum = /*@__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 = /*@__PURE__*/ + enumDesc(file_llm_multiverse_v1_common, 6); + diff --git a/src/lib/services/orchestrator.ts b/src/lib/services/orchestrator.ts index 0198ae6..d4d1a95 100644 --- a/src/lib/services/orchestrator.ts +++ b/src/lib/services/orchestrator.ts @@ -1,4 +1,4 @@ -import { createClient } from '@connectrpc/connect'; +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 { @@ -9,6 +9,7 @@ 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. @@ -112,10 +113,35 @@ function backoffDelay(attempt: number): number { const MAX_RETRIES = 3; +/** + * Map numeric Code enum values to lowercase string names matching GRPC_USER_MESSAGES keys. + */ +const CODE_TO_STRING: Record = { + [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(); @@ -129,6 +155,14 @@ function extractCode(err: unknown): string { */ 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); @@ -136,6 +170,13 @@ function toOrchestratorError(err: unknown): OrchestratorError { 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. * @@ -158,6 +199,11 @@ export async function* processRequest( sessionConfig }); + logger.debug('orchestrator', 'processRequest', { + sessionId, + messageLength: userMessage.length + }); + let lastError: OrchestratorError | null = null; for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { @@ -169,6 +215,11 @@ export async function* processRequest( 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); } @@ -178,9 +229,11 @@ export async function* processRequest( 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; @@ -192,7 +245,12 @@ export async function* processRequest( // Non-transient or exhausted retries connectionStore.reportFailure(); - toastStore.addToast({ message: lastError.message, type: 'error' }); + 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; } } diff --git a/src/lib/stores/audit.svelte.ts b/src/lib/stores/audit.svelte.ts index f99c505..5cf398c 100644 --- a/src/lib/stores/audit.svelte.ts +++ b/src/lib/stores/audit.svelte.ts @@ -1,5 +1,4 @@ import { SvelteMap } from 'svelte/reactivity'; -import { getSampleAuditData } from '$lib/data/sampleData'; export type AuditEventType = 'state_change' | 'tool_invocation' | 'error' | 'message'; @@ -44,15 +43,6 @@ function saveEvents(events: SvelteMap) { function createAuditStore() { const events = $state>(loadEvents()); - // Seed sample data if store is empty - if (events.size === 0) { - const samples = getSampleAuditData(); - for (const [id, evts] of samples) { - events.set(id, evts); - } - saveEvents(events); - } - function addEvent( sessionId: string, event: Omit diff --git a/src/lib/stores/memory.svelte.ts b/src/lib/stores/memory.svelte.ts index 6adcb07..381d251 100644 --- a/src/lib/stores/memory.svelte.ts +++ b/src/lib/stores/memory.svelte.ts @@ -1,6 +1,5 @@ import { SvelteMap } from 'svelte/reactivity'; import { ResultSource } from '$lib/proto/llm_multiverse/v1/common_pb'; -import { getSampleMemoryData } from '$lib/data/sampleData'; export interface StoredMemoryCandidate { content: string; @@ -14,9 +13,11 @@ export interface SessionMemory { } const STORAGE_KEY = 'llm-multiverse-memory-candidates'; +const SAMPLE_CLEANUP_KEY = 'llm-multiverse-sample-data-cleaned'; function loadMemory(): SvelteMap { if (typeof localStorage === 'undefined') return new SvelteMap(); + _cleanupSampleData(); try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return new SvelteMap(); @@ -27,6 +28,29 @@ function loadMemory(): 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) { if (typeof localStorage === 'undefined') return; localStorage.setItem(STORAGE_KEY, JSON.stringify([...memory.entries()])); @@ -35,15 +59,6 @@ function saveMemory(memory: SvelteMap) { function createMemoryStore() { const memory = $state>(loadMemory()); - // Seed sample data if store is empty - if (memory.size === 0) { - const samples = getSampleMemoryData(); - for (const [id, candidates] of samples) { - memory.set(id, candidates); - } - saveMemory(memory); - } - function addCandidates(sessionId: string, candidates: StoredMemoryCandidate[]) { if (candidates.length === 0) return; const existing = memory.get(sessionId) ?? []; diff --git a/src/lib/types/lineage.ts b/src/lib/types/lineage.ts index 74f11df..686197a 100644 --- a/src/lib/types/lineage.ts +++ b/src/lib/types/lineage.ts @@ -151,52 +151,3 @@ export function buildLineageTree(agents: SimpleAgentIdentifier[]): LineageNode[] return roots; } -/** - * Returns sample/demo lineage data for visualization development. - * This will be replaced with real API data when available. - */ -export function getSampleLineageData(): SimpleAgentIdentifier[] { - return [ - { - agentId: 'orch-001', - agentType: AgentType.ORCHESTRATOR, - spawnDepth: 0 - }, - { - agentId: 'research-001', - agentType: AgentType.RESEARCHER, - spawnDepth: 1, - parentId: 'orch-001' - }, - { - agentId: 'coder-001', - agentType: AgentType.CODER, - spawnDepth: 1, - parentId: 'orch-001' - }, - { - agentId: 'sysadmin-001', - agentType: AgentType.SYSADMIN, - spawnDepth: 1, - parentId: 'orch-001' - }, - { - agentId: 'assist-001', - agentType: AgentType.ASSISTANT, - spawnDepth: 2, - parentId: 'research-001' - }, - { - agentId: 'coder-002', - agentType: AgentType.CODER, - spawnDepth: 2, - parentId: 'coder-001' - }, - { - agentId: 'research-002', - agentType: AgentType.RESEARCHER, - spawnDepth: 3, - parentId: 'coder-002' - } - ]; -} diff --git a/src/lib/utils/logger.ts b/src/lib/utils/logger.ts new file mode 100644 index 0000000..d0e17e9 --- /dev/null +++ b/src/lib/utils/logger.ts @@ -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); + } + } +}; diff --git a/src/routes/audit/+page.svelte b/src/routes/audit/+page.svelte index 451db28..ae7da68 100644 --- a/src/routes/audit/+page.svelte +++ b/src/routes/audit/+page.svelte @@ -40,7 +40,7 @@
- +
diff --git a/src/routes/chat/+page.svelte b/src/routes/chat/+page.svelte index 4d9b815..5a83eb0 100644 --- a/src/routes/chat/+page.svelte +++ b/src/routes/chat/+page.svelte @@ -19,7 +19,7 @@ import { onMount, untrack } from 'svelte'; import { sessionStore } from '$lib/stores/sessions.svelte'; import { isNonDefaultConfig } from '$lib/utils/sessionConfig'; - import { createOrchestration } from '$lib/composables/useOrchestration.svelte'; + import { orchestrationStore as orchestration } from '$lib/composables/useOrchestration.svelte'; let messages: ChatMessage[] = $state([]); let initialized = $state(false); @@ -31,8 +31,6 @@ const lineageHref = resolveRoute('/lineage'); const memoryHref = resolveRoute('/memory'); const auditHref = resolveRoute('/audit'); - - const orchestration = createOrchestration(); const hasNonDefaultConfig = $derived(isNonDefaultConfig(sessionConfig)); function navigateToSession(sessionId: string, replace = false) { diff --git a/src/routes/lineage/+page.svelte b/src/routes/lineage/+page.svelte index f5f10ea..f695af7 100644 --- a/src/routes/lineage/+page.svelte +++ b/src/routes/lineage/+page.svelte @@ -7,7 +7,6 @@ import type { LineageNode, SimpleAgentIdentifier } from '$lib/types/lineage'; import { buildLineageTree, - getSampleLineageData, agentTypeLabel, agentTypeColor } from '$lib/types/lineage'; @@ -15,7 +14,7 @@ const chatHref = resolveRoute('/chat'); - let agents: SimpleAgentIdentifier[] = $state(getSampleLineageData()); + let agents: SimpleAgentIdentifier[] = $state([]); let treeNodes: LineageNode[] = $derived(buildLineageTree(agents)); let selectedNode: LineageNode | null = $state(null); @@ -34,7 +33,7 @@
- +
diff --git a/src/routes/memory/+page.svelte b/src/routes/memory/+page.svelte index 16cf645..6c60dab 100644 --- a/src/routes/memory/+page.svelte +++ b/src/routes/memory/+page.svelte @@ -39,7 +39,7 @@
- +
diff --git a/vite.config.ts b/vite.config.ts index bf699a8..cae7c30 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,5 +3,13 @@ import tailwindcss from '@tailwindcss/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [tailwindcss(), sveltekit()] + plugins: [tailwindcss(), sveltekit()], + server: { + proxy: { + '/llm_multiverse.v1.OrchestratorService': { + target: 'http://localhost:8080', + changeOrigin: true + } + } + } });