Skip to content
Merged

Dev #179

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
f3b657d
updated minor missing logics in the agent_base and event_stream
korivi-CraftOS Mar 30, 2026
6011a67
Node.js Stuck bug Fix
korivi-CraftOS Mar 30, 2026
b4897ba
fix schedule task import + details to schedule task desc
ahmad-ajmal Mar 30, 2026
745df23
prompt change for follow up questions
ahmad-ajmal Mar 30, 2026
cce7377
await send message
ahmad-ajmal Mar 30, 2026
15f55f2
Fix port 7926 occupied issue
zfoong Mar 31, 2026
709cae3
improvement:hide system error message when send message with attachem…
zfoong Mar 31, 2026
0c6a32f
improvement:improve and fix command
zfoong Mar 31, 2026
09ecb97
DeepSeek, Grok added to LLM's
korivi-CraftOS Mar 31, 2026
000adfa
feature:long-task and mission handling, bug:fix minor ui bug, enable …
zfoong Apr 1, 2026
421d889
Merge pull request #164 from CraftOS-dev/feature/long-task-handling
zfoong Apr 2, 2026
24edac5
overrite setting json
zfoong Apr 2, 2026
7a10bb5
Merge pull request #170 from CraftOS-dev/V1.2.2
korivi-CraftOS Apr 2, 2026
edfc98b
Added STT (Speech-to-Text) to the chat area
korivi-CraftOS Apr 2, 2026
1280bb7
feature:rate-limit, bug:fix caching and error spamming issue with Ant…
zfoong Apr 3, 2026
b9459d9
Merge pull request #171 from CraftOS-dev/feature/model-rate-limit
zfoong Apr 3, 2026
5c381c2
bug:fixed routing issue, include main event stream
zfoong Apr 3, 2026
fe2582d
Update settings.json
korivi-CraftOS Apr 3, 2026
a409c14
Merge pull request #172 from CraftOS-dev/feature/exp
korivi-CraftOS Apr 3, 2026
85a18d8
Added FORMAT.md
zfoong Apr 6, 2026
ce29b2b
Fix minor FORMAT.md mistake
zfoong Apr 6, 2026
584c23d
Merge pull request #173 from CraftOS-dev/feature/FORMAT.md
zfoong Apr 6, 2026
264feb1
SOUL.md update
zfoong Apr 6, 2026
7cdb43b
Merge pull request #174 from CraftOS-dev/feature/SOUL.md
zfoong Apr 6, 2026
eb326f2
bug:fix graceful shutdown with exit and q command
zfoong Apr 7, 2026
ca28342
feature:update version function
zfoong Apr 7, 2026
9724f73
feature:implemented tasks and event stream persistent
zfoong Apr 7, 2026
7413306
Merge pull request #175 from CraftOS-dev/feature/task-persistent
zfoong Apr 7, 2026
8b31167
Fix: Graceful Shutdown #160 & Agent can help user connect to external…
korivi-CraftOS Apr 7, 2026
20add45
Merge branch 'V1.2.2' of https://github.com/CraftOS-dev/CraftBot into…
korivi-CraftOS Apr 7, 2026
d81ed05
minor UI update for recording button
zfoong Apr 7, 2026
519d8d0
Merge branch 'V1.2.2' of https://github.com/craftos-dev/craftbot into…
zfoong Apr 7, 2026
8b01b2e
bug:fix Anthropic caching calculation and token usage calculation bug
zfoong Apr 7, 2026
fa4cc83
improvement:added Superpowers agent skill
zfoong Apr 7, 2026
991f654
remove byteplus API key from setting
zfoong Apr 7, 2026
99622f6
Hide GUI mode temporary
zfoong Apr 8, 2026
927f5f9
Service py added.
korivi-CraftOS Apr 8, 2026
fc89509
Whatsapp integration fix
ahmad-ajmal Apr 8, 2026
f9e2fbf
whatsapp prompting
ahmad-ajmal Apr 8, 2026
9b875b0
Fixed and updated the following issues in branch v1.2.2:
korivi-CraftOS Apr 8, 2026
5285bfe
Merge branch 'V1.2.2' of https://github.com/CraftOS-dev/CraftBot into…
korivi-CraftOS Apr 8, 2026
d758322
record whatsapp conversation history
ahmad-ajmal Apr 8, 2026
7942b9d
Merge branch 'V1.2.2' of https://github.com/CraftOS-dev/CraftBot into…
ahmad-ajmal Apr 8, 2026
b0a2779
The installation is failing Fix the issues
korivi-CraftOS Apr 8, 2026
5c3a13f
Merge branch 'V1.2.2' of https://github.com/CraftOS-dev/CraftBot into…
korivi-CraftOS Apr 8, 2026
66d0efd
Fix WhatsApp session persistence, Telegram send reliability, and thir…
ahmad-ajmal Apr 8, 2026
6687b14
Issue #176 on resetting agent, conversation history isn't reset
ahmad-ajmal Apr 8, 2026
dddb7fb
Bugfix #177 and other minor
korivi-CraftOS Apr 8, 2026
fc45886
Merge branch 'V1.2.2' of https://github.com/CraftOS-dev/CraftBot into…
korivi-CraftOS Apr 8, 2026
8811c55
Conda issue fix of terminal
korivi-CraftOS Apr 8, 2026
56bb95f
Node.js terminal window appears fix
korivi-CraftOS Apr 8, 2026
5bd90d8
The Ollama REST API issue fix
korivi-CraftOS Apr 8, 2026
016d001
The Ollama REST API
korivi-CraftOS Apr 8, 2026
b44992b
Ollama bug fix
korivi-CraftOS Apr 8, 2026
25a9f69
Local LLM fix.
korivi-CraftOS Apr 8, 2026
30afb49
Local LLM Fix
korivi-CraftOS Apr 8, 2026
5f73af1
dashboard_metrics_filter Bug fix
korivi-CraftOS Apr 8, 2026
d173a26
Update run shell action to enable it to run as background process
zfoong Apr 9, 2026
07fa7b7
renaming files in workspace persist file extension
ahmad-ajmal Apr 9, 2026
8956b07
replace base64 strings with file paths
ahmad-ajmal Apr 9, 2026
0cb6c05
Merge pull request #178 from CraftOS-dev/V1.2.2
ahmad-ajmal Apr 9, 2026
b03bc1c
Merge branch 'staging' into dev
ahmad-ajmal Apr 9, 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
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,18 @@ python run.py --gui
| `--cli` | Run in **CLI** mode (lightweight) |
| `--gui` | Enable GUI automation mode (requires `install.py --gui` first) |

### service.py

| Command | Description |
|---------|-------------|
| `install` | Install deps, register auto-start, and start CraftBot |
| `start` | Start CraftBot in the background |
| `stop` | Stop CraftBot |
| `restart` | Stop then start |
| `status` | Show running status and auto-start state |
| `logs [-n N]` | Show last N log lines (default: 50) |
| `uninstall` | Remove auto-start registration |

**Installation Examples:**
```bash
# Simple pip installation (no conda)
Expand Down Expand Up @@ -247,6 +259,39 @@ python run.py --gui
conda run -n craftbot python run.py
```

### 🔧 Background Service (Recommended)

Run CraftBot as a background service so it stays running even after you close the terminal. A desktop shortcut is created automatically so you can reopen the browser anytime.

```bash
# Install dependencies, register auto-start on login, and start CraftBot
python service.py install
```

That's it. The terminal closes itself, CraftBot runs in the background, and the browser opens automatically.

```bash
# Other service commands:
python service.py start # Start CraftBot in background
python service.py status # Check if it's running
python service.py stop # Stop CraftBot
python service.py restart # Restart CraftBot
python service.py logs # See recent log output
```

| Command | Description |
|---------|-------------|
| `python service.py install` | Install dependencies, register auto-start on login, start CraftBot, open browser, and close the terminal automatically |
| `python service.py start` | Start CraftBot in the background — auto-restarts if already running (terminal closes automatically) |
| `python service.py stop` | Stop CraftBot |
| `python service.py restart` | Stop and start CraftBot |
| `python service.py status` | Check if CraftBot is running and if auto-start is enabled |
| `python service.py logs` | Show recent log output (`-n 100` for more lines) |
| `python service.py uninstall` | Stop CraftBot, remove auto-start registration, uninstall pip packages, and purge pip cache |

> [!TIP]
> After `service.py start` or `service.py install`, a **CraftBot desktop shortcut** is created automatically. If you accidentally close the browser, just double-click the shortcut to reopen it.

> [!NOTE]
> **Installation:** The installer now provides clear guidance if dependencies are missing. If Node.js is not found, you'll be prompted to install it or can switch to TUI mode. Installation automatically detects GPU availability and falls back to CPU-only mode if needed.

Expand Down
2 changes: 1 addition & 1 deletion agent_core/core/embedding_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def _get_ollama_embedding(self, text: str) -> Optional[List[float]]:
"model": self.model,
"prompt": text, # Ollama accepts "prompt" for /api/embeddings
}
url: str = f"{self.remote_url.rstrip('/')}/embeddings"
url: str = f"{self.remote_url.rstrip('/')}/api/embeddings"
response = requests.post(url, json=payload, timeout=120)
response.raise_for_status()
result = response.json()
Expand Down
51 changes: 50 additions & 1 deletion agent_core/core/event_stream/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional, List
from typing import Any, Dict, Optional, List


SEVERITIES = ("DEBUG", "INFO", "WARN", "ERROR")
Expand Down Expand Up @@ -64,6 +64,32 @@ def display_text(self) -> Optional[str]:
"""
return self.display_message

def to_dict(self) -> Dict[str, Any]:
"""Serialize the event to a dictionary for persistence."""
return {
"message": self.message,
"kind": self.kind,
"severity": self.severity,
"display_message": self.display_message,
"ts": self.ts.isoformat(),
}

@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Event":
"""Deserialize an event from a dictionary."""
ts = (
datetime.fromisoformat(data["ts"])
if isinstance(data.get("ts"), str)
else datetime.now(timezone.utc)
)
return cls(
message=data["message"],
kind=data["kind"],
severity=data["severity"],
display_message=data.get("display_message"),
ts=ts,
)

@property
def iso_ts(self) -> str:
"""
Expand Down Expand Up @@ -92,6 +118,29 @@ class EventRecord:
repeat_count: int = 1
_cached_tokens: int | None = field(default=None, repr=False)

def to_dict(self) -> Dict[str, Any]:
"""Serialize the event record to a dictionary for persistence."""
return {
"event": self.event.to_dict(),
"ts": self.ts.isoformat(),
"repeat_count": self.repeat_count,
}

@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "EventRecord":
"""Deserialize an event record from a dictionary."""
event = Event.from_dict(data["event"])
ts = (
datetime.fromisoformat(data["ts"])
if isinstance(data.get("ts"), str)
else datetime.now(timezone.utc)
)
return cls(
event=event,
ts=ts,
repeat_count=data.get("repeat_count", 1),
)

def compact_line(self) -> str:
"""
Generate a compact single-line representation of this event.
Expand Down
67 changes: 67 additions & 0 deletions agent_core/core/impl/action/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,10 @@ async def execute_action(
logger.error(f"[ERROR] Failed to execute divisible action {action.name}: {e}", exc_info=True)
raise e

# Auto-save large base64 strings in action output to temp files
# This prevents LLMs from truncating binary data when it appears in context
outputs = self._extract_base64_to_files(outputs, action.name)

logger.debug(f"[OUTPUT DATA] Final outputs for action {action.name}: {outputs}")

if status != "error":
Expand Down Expand Up @@ -591,3 +595,66 @@ async def run_observe_step(self, action: Action, action_output: Dict) -> Dict[st
attempt += 1

return {"success": False, "message": "Observation failed or timed out."}

@staticmethod
def _extract_base64_to_files(data: dict, action_name: str) -> dict:
"""
Scan action output for large base64 data URLs and save them to temp files.
Replaces the base64 string with the file path so LLMs don't truncate it.
"""
import tempfile
import base64
import os
import re

if not isinstance(data, dict):
return data

MIN_BASE64_LENGTH = 500 # Only process strings longer than this

def process_value(key: str, value):
if not isinstance(value, str) or len(value) < MIN_BASE64_LENGTH:
return value

# Check for data URL format: data:image/png;base64,iVBOR...
match = re.match(r'^data:([\w/+.-]+);base64,(.+)$', value, re.DOTALL)
if match:
mime_type = match.group(1)
b64_data = match.group(2)
ext = {
'image/png': '.png',
'image/jpeg': '.jpg',
'image/gif': '.gif',
'image/webp': '.webp',
'application/pdf': '.pdf',
}.get(mime_type, '.bin')

try:
decoded = base64.b64decode(b64_data)
tmp = tempfile.NamedTemporaryFile(
delete=False, suffix=ext,
prefix=f"{action_name}_{key}_",
)
tmp.write(decoded)
tmp.close()
logger.info(f"[ACTION] Saved base64 {key} ({len(b64_data)} chars) to {tmp.name}")
return tmp.name
except Exception as e:
logger.warning(f"[ACTION] Failed to extract base64 from {key}: {e}")

return value

result = {}
for k, v in data.items():
if isinstance(v, dict):
result[k] = ActionManager._extract_base64_to_files(v, action_name)
elif isinstance(v, list):
result[k] = [
ActionManager._extract_base64_to_files(item, action_name) if isinstance(item, dict)
else process_value(k, item) if isinstance(item, str)
else item
for item in v
]
else:
result[k] = process_value(k, v)
return result
10 changes: 7 additions & 3 deletions agent_core/core/impl/action/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from agent_core.core.protocols.context import ContextEngineProtocol
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.prompts import (
SELECT_ACTION_PROMPT,
SELECT_ACTION_IN_TASK_PROMPT,
Expand Down Expand Up @@ -538,7 +539,7 @@ async def _prompt_for_decision(
# agent_info is included for all modes to provide consistent agent context
system_prompt, _ = self.context_engine.make_prompt(
user_flags={"query": False, "expected_output": False},
system_flags={"agent_info": True, "policy": False},
system_flags={"agent_info": True},
)

raw_response = None
Expand Down Expand Up @@ -620,6 +621,9 @@ async def _prompt_for_decision(
f"{raw_response} | error={feedback_error}"
)
current_prompt = self._augment_prompt_with_feedback(prompt, attempt + 1, raw_response, feedback_error)
except LLMConsecutiveFailureError:
# Fatal: LLM is in a broken state - re-raise immediately, do not retry
raise
except RuntimeError as e:
# LLM provider error (empty response, API error, auth failure, etc.)
error_msg = str(e)
Expand All @@ -633,8 +637,8 @@ async def _prompt_for_decision(
raise last_error
# Otherwise, retry with more context in the prompt
current_prompt = self._augment_prompt_with_feedback(
prompt, attempt + 1,
f"[LLM ERROR] {error_msg}",
prompt, attempt + 1,
f"[LLM ERROR] {error_msg}",
"LLM provider failed - retrying"
)
except Exception as e:
Expand Down
18 changes: 18 additions & 0 deletions agent_core/core/impl/context/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
AGENT_FILE_SYSTEM_CONTEXT_PROMPT,
POLICY_PROMPT,
USER_PROFILE_PROMPT,
SOUL_PROMPT,
LANGUAGE_INSTRUCTION,
)
from agent_core.core.state import get_state, get_session_or_none
Expand Down Expand Up @@ -225,6 +226,21 @@ def create_system_user_profile(self) -> str:

return ""

def create_system_soul(self) -> str:
"""Create a system message block with agent soul/personality from SOUL.md."""
try:
from app.config import AGENT_FILE_SYSTEM_PATH
soul_md_path = AGENT_FILE_SYSTEM_PATH / "SOUL.md"

if soul_md_path.exists():
content = soul_md_path.read_text(encoding="utf-8").strip()
if content:
return SOUL_PROMPT.format(soul_content=content)
except Exception as e:
logger.warning(f"[CONTEXT] Failed to read SOUL.md: {e}")

return ""

def create_system_language_instruction(self) -> str:
"""Create a system message block with language instruction.

Expand Down Expand Up @@ -683,6 +699,7 @@ def make_prompt(
"role_info": True,
"agent_info": True,
"user_profile": True,
"soul": True,
"language_instruction": True,
"policy": True,
"environment": True,
Expand All @@ -700,6 +717,7 @@ def make_prompt(
system_sections = [
("agent_info", self.create_system_agent_info),
("user_profile", self.create_system_user_profile),
("soul", self.create_system_soul),
("language_instruction", self.create_system_language_instruction),
("policy", self.create_system_policy),
("role_info", self.create_system_role_info),
Expand Down
4 changes: 3 additions & 1 deletion agent_core/core/impl/event_stream/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
# Re-export data classes from existing location
from agent_core.core.event_stream.event import Event, EventRecord

# Token utilities (canonical location: agent_core.utils.token)
from agent_core.utils.token import count_tokens

# Implementation classes
from agent_core.core.impl.event_stream.event_stream import (
EventStream,
count_tokens,
get_cached_token_count,
SEVERITIES,
MAX_EVENT_INLINE_CHARS,
Expand Down
50 changes: 22 additions & 28 deletions agent_core/core/impl/event_stream/event_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,38 +26,12 @@
from sklearn.feature_extraction.text import TfidfVectorizer
from agent_core.utils.logger import logger
from agent_core.decorators import profiler, OperationCategory
from agent_core.utils.token import count_tokens
import threading
import tiktoken
# Ensure tiktoken extension encodings (cl100k_base, etc.) are registered.
# Required for tiktoken >= 0.12 and PyInstaller frozen builds.
try:
import tiktoken_ext.openai_public # noqa: F401
except ImportError:
pass

SEVERITIES = ("DEBUG", "INFO", "WARN", "ERROR")
MAX_EVENT_INLINE_CHARS = 200000

# Token counting utility
_tokenizer = None

def _get_tokenizer():
"""Get or create the tiktoken tokenizer (cached for performance)."""
global _tokenizer
if _tokenizer is None:
try:
_tokenizer = tiktoken.get_encoding("cl100k_base")
except Exception:
# Fallback: use o200k_base if cl100k_base is unavailable
_tokenizer = tiktoken.get_encoding("o200k_base")
return _tokenizer

def count_tokens(text: str) -> int:
"""Count the number of tokens in a text string using tiktoken."""
if not text:
return 0
return len(_get_tokenizer().encode(text))


def get_cached_token_count(rec: "EventRecord") -> int:
"""Get token count for an EventRecord, using cached value if available.
Expand Down Expand Up @@ -281,6 +255,16 @@ def summarize_by_LLM(self) -> None:
)

try:
# Skip LLM call if the LLM is already in a consecutive failure state
max_failures = getattr(self.llm, "_max_consecutive_failures", 5)
current_failures = getattr(self.llm, "consecutive_failures", 0)
if current_failures >= max_failures:
logger.warning(
f"[EventStream] Skipping LLM summarization: LLM has {current_failures} "
f"consecutive failures (max={max_failures}). Falling back to prune."
)
raise RuntimeError("LLM in consecutive failure state, skip summarization")

logger.info(f"[EventStream] Running synchronous summarization ({self._total_tokens} tokens)")
llm_output = self.llm.generate_response(user_prompt=prompt)
new_summary = (llm_output or "").strip()
Expand All @@ -303,7 +287,17 @@ def summarize_by_LLM(self) -> None:
logger.info(f"[EventStream] Summarization complete. Tokens: {self._total_tokens}")

except Exception:
logger.exception("[EventStream] LLM summarization failed. Keeping all events without summarization.")
logger.exception(
"[EventStream] LLM summarization failed. "
"Pruning oldest events without a summary to prevent retry spam."
)
# Fallback: drop the oldest chunk without generating a summary so that
# _total_tokens falls below the threshold. Without this, every subsequent
# log() call would immediately re-trigger summarization and flood the logs.
removed_tokens = sum(get_cached_token_count(r) for r in chunk)
self._total_tokens -= removed_tokens
self.tail_events = self.tail_events[cutoff:]
self._session_sync_points.clear()

# ───────────────────── utilities ─────────────────────

Expand Down
Loading