#region ModulesContract [C:4] [TYPE ADR] [SEMANTICS contracts,modules,agent-chat] @defgroup Contracts Module and function contracts for 035-agent-chat-context implementation. @RATIONALE Content model updated for Path B (/agent dedicated page), Gradio payload safety, two-layer RBAC enforcement, and permission_denied flow separation. @REJECTED Path A (drawer panel on any page) — rejected in favor of dedicated /agent route for simpler state management. #region AgentChat.LangGraph.Handler [C:4] [TYPE Function] [SEMANTICS agent-chat,handler,context] # @ingroup AgentChat # @BRIEF Extended agent_handler — accepts uicontext JSON appended at end of positional args, injects into runtime context, filters tools by objectType. # @PRE JWT valid, user authenticated. uicontext_str is valid JSON or None. # @POST UIContext injected into system prompt. Tools filtered by objectType. Stream unchanged for uicontext=None (backward-compat). # @SIDE_EFFECT Calls LLM, invokes tools, writes checkpoints. UIContext validated and logged via CoT logger. # @RELATION DEPENDS_ON -> [AgentChat.ToolFilter] # @RELATION DEPENDS_ON -> [AgentChat.Confirmation] # @DATA_CONTRACT Input: (message, history, convId, userId, userJwt, envId, uicontext_str?) -> Output: AsyncGenerator[str] # @TEST_EDGE: null_uicontext -> All 22 tools passed to LLM (backward-compat). # @TEST_EDGE: dashboard_context -> Only 9 dashboard tools passed. # @TEST_EDGE: invalid_json_uicontext -> Gracefully ignored with audit log, all tools passed. # @TEST_EDGE: malformed_objectType -> Rejected with 422, audit log written. # @RATIONALE uicontext_str appended at END of positional args (position 6) — inserting in the middle would break existing Gradio clients that pass 6 args. Null default ensures zero breakage. # @REJECTED Middle-position insertion — rejected: breaking change for existing Gradio clients. #endregion AgentChat.LangGraph.Handler #region AgentChat.ToolFilter [C:3] [TYPE Module] [SEMANTICS agent-chat,tools,filter,context] # @defgroup AgentChat Context-aware tool filtering by objectType. # @LAYER Service # @RELATION DEPENDS_ON -> [AgentChat.Tools] # #region AgentChat.ToolFilter.Pipeline [C:4] [TYPE Function] [SEMANTICS agent-chat,tools,filter,pipeline] # @ingroup AgentChat # @BRIEF Canonical tool selection pipeline — applies in order: RBAC filter → context affinity filter. # @POST Returns list[BaseTool] after applying: (1) role-based exclusion, (2) context-type hard filter. show_capabilities always included. # @SIDE_EFFECT CoT logs reason for each excluded tool. # @RELATION DEPENDS_ON -> [AgentChat.Tools] # @DATA_CONTRACT Input: (all_tools, user_role, object_type) -> Output: filtered_tools # @RATIONALE Ordered pipeline ensures RBAC is enforced FIRST (security), then context filtering (UX). Two-layer: prompt filtering + invocation enforcement (see AGTL-FR-005). # @REJECTED Embedding-based filtering — rejected for initial release; postponed to post-MVP (AGTL-FR-006). # @TEST_EDGE: null_objectType -> Returns all RBAC-allowed tools. # @TEST_EDGE: dashboard+analyst -> Returns 9 dashboard tools minus any requiring admin. # @TEST_EDGE: unknown_objectType -> Returns all RBAC-allowed tools (graceful fallback). _CONTEXT_TOOL_AFFINITY: dict[str, set[str]] = { "dashboard": {"search_dashboards", "get_health_summary", "deploy_dashboard", "run_llm_validation", "run_llm_documentation", "execute_migration", "create_branch", "commit_changes"}, "dataset": {"superset_explore_database", "superset_format_sql", "superset_audit_permissions", "superset_execute_sql", "superset_create_dataset", "search_dashboards", "get_task_status", "list_environments"}, "migration": {"execute_migration", "search_dashboards", "get_health_summary", "deploy_dashboard", "list_environments"}, } _TOOL_PERMISSIONS: dict[str, list[str]] = { "deploy_dashboard": ["admin"], "commit_changes": ["admin"], "create_branch": ["admin"], "run_backup": ["admin"], "execute_migration": ["admin"], "start_maintenance": ["admin"], "end_maintenance": ["admin"], } def build_tool_pipeline(tools, user_role, object_type): """Apply RBAC + context filtering in canonical order.""" pass # #endregion AgentChat.ToolFilter.Pipeline # #region AgentChat.ToolFilter.InvocationGuard [C:3] [TYPE Function] [SEMANTICS agent-chat,tools,security,guard] # @ingroup AgentChat # @BRIEF Invocation-time RBAC enforcement — validates tool execution permission regardless of prompt filtering. # @PRE tool_name extracted from tool invocation request. User role resolved from JWT. # @POST If role insufficient: yield permission_denied SSE event (NOT confirm_required). If role sufficient: proceed. # @SIDE_EFFECT CoT logger.explore on denied. Audit log written. # @DATA_CONTRACT Input: (tool_name, user_role) -> Output: None (allowed) or yield permission_denied SSE # @RATIONALE Two-layer enforcement (AGTL-FR-005): prompt filtering prevents LLM from proposing disallowed tools, but invocation guard catches hallucinated/replayed/direct calls. Security-critical — prompt filtering alone is insufficient. # @TEST_EDGE: analyst_calls_deploy -> permission_denied SSE, tool not executed. # @TEST_EDGE: admin_calls_deploy -> allowed, no event. # @TEST_EDGE: unknown_tool -> rejected with error, not permission_denied. def enforce_tool_permission(tool_name, user_role): """Invocation-time RBAC enforcement — yield permission_denied or proceed.""" pass # #endregion AgentChat.ToolFilter.InvocationGuard #endregion AgentChat.ToolFilter #region AgentChat.Tools.Retry [C:3] [TYPE Function] [SEMANTICS agent-chat,tools,retry,http] # @ingroup AgentChat # @BRIEF Fixed-delay retry wrapper for read-only tool HTTP calls on transient errors. # @PRE Tool is classified as read-only (risk_level="safe"). Error is 5xx or connection error. # @POST Retries once with fixed 1s delay. On success: returns response. On exhaust: raises with retry_exhausted=True. # @SIDE_EFFECT HTTP call retried; CoT loggers REASON/EXPLORE/REFLECT on each attempt. # @RELATION DEPENDS_ON -> [EXT:httpx:AsyncClient] # @TEST_EDGE: first_attempt_502 -> Auto-retries once, succeeds. # @TEST_EDGE: both_attempts_502 -> Raises error with retry_exhausted=True. # @TEST_EDGE: write_tool_502 -> No retry (read-only only), raises immediately. # @RATIONALE 1 retry with 1s fixed delay catches ~80% of transient failures. Read-only only — write idempotency not guaranteed. # @REJECTED Exponential backoff — rejected: single retry cannot be exponential; 2+ retries with backoff adds 3s+ latency. # @REJECTED Retry all tools — rejected: write operations may not be idempotent. async def _retry_read_tool(func, *args, **kwargs): """Auto-retry read-only tool on transient HTTP errors. Fixed 1s delay.""" pass # #endregion AgentChat.Tools.Retry #region AgentChat.Tools.Summarise [C:2] [TYPE Function] [SEMANTICS agent-chat,tools,summarise,response] # @ingroup AgentChat # @BRIEF Structured response summarisation — top-N items + total count for large tool responses. # @POST When response >4000 chars and JSON array: "Found {total} items:\n - {item1}\n ...\n + {total-5} more." # When >4000 chars and JSON object: "Result keys: {keys}. Values: {sample}..." # When >4000 chars and not JSON: truncate at sentence boundary near 4000 chars + "..." # When ≤4000 chars: return original. # @RATIONALE Structured truncation preserves semantic value for LLM context better than hard cut. # @REJECTED LLM-based summarisation — extra token cost and latency. # @REJECTED Pagination in every tool — 22 tools to modify. Terminology: "structured truncation" not "semantic summarisation." def _summarise_response(text: str, limit: int = 4000) -> str: """Structured truncation — top-N + count for large responses.""" pass # #endregion AgentChat.Tools.Summarise #region AgentChat.Tools.Timeout [C:3] [TYPE Function] [SEMANTICS agent-chat,tools,timeout] # @ingroup AgentChat # @BRIEF Configurable timeout wrapper for tool execution (default 30s). # @PRE asyncio event loop running. # @POST Returns tool result within timeout. On timeout: yields tool_timeout SSE. For write tools: message says "operation status unknown — check status" (NOT "retry"). # @SIDE_EFFECT asyncio.wait_for with timeout; CoT logger.explore on timeout. # @RELATION DEPENDS_ON -> [EXT:asyncio] # @TEST_EDGE: tool_completes_under_timeout -> Returns result normally. # @TEST_EDGE: read_tool_exceeds_30s -> Yields tool_timeout with retryable=true. # @TEST_EDGE: write_tool_exceeds_30s -> Yields tool_timeout with retryable=false, status_unknown=true. # @RATIONALE 30s default balances responsiveness with Superset API variability. Write-tool timeout is NOT safe to retry — the mutation may have succeeded server-side. # @REJECTED Auto-retry on write timeout — rejected: unsafe, may duplicate mutations. async def _execute_with_timeout(tool_fn, is_write: bool = False, timeout_s: int = 30): """Execute tool with configurable timeout. Write tools: no auto-retry on timeout.""" pass # #endregion AgentChat.Tools.Timeout #region AgentChat.Confirmation.GuardV2 [C:4] [TYPE Function] [SEMANTICS agent-chat,hitl,confirmation,guardrails] # @ingroup AgentChat # @BRIEF Build extended confirmation contract — three-axis risk classification with env context. # @POST Returns dict with risk, risk_level, dangerous, env_context, permission_granted, required_role, alternatives. # @RELATION DEPENDS_ON -> [AgentChat.ToolResolver] # @RELATION DEPENDS_ON -> [EXT:FastAPI:RBAC] # @DATA_CONTRACT Input: (tool_name, tool_args, user_role, target_env) -> Output: ConfirmMetadataV2 # @RATIONALE Three-axis classification (tool_risk × env_risk × permission) replaces single-axis prefix heuristic. # Env resolution: tool_args.env_id > submit envId > UIContext.envId > null. # Env normalization for risk tiers: env_id containing "prod" → "prod"; "stag"/"test" → "staging"; "dev"/"local" → "dev". # @REJECTED Single-axis prefix-only — cannot distinguish deploy to prod vs staging. # @REJECTED LLM-based risk scoring — adds 500ms latency. # @TEST_EDGE: deploy_to_prod -> risk="write", risk_level="guarded", env_context="prod", dangerous=false. # @TEST_EDGE: delete_dashboard_any_env -> risk_level="dangerous", dangerous=true. # @TEST_EDGE: search_dashboards -> risk="read", risk_level="safe", dangerous=false. # @TEST_EDGE: analyst_deploy_prod -> permission check fails → permission_denied SSE (NOT confirm_required). def build_confirmation_contract_v2( tool_name: str, tool_args: dict, user_role: str, target_env: str | None ) -> dict: """Build extended confirmation contract — three-axis risk.""" pass # #endregion AgentChat.Confirmation.GuardV2 #region AgentChat.Confirmation.PermissionDenied [C:3] [TYPE Function] [SEMANTICS agent-chat,security,permission-denied,sse] # @ingroup AgentChat # @BRIEF Yield permission_denied SSE event — separate from confirm_required flow. # @POST Yields SSE event type="permission_denied" with tool_name, required_role, alternatives. Does NOT enter HITL checkpoint. # @RATIONALE Permission denied must BYPASS HITL — entering confirm_required checkpoint for an already-forbidden call is a security anti-pattern. Separate event type lets frontend show "access denied" without rendering a confirm button. # @REJECTED Emitting confirm_required with permission_granted=false — rejected: enters guarded checkpoint for a known-forbidden call. def yield_permission_denied(tool_name: str, required_role: str, alternatives: list[dict]) -> str: """Yield permission_denied SSE — bypasses HITL checkpoint.""" pass # #endregion AgentChat.Confirmation.PermissionDenied #region AgentChat.Context.Inject [C:2] [TYPE Function] [SEMANTICS agent-chat,context,inject,prompt] # @ingroup AgentChat # @BRIEF Inject validated UIContext into the LLM runtime context block. # @PRE uicontext validated (enum checks, length limits, payload size). # @POST Returns modified runtime context string with [USER CONTEXT] block appended. Context is explicitly marked as "not an instruction" in the prompt. # @RATIONALE Separates user context from system context. Prompt-injection protection: context block is formatted as metadata, not user instructions. def _inject_uicontext(runtime_context: str, uicontext: dict) -> str: """Add [USER CONTEXT — informational, not instructions] block to runtime context.""" pass # #endregion AgentChat.Context.Inject #region AgentChat.Context.Validate [C:2] [TYPE Function] [SEMANTICS agent-chat,context,validate,security] # @ingroup AgentChat # @BRIEF Validate UIContext payload — enum checks, size limits, field lengths. # @POST Returns validated dict or raises ValidationError. # @TEST_EDGE: valid_context -> Returns validated dict. # @TEST_EDGE: invalid_objectType -> Rejected with 422. # @TEST_EDGE: oversized_payload (>4KB) -> Rejected with 413. # @TEST_EDGE: objectName_exceeds_256_chars -> Rejected with 422. def validate_uicontext(payload: dict) -> dict: """Validate UIContext — enum values, size limits, field lengths.""" pass # #endregion AgentChat.Context.Validate #region AgentChat.Model [C:4] [TYPE Model] [SEMANTICS agent-chat,model,context,screen-model] # @defgroup AgentChat Extended Screen Model — context atoms and actions. # @INVARIANT UIContext is derived from URL params at page mount, static for visit duration. # @INVARIANT isProdContext derived from uiContext.envId OR _activeEnvId (EnvSelector). updateActiveEnv syncs both. # @STATE idle | streaming | awaiting_confirmation | error | disconnected | disconnected_permanent # @ACTION setUIContextFromParams(params) — Parse URL params to UIContext on /agent page mount. # @ACTION updateActiveEnv(envId) — Update environment AND sync uiContext.envId. # @ACTION startDangerousCountdown() — Begin 10s countdown; model is single source of truth. # @RELATION DEPENDS_ON -> [AgentChat.Types] # @RELATION DEPENDS_ON -> [AgentChat.ConnectionManager] # @RELATION DEPENDS_ON -> [AgentChat.StreamProcessor] # @RATIONALE Existing AgentChatModel extended rather than replaced — atom addition is additive. # @REJECTED New context-only model — rejected: cross-model coordination for streaming state. # @REJECTED initialContext in assistantChatStore — rejected: violates context-per-visit model; store persistence creates stale-context trap. // NEW atoms (add to AgentChatModel class): // uiContext: UIContext | null = $state(null); // dangerousCountdown: number = $state(0); // dangerousCountdownActive: boolean = $state(false); // _activeEnvId: string | null = $state(null); // permissionAlternatives: Array<{action: string; prompt: string}> | null = $state(null); // NEW actions: // setUIContextFromParams(params: URLSearchParams): void { ... } // updateActiveEnv(envId: string | null): void { this._activeEnvId = envId; if (this.uiContext) this.uiContext.envId = envId; } // startDangerousCountdown(): void { ... } // #endregion AgentChat.Model # #region AgentChat.Model.SetContext [C:3] [TYPE Function] [SEMANTICS agent-chat,model,context,action] # @ingroup AgentChat # @BRIEF Parse URL search params into UIContext atom on /agent page mount. # @ACTION Public action — called from +page.svelte onMount. # @POST this.uiContext populated. this._activeEnvId set. CoT log emitted. # @TEST_EDGE: full_params -> uiContext populated with all fields, contextVersion=1. # @TEST_EDGE: no_params -> uiContext is null, pill shows "Контекст не выбран". # @TEST_EDGE: malformed_objectType -> treated as null (frontend validation). // setUIContextFromParams(params: URLSearchParams): void { // const objectType = (["dashboard","dataset","migration"].includes(params.get("objectType")||"") // ? params.get("objectType") : null) as UIContext["objectType"]; // ... // } # #endregion AgentChat.Model.SetContext #region AgentChat.Component [C:4] [TYPE Component] [SEMANTICS agent,chat,ui,svelte] # @ingroup AgentChat # @BRIEF Agent chat panel — extended with prod warning background+banner and context-aware process steps. # @RELATION BINDS_TO -> [AgentChat.Model] # @RELATION DEPENDS_ON -> [$lib/ui/Icon] # @UX_STATE prod_env -> bg-warning-light background + red PRODUCTION banner atop chat panel. # @UX_STATE non_prod_env -> Standard bg-surface-page background, no banner. # @UX_FEEDBACK Prod banner appears/disappears reactively on env change. # @UX_RECOVERY EnvSelector → select non-prod → background restores. # @RATIONALE Reactive background change gives immediate, non-intrusive prod warning. # @REJECTED Modal warning on every prod message — too intrusive. // CHANGES to AgentChat.svelte: // 1. Root div: class binding for prod background // 2. Prod banner using $lib/ui/Icon (existing) // 3. Process steps: first step uses model.contextPillLabel #endregion AgentChat.Component #region AgentChat.ConfirmationCard [C:4] [TYPE Component] [SEMANTICS agent-chat,confirmation,hitl,guardrails,svelte] # @ingroup AgentChat # @BRIEF HITL guardrails card — env badge, risk toning, dangerous countdown, permission-denied state. # @RELATION BINDS_TO -> [AgentChat.Model] # @RELATION DEPENDS_ON -> [$lib/ui/Button] # @RELATION DEPENDS_ON -> [$lib/ui/Icon] # @UX_STATE read/write_staging/write_prod/write_dev/dangerous/loading_confirm/loading_deny — 7 states. # @UX_STATE permission_denied — Separate rendering path; received as SSE event type="permission_denied" (NOT confirm_required). # @UX_FEEDBACK Dangerous countdown aria-live="assertive" every 1s (aligned with 1s decrement). # @UX_RECOVERY Deny → "Операция отменена" agent message in chat. # @UX_RECOVERY Permission denied → close card → agent message with alternatives. # @INVARIANT Card renders from confirm_required for permitted operations. permission_denied events bypass HITL checkpoint. # @INVARIANT Countdown timer owned by model (dangerousCountdown atom), component reads model only. # @RATIONALE Model-owns-countdown eliminates race condition between model and component timers. # @REJECTED Component-owned countdown — rejected: violates model-first pattern, creates timer duplication. // CHANGES to ConfirmationCard.svelte: // 1. 7 risk-based states (add write_dev) // 2. Countdown reads model.dangerousCountdown (no component timer) // 3. permission_denied: listens for SSE event type="permission_denied" // 4. Risk-based toneClasses extended with write_dev #endregion AgentChat.ConfirmationCard #region AgentChat.ToolCallCard [C:3] [TYPE Component] [SEMANTICS agent-chat,tools,retry,timeout,svelte] # @ingroup AgentChat # @BRIEF Inline tool call display — retrying, timeout, cancelled states. # @RELATION DEPENDS_ON -> [$lib/ui/Button] # @UX_STATE retrying/timeout/cancelled # @UX_FEEDBACK Retry button triggers message resubmission for read-only tools. Write tool timeout: no retry button ("check operation status"). # @UX_RECOVERY Read timeout → retry. Write timeout → check status. # @RATIONALE Write-tool retry is unsafe — timeout does not mean mutation didn't happen. # @REJECTED Retry for all timeouts — rejected: write retry may duplicate mutations. // CHANGES to ToolCallCard.svelte: // 1. onRetry prop shown only for read tools / transient errors // 2. Write timeout: "⏱️ Operation status unknown — check Superset" (no retry button) #endregion AgentChat.ToolCallCard #region AgentChat.Page [C:3] [TYPE Component] [SEMANTICS agent-chat,page,svelte] # @ingroup AgentChat # @BRIEF Agent page wrapper — reads UIContext from URL params on mount. # @RELATION BINDS_TO -> [AgentChat.Model] # @UX_STATE mount -> Reads URL params, calls model.setUIContextFromParams(). // CHANGES to +page.svelte: // import { page } from "$app/stores"; // onMount(() => { // const params = new URLSearchParams($page.url.search); // model.setUIContextFromParams(params); // }); #endregion AgentChat.Page #region AgentChat.StreamProcessor [C:4] [TYPE Module] [SEMANTICS agent-chat,streaming,sse,svelte] # @ingroup AgentChat # @BRIEF SSE stream processor — handles tool_retry, tool_timeout, permission_denied (new), extended confirm_required. # @RELATION BINDS_TO -> [AgentChat.Model] # @RELATION DEPENDS_ON -> [AgentChat.Types] // NEW event handler: // case "permission_denied": render permission-denied card (separate from confirm_required) // EXTENDED event handler: // case "confirm_required": parse dangerous, env_context, permission_granted. If permission_granted=false → redirect to permission_denied handler. // NEW handlers: // case "tool_retry": update ToolCall status=retrying // case "tool_timeout": update ToolCall status=timeout; if write tool → no retry button #endregion AgentChat.StreamProcessor #region AgentChat.Types [C:2] [TYPE Module] [SEMANTICS agent-chat,types,svelte] # @ingroup AgentChat # @BRIEF Shared TypeScript types — UIContext, ToolAffinityEntry, ConfirmMetadataV2, PermissionDeniedMetadata. // ADD interfaces (see data-model.md for full shapes): // export interface UIContext { objectType, objectId, objectName, envId, route, contextVersion } // export interface ConfirmMetadataV2 { ... with dangerous, env_context, permission_granted } // export interface PermissionDeniedMetadata { type:"permission_denied", tool_name, required_role, alternatives } // export type ToolCallStatus = "executing" | "retrying" | "completed" | "failed" | "timeout" | "cancelled"; // EXTEND ToolCall: +retryCount?, +maxRetries?, +timeoutSeconds?, +isWriteTool? #endregion AgentChat.Types #endregion ModulesContract