Compare commits

..

No commits in common. "ba84ca1a3a678b6bff467c0a8eb520fc2760aca1" and "a56fd15d6739343c0b81a4b598c32ad22ed8d18e" have entirely different histories.

23 changed files with 138 additions and 369 deletions

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 Dita Aji Pratama
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1 +0,0 @@
service.yaml

View File

@ -1,23 +0,0 @@
# This file serves as a template for character-specific service overrides.
# Copy this to agent/characters/<character>/service.yaml and fill in the credentials.
xmpp:
enabled: false
username: ""
password: ""
muc_rooms: # YAML list, e.g.:
# - "room1@conference.server"
# - "room2@conference.server"
nickname: "" # custom MUC nickname (empty = use username)
selective_response: true # true = only response if mentioned/relevant
debug: false # true = tampilkan log debug menyeluruh (semua stanza XML, lifecycle, dll)
mute_session_end: false # true = tidak kirim pesan saat sesi berakhir (timeout)
telegram:
enabled: false
token: ""
allowed_group_ids: # YAML list, empty = all group. e.g.:
# - "123456789"
# - "987654321"
selective_response: true # true = only response if mentioned/relevant
mute_session_end: false # true = tidak kirim pesan saat sesi berakhir (timeout)

View File

@ -2,9 +2,9 @@ import os, sys, threading, time, signal
import config
from lib import gadget, personality, moop, workspace, ragroleplay as ragroleplay_lib
from lib import gadget, personality, moop, ragroleplay as ragroleplay_lib
from modules import argsparser
from tools import coder, carrack, ragroleplay as ragroleplay_tools, vision, imagegen, smart, smart_rp, syshendrik
from tools import coder, carrack, ragroleplay as ragroleplay_tools, vision, imagegen, smart, smart_rp
from services.llm_client import LLMClient
from services.xmpp_client import XMPPClient
@ -20,6 +20,7 @@ tools_definition = [
gadget.tools_mapping( schema = coder.schema_run_bash, handler = coder.run_bash ),
gadget.tools_mapping( schema = coder.schema_search_code, handler = coder.search_code ),
gadget.tools_mapping( schema = coder.schema_git_operation, handler = coder.git_operation ),
gadget.tools_mapping( schema = coder.schema_workspace_change, handler = coder.workspace_change ),
gadget.tools_mapping( schema = coder.schema_search_grep, handler = coder.search_grep ),
gadget.tools_mapping( schema = coder.schema_search_glob, handler = coder.search_glob ),
# Carrack Tools
@ -46,8 +47,6 @@ tools_definition = [
gadget.tools_mapping( schema = ragroleplay_tools.schema_memories_delete, handler = ragroleplay_tools.memories_delete ),
gadget.tools_mapping( schema = ragroleplay_tools.schema_end_session, handler = ragroleplay_tools.end_session ),
# SysHendrik Tools
gadget.tools_mapping( schema = syshendrik.schema_workspace_change, handler = syshendrik.workspace_change ),
]
@ -68,7 +67,7 @@ def main():
if not os.path.isdir(resolved):
print(f"Error: '{resolved}' is not a valid directory", flush=True)
sys.exit(1)
workspace.set_current_workspace(resolved)
coder.set_current_workspace(resolved)
# Inference
llm_client = LLMClient("", "", "", config.llm_timeout)

View File

@ -9,7 +9,7 @@ import curses
import os
import config
from .agent import log
from lib import workspace
from tools.coder import set_current_workspace
from . import keycodes
from .moopui import (
model_selector_popup,
@ -157,7 +157,7 @@ def workspace_popup(app, stdscr):
if ws:
resolved = os.path.abspath(ws)
if os.path.isdir(resolved):
workspace.set_current_workspace(resolved)
set_current_workspace(resolved)
log(app, "system", f"Workspace \u2192 {resolved}")
else:
log(app, "error", f"Invalid directory: {resolved}")

View File

@ -6,7 +6,7 @@
import curses
import json
from lib import workspace
from tools.coder import get_current_workspace
from . import theme
from .actions import ACTIONS
@ -361,7 +361,7 @@ def draw_status(app, stdscr):
mode = " PROCESSING " if app.processing else " READY "
set_name = (getattr(app, "model_set_name", "") or "-").strip() or "-"
model = (app.llm.model or "-").strip() or "-"
ws = workspace.get_current_workspace() or "-"
ws = get_current_workspace() or "-"
send_key = ACTIONS["send_prompt"].get("shortcut") or "Ctrl+Return"
right = f" {app.character_name} {send_key}:Send "

View File

@ -1,18 +0,0 @@
import os
# Pemilik tunggal state workspace sesi agent.
# Modul lain (tools.coder, tools.syshendrik, tools.vision, interfaces.tui)
# membaca/mengganti workspace lewat getter/setter di sini, bukan menyimpan
# salinan state sendiri, agar tidak ada dua sumber kebenaran.
CURRENT_WORKSPACE = os.getcwd()
def get_current_workspace():
global CURRENT_WORKSPACE
return CURRENT_WORKSPACE
def set_current_workspace(path):
global CURRENT_WORKSPACE
# chdir dulu agar state hanya ter-update ketika perpindahan benar-benar sukses
os.chdir(path)
CURRENT_WORKSPACE = path

View File

@ -1,78 +0,0 @@
# Feature: Mute Session End Notification
## Goal
Tambah field konfigurasi baru per-service (xmpp & telegram) untuk menonaktifkan
pesan pemberitahuan saat sesi berakhir. Jika `true`, agent tidak mengirim pesan
"pemberitahuan sesi berakhir" (`*session end*` / `Sesi ditutup. Sampai jumpa`)
ketika sesi ditutup karena timeout/kegiatan berhenti.
Field mengikuti pola yang sudah ada di `service.yaml` (sudah mendukung override
per karakter untuk xmpp & telegram).
## Field
- Nama: `mute_session_end`
- Tipe: bool
- Default: `false` (perilaku lama: tetap kirim pesan penutup)
- Letak: di bawah section `xmpp:` dan `telegram:` (service.yaml & config.yaml)
- Konstanta global baru: `XMPP_MUTE_SESSION_END`, `TELEGRAM_MUTE_SESSION_END`
## Scope pesan yang di-mute (jalur target)
Hanya pesan penutup yang dihasilkan oleh sistem saat sesi berakhir otomatis:
- `xmpp_client._timeout_session`
- `telegram_client._timeout_session`
Sesi tetap di-reset seperti semula; hanya pengiriman teks penutupnya yang dilewati.
**Dikonfirmasi:** natural-close via tool `end_session` TIDAK ikut di-mute (farewell
adalah bagian percakapan model, bukan pesan sistem).
## Perubahan tambahan: timeout group/MUC
Diperpanjang dari 5 menit (300s) menjadi 30 menit (1800s) untuk group/MUC.
Titik yang diubah:
- `telegram_client.py` (`_process_message`): `... else 300` → `1800` (chat_type group).
- `xmpp_client.py`: dua `session.start_timer(300, ..., 'groupchat')` → `1800`.
Private/DM tetap 86400 (di luar scope).
## Files to change
### 1. config.py
- Tambah pembacaan default dari `config.yaml` pada blok modul-level:
- `XMPP_MUTE_SESSION_END` (xmpp section, default "false")
- `TELEGRAM_MUTE_SESSION_END` (telegram section, default "false")
- Tambah keduanya ke `global` + reset di `_reset_service_from_config()`.
- Tambah keduanya ke `global` + aplikasi override di `load_service_config()`
memakai pola yang sama seperti field lain:
- `if "mute_session_end" in xmpp_data: XMPP_MUTE_SESSION_END = _to_bool(...)`
- `if "mute_session_end" in telegram_data: TELEGRAM_MUTE_SESSION_END = _to_bool(...)`
- Parsing bool pakai pola string existing: `str(v).strip().lower() in ("true","1","yes")`.
### 2. services/xmpp_client.py (`_timeout_session`)
- Bungkus `self._schedule_send(session_id, body, mtype)` dengan guard:
`if not config.XMPP_MUTE_SESSION_END:` — tetap panggil `self._session_mgr.reset(session_id)`.
- Opsional: log bahwa notifikasi di-mute.
### 3. services/telegram_client.py (`_timeout_session`)
- Bungkus `self._schedule_send(chat_id, body)` dengan guard:
`if not config.TELEGRAM_MUTE_SESSION_END:` — tetap `self._session_mgr.reset(...)`.
### 4. Templates (dokumentasi, agar field mudah ditemukan)
- `config.default.yaml`: tambah `mute_session_end: false` di section xmpp & telegram.
- `agent/characters/*/service.default.yaml` (semua karakter): tambah field yang sama.
- Catatan: `service.yaml` runtime bersifat gitignored; karena loader cek keberadaan
key, karakter tanpa key baru akan fallback ke default `false` (backward compatible).
## Backward compatibility
- Tidak ada service.yaml existing yang wajib diubah.
- Key hilang => default `false` => behavior identik dengan sekarang.
## Validation plan
- `python -c "import config; print(config.XMPP_MUTE_SESSION_END, config.TELEGRAM_MUTE_SESSION_END)"`
untuk default.
- Load karakter dengan `mute_session_end: true` (via service.yaml test / char override)
dan verifikasi konstanta menjadi True.
- Kode review manual kedua `_timeout_session`: saat True, tidak ada `send`/`schedule_send`,
sesi tetap reset.
## Resolved
Scope final: hanya `_timeout_session`. Natural-close `end_session` tidak disentuh
(sudah dikonfirmasi user). Timeout group/MUC dinaikkan ke 30 menit.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -1,38 +0,0 @@
@startmindmap
<style>
mindmapDiagram {
//FontColor white
.done {
BackgroundColor green
BackgroundColor lightgreen
//FontColor black
}
.unknown {
BackgroundColor #FFBBCC
BackgroundColor purple
FontColor white
}
.skip {
BackgroundColor #FFBBCC
BackgroundColor red
FontColor white
}
.info {
BackgroundColor blue
BackgroundColor lightblue
}
}
</style>
;top to bottom direction
* TUI/GUI/API Settings
** TUI
*** Parameters for tokenizer
*** Service list
****_ Kontak dengan daemon
*** Model set & Provider list <<done>>
*** Roleplayer user ID
@endmindmap

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -1,36 +0,0 @@
@startmindmap
<style>
mindmapDiagram {
//FontColor white
.done {
BackgroundColor green
BackgroundColor lightgreen
//FontColor black
}
.unknown {
BackgroundColor #FFBBCC
BackgroundColor purple
FontColor white
}
.skip {
BackgroundColor #FFBBCC
BackgroundColor red
FontColor white
}
.info {
BackgroundColor blue
BackgroundColor lightblue
}
}
</style>
;top to bottom direction
* Configuration per character <<done>>
** service.yaml <<done>>
***_ Untuk kredensial service yang di gunakan
** model.yaml <<done>>
***_ Untuk menetapkan model set default yang digunakan
@endmindmap

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -59,9 +59,5 @@ mindmapDiagram {
*** World
**** Building
***** Room
** Mute Session End (Per Character) <<done>>
***_ Tidak Menampilkan "*Session End*" saat session timeout
** Continuous Greet after session end (Per Character)
***_ Untuk reset history di chat
@endmindmap
@endmindmap

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,42 +0,0 @@
@startmindmap
<style>
mindmapDiagram {
//FontColor white
.done {
BackgroundColor green
BackgroundColor lightgreen
//FontColor black
}
.unknown {
BackgroundColor #FFBBCC
BackgroundColor purple
FontColor white
}
.skip {
BackgroundColor #FFBBCC
BackgroundColor red
FontColor white
}
.info {
BackgroundColor blue
BackgroundColor lightblue
}
}
</style>
;top to bottom direction
* Tools for configuration
** Workspace Switch <<done>>
** Service toggler
** Listing model set and provider
** Model set switch <<skip>>
***_ Sepertinya mustahil dan potensi ngerusak sistem
* Tools for generation
** plantuml generator
** Slide deck
** Motion graphic
@endmindmap

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -0,0 +1,76 @@
@startmindmap
<style>
mindmapDiagram {
//FontColor white
.done {
BackgroundColor green
BackgroundColor lightgreen
//FontColor black
}
.unknown {
BackgroundColor #FFBBCC
BackgroundColor purple
FontColor white
}
.skip {
BackgroundColor #FFBBCC
BackgroundColor red
FontColor white
}
.info {
BackgroundColor blue
BackgroundColor lightblue
}
}
</style>
;top to bottom direction
* Tools for configuration
** Workspace Switch <<done>>
** Service toggler
** Listing model set and provider
** Model set switch <<skip>>
***_ Sepertinya mustahil dan potensi ngerusak sistem
* Tools for generation
** plantuml generator
** Slide deck
** Motion graphic
* TUI/GUI/API Settings
** TUI
*** Parameters for tokenizer
*** Service list
****_ Kontak dengan daemon
*** Model set & Provider list <<done>>
*** Roleplayer user ID
* Configuration per character <<done>>
** service.yaml <<done>>
***_ Untuk kredensial service yang di gunakan
** model.yaml <<done>>
***_ Untuk menetapkan model set default yang digunakan
* opencode main skill
** Explore (Passive)
** Plan
** Build
* Verbosity
** Concise
***_ Keep your answers short and to the point.
** Balanced
***_ Provide balanced answers. Not too brief, not too long.
** Detailed
***_ Give thorough, detailed answers with explanations.
* Full-Duplex Model
**_ Voice Activity Detection
**_ Noise interruption detector included
** Mic & Voice Frontend
** Models
*** GPT-Live-1
@endmindmap

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View File

@ -27,43 +27,6 @@ mindmapDiagram {
;top to bottom direction
* Decision/Classification Features
** API Inference
*** Library
****_ Dependency
*****_ .venv/bin/pip install huggingface_hub transformers
****_ Download
*****_ .venv/bin/hf download convaiinnovations/laya --local-dir models/convaiinnovations/laya
*****_ .venv/bin/hf download convaiinnovations/laya-multilingual --local-dir models/convaiinnovations/laya-multilingual
*** Model
**** Classification
***** convaiinnovations/laya
***** convaiinnovations/laya-multilingual
*** Inference
**** llama.cpp
**** laya.cpp (lkarlslund)
*****_ HTTP Server bawaan
* opencode main skill
** Explore (Passive)
** Plan
** Build
* Verbosity
** Concise
***_ Keep your answers short and to the point.
** Balanced
***_ Provide balanced answers. Not too brief, not too long.
** Detailed
***_ Give thorough, detailed answers with explanations.
* Full-Duplex Model
**_ Voice Activity Detection
**_ Noise interruption detector included
** Mic & Voice Frontend
** Models
*** GPT-Live-1
* Speech
** Push to talk
*** STT (ASR)

View File

@ -4,7 +4,18 @@ import re
import fnmatch
import glob as glob_module
from lib import workspace
# Global state to track the agent's current working directory
# This ensures that subsequent bash commands run in the correct directory
CURRENT_WORKSPACE = os.getcwd()
def get_current_workspace():
global CURRENT_WORKSPACE
return CURRENT_WORKSPACE
def set_current_workspace(path):
global CURRENT_WORKSPACE
CURRENT_WORKSPACE = path
os.chdir(path)
schema_read_file = {
"type": "function",
@ -23,8 +34,8 @@ schema_read_file = {
def read_file(path):
try:
# Use absolute path if it's relative to current workspace
full_path = os.path.join(workspace.get_current_workspace(), path) if not os.path.isabs(path) else path
# Use absolute path if it's relative to CURRENT_WORKSPACE
full_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path
with open(full_path, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
return ''.join(f"{i+1}: {line}" for i, line in enumerate(lines))
@ -49,7 +60,7 @@ schema_write_file = {
def write_file(path, content):
try:
full_path = os.path.join(workspace.get_current_workspace(), path) if not os.path.isabs(path) else path
full_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path
with open(full_path, 'w', encoding='utf-8') as f:
f.write(content)
return f"Successfully wrote to {full_path}"
@ -75,7 +86,7 @@ schema_edit_file = {
def edit_file(path, old_string, new_string):
try:
full_path = os.path.join(workspace.get_current_workspace(), path) if not os.path.isabs(path) else path
full_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path
with open(full_path, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
if old_string not in content:
@ -105,7 +116,7 @@ schema_run_bash = {
def run_bash(command):
try:
# Prepend 'cd' to the command to ensure it runs in the correct workspace
full_command = f"cd '{workspace.get_current_workspace()}' && {command}"
full_command = f"cd '{get_current_workspace()}' && {command}"
result = subprocess.run(full_command, shell=True, capture_output=True, text=True, timeout=30)
return f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}\nreturn code: {result.returncode}"
except Exception as e:
@ -131,7 +142,7 @@ schema_search_code = {
def search_code(pattern, search_type, path="."):
try:
# Resolve path relative to workspace
search_path = os.path.join(workspace.get_current_workspace(), path) if not os.path.isabs(path) else path
search_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path
if search_type == "glob":
files = glob_module.glob(f"{search_path}/**/{pattern}", recursive=True)
@ -176,11 +187,41 @@ def git_operation(args):
try:
# Execute git command from the current workspace
full_command = ["git", *args]
result = subprocess.run(full_command, cwd=workspace.get_current_workspace(), capture_output=True, text=True, timeout=10)
result = subprocess.run(full_command, cwd=get_current_workspace(), capture_output=True, text=True, timeout=10)
return f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}\nreturn code: {result.returncode}"
except Exception as e:
return f"Error running git command: {str(e)}"
schema_workspace_change = {
"type": "function",
"function": {
"name": "workspace_change",
"description": "Change the current working directory of the agent session. Supports tilde (~) expansion for the user home directory (/home/aji/).",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "The target directory path to change to."}
},
"required": ["path"]
}
}
}
def workspace_change(path):
try:
if path.startswith("~"):
target_path = os.path.expanduser(path)
else:
target_path = os.path.abspath(path)
if os.path.exists(target_path) and os.path.isdir(target_path):
set_current_workspace(target_path)
return f"Successfully changed workspace to: {target_path}"
else:
return f"Error: The path {target_path} does not exist or is not a directory."
except Exception as e:
return f"Error changing workspace: {str(e)}"
schema_search_grep = {
"type": "function",
"function": {
@ -200,7 +241,7 @@ schema_search_grep = {
def search_grep(pattern, path=".", include=None):
try:
search_path = os.path.join(workspace.get_current_workspace(), path) if not os.path.isabs(path) else path
search_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path
results = []
for root, dirs, files in os.walk(search_path):
for file in files:
@ -236,7 +277,7 @@ schema_search_glob = {
def search_glob(pattern, path="."):
try:
search_path = os.path.join(workspace.get_current_workspace(), path) if not os.path.isabs(path) else path
search_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path
files = glob_module.glob(f"{search_path}/{pattern}", recursive=True)
def _isfile(f):
try:

View File

@ -1,49 +0,0 @@
import os
from lib import workspace
# Tools untuk membantu sistem Hendrik.
# State workspace sesi dimiliki lib.workspace dan dipakai bersama oleh
# tool coding, vision, dan TUI -- di sini hanya dibungkus sebagai tool aksi.
schema_workspace_change = {
"type": "function",
"function": {
"name": "workspace_change",
"description": "Change the current working directory of the agent session. Supports tilde (~) expansion for the user home directory (/home/aji/).",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "The target directory path to change to."}
},
"required": ["path"]
}
}
}
def workspace_change(path):
previous_path = workspace.get_current_workspace()
try:
if path.startswith("~"):
target_path = os.path.expanduser(path)
else:
target_path = os.path.abspath(path)
if os.path.exists(target_path) and os.path.isdir(target_path):
workspace.set_current_workspace(target_path)
return (
f"Successfully changed workspace.\n"
f"previous workspace: {previous_path}\n"
f"current workspace : {target_path}"
)
else:
return (
f"Error: The path {target_path} does not exist or is not a directory.\n"
f"previous workspace: {previous_path}\n"
f"current workspace : {previous_path} (unchanged)"
)
except Exception as e:
return (
f"Error changing workspace: {str(e)}\n"
f"previous workspace: {previous_path}\n"
f"current workspace : {workspace.get_current_workspace()}"
)

View File

@ -6,7 +6,7 @@ import tempfile
import urllib.request
import ftplib
from urllib.parse import urlparse
from lib import workspace
from tools.coder import get_current_workspace
MIME_MAP = {
".jpg": "image/jpeg",
@ -152,7 +152,7 @@ def describe_image(path):
return f"Error fetching SFTP: {e}"
# ── Local file ─────────────────────────────────────────────────────
full_path = os.path.join(workspace.get_current_workspace(), path) if not os.path.isabs(path) else path
full_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path
if not os.path.isfile(full_path):
return f"Error: file not found: {full_path}"