Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9aead2a
Living UI V2: replace FastAPI/Vite system with PocketBase + vendored-…
ahmad-ajmal Jul 24, 2026
b1d4370
Add form and QnA workflow, some UI update
Jul 26, 2026
4eb12e9
Theme fix
ahmad-ajmal Jul 25, 2026
bff7956
Living UI: shadcn-conventional kit APIs + approved npm dependency gate
ahmad-ajmal Jul 26, 2026
f4e597f
living UI building visualizer
Jul 26, 2026
86a1a5d
Merge branch 'livingui-redesign' of https://github.com/craftos-dev/cr…
Jul 26, 2026
74c3b17
living UI visual update during creation
Jul 26, 2026
a7457d2
separate walk verify + headless browser
ahmad-ajmal Jul 26, 2026
04e89a4
living UI import
ahmad-ajmal Jul 26, 2026
d0a68d3
Add more ShadCN compatible UI component
Jul 26, 2026
6c175b1
Merge branch 'livingui-redesign' of https://github.com/craftos-dev/cr…
Jul 26, 2026
cb578f1
allow installing any npm
ahmad-ajmal Jul 26, 2026
aec0087
auth fix
ahmad-ajmal Jul 26, 2026
252d20e
fix cli issue
Jul 26, 2026
c6ff943
Fix: Hide terminal pop ups
makiroll1125 Jul 27, 2026
60b37e7
Fix: Revert dev branch for living ui repo URL
makiroll1125 Jul 27, 2026
b52e957
Fix: Chat panel shows after installing Living UI for the first time
makiroll1125 Jul 28, 2026
bf9f399
Fix: Include sessionId in both places that broadcast living_ui_ready
makiroll1125 Jul 28, 2026
645c821
Fix: Make ensure_project_session() checks non-fatal
makiroll1125 Jul 28, 2026
6aa519e
Fix: Assign env vars for callLLM() for Living UIs
makiroll1125 Jul 28, 2026
5f3bea8
Revert changes Living UI marketplace links
makiroll1125 Jul 28, 2026
fb591fa
Error Catalogue revamp. gent_core/core/errors.py provides new shared…
makiroll1125 Jul 30, 2026
8241454
Fix: Ensure no consecutive errors, revert system error changes
makiroll1125 Jul 30, 2026
55861b0
Add A2APP — make agent writes to Living UIs verifiable
ahmad-ajmal Jul 30, 2026
b55d44f
lint fixes
ahmad-ajmal Jul 30, 2026
5412872
Split error messages into two presentation tiers based level of impor…
makiroll1125 Jul 31, 2026
7edd361
Fix: Testing and edge case fixes (blocked content error, NoneType cra…
makiroll1125 Jul 31, 2026
a0569ce
Improvement: Sync test between ErrorCategory and ERROR_CATEGORY_STYLE
makiroll1125 Jul 31, 2026
0369961
Port error catalogue to Provider interfaces (embedding interface, ima…
makiroll1125 Aug 3, 2026
d8c1475
Add the Factory — deterministic build orchestration for weak models
ahmad-ajmal Aug 3, 2026
f140637
Removed false claim gate
ahmad-ajmal Aug 3, 2026
592dfab
Merge remote-tracking branch 'origin/livingui-communication-protocol'…
ahmad-ajmal Aug 3, 2026
cf8d246
Merge pull request #397 from CraftOS-dev/livingui-third-party-fix
ahmad-ajmal Aug 4, 2026
28bd09c
Merge pull request #394 from CraftOS-dev/improvement/error-message
zfoong Aug 4, 2026
981f57b
Merge branch 'session-native-redesign' into livingui-redesign
ahmad-ajmal Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/living-ui-v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: living-ui-v2

# Self-test for the Living UI TEMPLATE code (kit/blueprint/tools) in this repo.
# Scaffolds a throwaway project and runs the local validation gate on it.
# User-made Living UIs never touch this workflow — they validate locally.

on:
push:
paths:
- 'living-ui-v2/**'
- '.github/workflows/living-ui-v2.yml'
pull_request:
paths:
- 'living-ui-v2/**'
- '.github/workflows/living-ui-v2.yml'

defaults:
run:
working-directory: living-ui-v2

jobs:
gate:
name: gate (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 24

- name: Cache PocketBase binary
uses: actions/cache@v4
with:
path: |
~/Library/Caches/craftos-living-ui/pb
~/.cache/craftos-living-ui/pb
~\AppData\Local\craftos-living-ui\pb
key: pb-${{ runner.os }}-${{ hashFiles('living-ui-v2/spec/pocketbase.version') }}

- name: Install workspace
run: npm install

- name: Typecheck (kit + tools)
run: npm run typecheck

- name: Lint
run: npx eslint .

- name: Scaffold demo project
run: node tools/src/cli.ts create "CI Demo" --description "CI validation project" --port 8090

- name: Link demo workspace
run: npm install

- name: Validation gate
run: node tools/src/cli.ts validate examples/ci-demo
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,5 @@ docs/LIVING_UI_DEVELOPER_GUIDE.md
agent_file_system/ACTIONS.md
agent_bundle/
**/.craftbot/
app/data/.file_index/
app/data/.file_index/
.playwright-mcp
1 change: 0 additions & 1 deletion .ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,5 @@ extend-exclude = [
"agents/dog_agent/data/action/dog_behaviour.py" = ["E402"]
"app/action/action_framework/run_actions_tests.py" = ["E402"]
"app/config.py" = ["E402"]
"app/llm_interface.py" = ["E402"]
"app/main.py" = ["E402"]
"craftos_integrations/__init__.py" = ["E402"]
38 changes: 27 additions & 11 deletions agent_core/core/embedding_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from agent_core.core.models.types import InterfaceType
from agent_core.utils.logger import logger

from agent_core.core.llm.google_gemini_client import GeminiAPIError, GeminiClient
from agent_core.core.llm.google_gemini_client import GeminiClient


class EmbeddingInterface:
Expand Down Expand Up @@ -91,26 +91,42 @@ def get_embedding(self, text: str) -> Optional[List[float]]:
raise RuntimeError(f"Unknown provider {self.provider!r}")

# ───────────────────── Provider-specific helpers ───────────────────
def _log_classified(self, tag: str, e: Exception) -> None:
"""Log *e* through the shared classifier instead of raw str(e)."""
from agent_core.core.impl.llm.errors import classify_llm_error

info = classify_llm_error(e, provider=self.provider, model=self.model)
logger.error(f"[EMBEDDING] {tag}: {info.message}")

@staticmethod
def _not_initialised(provider: str, client_name: str) -> "ClassifiedError":
from agent_core.core.errors import ClassifiedError
from agent_core.core.impl.llm.errors import classify_llm_error

return ClassifiedError(
classify_llm_error(
RuntimeError(f"{client_name} client was not initialised."),
provider=provider,
)
)

def _get_openai_embedding(self, text: str) -> Optional[List[float]]:
try:
response = self.client.embeddings.create(model=self.model, input=text)
# OpenAI returns: response.data[0].embedding
return response.data[0].embedding # type: ignore[attr-defined]
except Exception as e:
logger.exception(f"Error calling OpenAI Embedding API: {e}")
self._log_classified("OpenAI", e)
return None

def _get_gemini_embedding(self, text: str) -> Optional[List[float]]:
if not self._gemini_client:
raise RuntimeError("Gemini client was not initialised.")
raise self._not_initialised("gemini", "Gemini")

try:
return self._gemini_client.embed_text(self.model, text=text)
except GeminiAPIError as e:
logger.exception(f"Gemini rejected the embedding request: {e}")
return None
except Exception as e:
logger.exception(f"Error calling Gemini Embedding API: {e}")
self._log_classified("Gemini", e)
return None

def _get_byteplus_embedding(self, text: str) -> Optional[List[float]]:
Expand All @@ -137,7 +153,7 @@ def _get_byteplus_embedding(self, text: str) -> Optional[List[float]]:
return None
return data.get("embedding")
except Exception as e:
logger.exception(f"Error calling BytePlus Embedding API: {e}")
self._log_classified("BytePlus", e)
return None

def _get_bedrock_embedding(self, text: str) -> Optional[List[float]]:
Expand All @@ -148,7 +164,7 @@ def _get_bedrock_embedding(self, text: str) -> Optional[List[float]]:
(Converse doesn't expose embeddings).
"""
if not self._bedrock_client:
raise RuntimeError("Bedrock client was not initialised.")
raise self._not_initialised("bedrock", "Bedrock")

try:
import json as _json
Expand All @@ -165,7 +181,7 @@ def _get_bedrock_embedding(self, text: str) -> Optional[List[float]]:
result = _json.loads(raw)
return result.get("embedding")
except Exception as e:
logger.exception(f"Error calling Bedrock Embedding API: {e}")
self._log_classified("Bedrock", e)
return None

def _get_ollama_embedding(self, text: str) -> Optional[List[float]]:
Expand All @@ -181,5 +197,5 @@ def _get_ollama_embedding(self, text: str) -> Optional[List[float]]:
# Ollama returns {"embedding": [floats]}
return result.get("embedding", None)
except Exception as e:
logger.exception(f"Error calling Ollama Embedding API: {e}")
self._log_classified("Ollama", e)
return None
159 changes: 159 additions & 0 deletions agent_core/core/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
"""
Shared error-catalogue primitives.

`agent_core` never imports from `app` (the dependency runs the other way), so
the category/severity/action vocabulary shared between the LLM classifier
(`agent_core/core/impl/llm/errors.py`) and app-layer call sites
(`app/errors/codebook.py`) lives here.

`LLMErrorInfo` (in the LLM package) is intentionally NOT made a subclass of
`ErrorInfo` — its `provider` field is positional/non-default and reordering it
behind new defaulted base fields would break its existing consumers. Both
satisfy `ErrorInfoLike` structurally instead.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field, asdict
from enum import Enum
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable


class Severity(str, Enum):
INFO = "info"
WARNING = "warning"
ERROR = "error"
CRITICAL = "critical" # aborts a run


class ErrorCategory(str, Enum):
AUTH = "auth" # 401/403 — bad/missing key, key revoked
CREDIT = "credit" # 402, "insufficient_quota", "credit_balance_too_low"
RATE_LIMIT = "rate_limit" # 429 — transient
QUOTA = "quota" # 429 + monthly/account scope (separable from per-min)
MODEL = "model" # 404, "model_not_found"
BAD_REQUEST = "bad_request" # 400 — request malformed (context overflow, etc.)
BLOCKED = "blocked" # safety filter (Gemini/Anthropic)
SERVER = "server" # 5xx, "overloaded_error"
CONNECTION = "connection" # network / timeout / DNS
UNKNOWN = "unknown"
# App-layer categories, not produced by the LLM classifier:
VALIDATION = "validation" # malformed input outside an LLM call
NOT_FOUND = "not_found"
CONFIG = "config" # local misconfiguration (e.g. no key set, before any network call)
PERMISSION = "permission" # local/file/OS permission issues
INTERNAL = "internal" # unexpected/bug-shaped exception


# Categories where retrying the same request essentially never succeeds —
# these should fail fast instead of consuming a retry budget. RATE_LIMIT,
# SERVER, CONNECTION, and UNKNOWN are left out deliberately: they're the
# genuinely transient cases retries exist for.
FAIL_FAST_CATEGORIES = frozenset(
{
ErrorCategory.AUTH,
ErrorCategory.CREDIT,
ErrorCategory.QUOTA,
ErrorCategory.MODEL,
ErrorCategory.BLOCKED,
ErrorCategory.BAD_REQUEST,
ErrorCategory.CONFIG,
}
)


def is_transient(category: ErrorCategory) -> bool:
"""Whether retrying the same request has a real chance of succeeding."""
return category not in FAIL_FAST_CATEGORIES


@dataclass
class ErrorAction:
"""A clickable affordance attached to an error.

`url` opens in a new tab; `action` is a frontend-resolved verb such as
"open_settings_model" — handled by the chat component, not by URL nav.
Exactly one of url/action should be set.
"""

label: str
url: Optional[str] = None
action: Optional[str] = None


@dataclass
class ErrorInfo:
"""Generic app-wide structured error, for call sites outside the LLM
provider classifier (which uses the richer `LLMErrorInfo`)."""

category: ErrorCategory
code: str
title: str
message: str
severity: Severity = Severity.ERROR
actions: List[ErrorAction] = field(default_factory=list)
raw_message: Optional[str] = None
context: Dict[str, Any] = field(default_factory=dict)

@property
def is_transient(self) -> bool:
return is_transient(self.category)

def to_dict(self) -> Dict[str, Any]:
d = asdict(self)
d["category"] = self.category.value
d["severity"] = self.severity.value
return d


@runtime_checkable
class ErrorInfoLike(Protocol):
"""Structural type both `ErrorInfo` and `LLMErrorInfo` satisfy."""

category: ErrorCategory
title: str
message: str
actions: List[ErrorAction]


class ClassifiedError(Exception):
"""Wraps a classified `ErrorInfoLike`.

Presentation code (see `app/agent_base.py:_handle_react_error`) uses the
presence of this type anywhere in an exception's `__cause__`/`__context__`
chain — or an `LLMConsecutiveFailureError` with a populated
`last_error_info` — to tell a recognized, user-actionable failure ("minor"
tier: bad key, no credits, misconfigured provider) apart from a genuinely
unexpected crash ("critical" tier: unclassified bugs, broken agent loop).
Raise this instead of a bare `RuntimeError` at any call site that already
knows what went wrong.
"""

def __init__(self, info: ErrorInfoLike):
self.info = info
super().__init__(info.message)


# ─── Redaction ──────────────────────────────────────────────────────────
# Ported from the now-removed app/security/error_handler.py — applied to raw
# upstream/exception text before it's echoed to the UI (e.g. UNKNOWN/BAD_REQUEST
# fallback messages), not to the curated, hand-written catalogue strings.

_REDACT_PATTERNS = [
re.compile(r"/[^/\s]+\.py"), # file paths
re.compile(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+"), # emails
re.compile(r"://[^/\s]+"), # URLs/hostnames
re.compile(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"), # IPv4 addresses
]


def redact(raw: str, max_length: int = 500) -> str:
"""Strip file paths/emails/hostnames/IPs from raw exception text."""
text = raw
for pattern in _REDACT_PATTERNS:
text = pattern.sub("[REDACTED]", text)
if len(text) > max_length:
text = text[:max_length] + "..."
return text
18 changes: 15 additions & 3 deletions agent_core/core/impl/action/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from agent_core.core.protocols.llm import LLMInterfaceProtocol
from agent_core.core.impl.llm import LLMCallType
from agent_core.core.impl.llm.errors import LLMConsecutiveFailureError
from agent_core.core.errors import ClassifiedError, ErrorCategory, ErrorInfo
from agent_core.core.prompts import SELECT_ACTION_PROMPT
from agent_core.utils.logger import logger

Expand Down Expand Up @@ -377,13 +378,24 @@ async def _prompt_for_decision(
raise
except RuntimeError as e:
# LLM provider error (empty response, API error, auth failure, etc.)
# — a recognized, user-actionable failure, not a code bug. The
# attempt-number bookkeeping stays in the log only; the
# user-facing message (ClassifiedError.info.message) stays
# short and skips it.
error_msg = str(e)
logger.error(
f"[ACTION ROUTER] LLM provider error on attempt {attempt + 1}: {error_msg}"
)
last_error = RuntimeError(
f"Unable to generate action decision on attempt {attempt + 1}: {error_msg}. "
f"Check LLM configuration, API credentials, and service availability."
last_error = ClassifiedError(
ErrorInfo(
category=ErrorCategory.UNKNOWN,
code="ACTION_DECISION_FAILED",
title="Action decision failed",
message=(
f"{error_msg.rstrip('.')}. Check LLM configuration, "
f"API credentials, and service availability."
),
)
)
# After 3 attempts, give up
if attempt >= max_retries - 1:
Expand Down
Loading
Loading