Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@

### News

- **2026-08-12**: v0.0.9 released — TUI thinking display (LLM reasoning surfaced in-place with spinner preview + final panel), approval bypass mode (`approval_bypass` config + session-wide "Allow ALL"), workspace boundary softened to approval-gated, long-task convergence hardening (false-progress fix, repeated-read gate, periodic checkpoint forcing, pre-compression knowledge extraction), cross-session task history (automatic session summaries + proactive history injection), dynamic tool registry rebuild for late-registered tools, terminal sessions enabled by default.
- **2026-08-06**: v0.0.8 released — Cross-platform Windows support (DaemonTransport protocol with TCP loopback IPC), real journey test layer with cassette-backed CI (6 e2e journeys, cost-bounded), community Windows fixes (@fanqiNO1). 1,540 tests.
- **2026-08-06**: v0.0.7 released — 1M-class context windows end-to-end, self-calibrating token estimator, internal-defect failure category, concurrent-TUI session identity isolation. 1,442 tests.

<details>
<summary>Previous releases</summary>

- **2026-08-06**: v0.0.7 released — 1M-class context windows end-to-end, self-calibrating token estimator, internal-defect failure category, concurrent-TUI session identity isolation. 1,442 tests.

- **2026-07-31**: v0.0.6 released — side-effect-gated recovery (checkpointed halts with structured `InteractionRequest`), uncertain-effect reporting for failed outbound calls, centralized logging with an independent daemon log level, session-bound LeapBoard analysis, platform-neutral gateway validators, and end-to-end architecture contract tests with the CI gate restored.
- **2026-07-28**: v0.0.5 released — adaptive-depth execution for long-horizon tasks, built-in coding tools, per-session daemon concurrency, improved TUI stability, and hardened leapd recovery/status diagnostics.
- **2026-07-16**: **LeapBoard** — general-purpose monitoring web dashboard (Watch→Finding + Server-Driven UI); `/board` entry, live session analysis, and finance/sentiment/research templates.
Expand Down
8 changes: 8 additions & 0 deletions src/leapflow/cli/commands/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,10 @@ async def _stream_response(prompt_text: str) -> None:
renderer.feed(event.content)
elif event.type == "thinking":
renderer.feed_thinking(event.content)
# Phase 2: route thinking to spinner for in-place display
_preview = (event.content or "").strip().replace("\n", " ")[:80]
if _preview:
app.spinner_text = f"💭 {_preview}"
elif event.type == "tool_start":
app.spinner_text = renderer.tool_started(
event.content,
Expand Down Expand Up @@ -1213,6 +1217,10 @@ async def _stream_response(
renderer.feed(event.content)
elif event.type == "thinking":
renderer.feed_thinking(event.content)
# Phase 2: route thinking to spinner for in-place display
_preview = (event.content or "").strip().replace("\n", " ")[:80]
if _preview:
app.spinner_text = f"💭 {_preview}"
elif event.type == "tool_start":
app.spinner_text = renderer.tool_started(
event.content,
Expand Down
52 changes: 51 additions & 1 deletion src/leapflow/cli/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -1806,6 +1806,56 @@ async def _session_search_handler(params: dict) -> dict:
TOOL_HANDLERS["session_search"] = _session_search_handler
TOOL_HANDLERS["gp_session_search"] = _session_search_handler
logger.debug("session_search tool registered")

# ── Register session_list tool ──
def _format_ts(ts: float) -> str:
if not ts:
return ""
import datetime
try:
return datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M")
except (TypeError, ValueError, OSError):
return str(ts)[:16]

TOOL_DEFINITIONS.append({
"type": "function",
"function": {
"name": "session_list",
"description": (
"List recent conversation sessions with titles, dates, and summaries. "
"Use for browsing past tasks or when user asks to see history without specific search terms. "
"No keywords needed — returns chronological list."
),
"parameters": {
"type": "object",
"properties": {
"limit": {"type": "integer", "description": "Max sessions to return (default: 10, max: 30)"},
},
"required": [],
},
},
})

async def _session_list_handler(params: dict) -> dict:
limit = min(int(params.get("limit", 10)), 30)
sessions = conv_store.list_sessions(limit=limit, active_only=False)
items = []
for s in sessions:
item = {
"title": getattr(s, 'title', '') or getattr(s, 'session_id', '')[:8],
"date": _format_ts(getattr(s, 'updated_at', 0) or getattr(s, 'created_at', 0)),
"messages": getattr(s, 'message_count', 0),
}
summary = getattr(s, 'summary', '')
if summary:
item["summary"] = summary[:200]
items.append(item)
import json as _json_sl
return {"ok": True, "result": _json_sl.dumps(items, ensure_ascii=False)}

TOOL_HANDLERS["session_list"] = _session_list_handler
TOOL_HANDLERS["gp_session_list"] = _session_list_handler
logger.debug("session_list tool registered")
except Exception:
logger.debug("session_search tool registration failed", exc_info=True)

Expand Down Expand Up @@ -2588,7 +2638,7 @@ async def _persist_session_summary(self) -> None:
conv_store = getattr(self, '_conversation_store', None)
if conv_store and session_id and goal_line:
try:
conv_store.end_session(session_id, title=goal_line)
conv_store.end_session(session_id, title=goal_line, summary=summary[:500])
except Exception:
logger.debug("session title update failed", exc_info=True)
except Exception:
Expand Down
6 changes: 4 additions & 2 deletions src/leapflow/cli/tui_app/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,8 +430,10 @@ def tool_started(self, name: str, metadata: dict[str, Any] | None = None) -> str
Discards any pending content — it was preamble preceding the tool call
and should not appear in the final answer.
"""
# Flush accumulated thinking before starting new tool display
if self._thinking_buffer.strip():
# Flush thinking ONCE per LLM round: only when this is the first tool
# in a new batch (no other tools currently active). This shows one
# thinking panel per round during execution, not per individual tool.
if not self._active_tools and self._thinking_buffer.strip():
self._console.thinking(self._thinking_buffer)
self._thinking_buffer = ""
self._pending = ""
Expand Down
13 changes: 11 additions & 2 deletions src/leapflow/engine/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -980,8 +980,17 @@ def _extract_json_object(text: str) -> Dict[str, Any]:


def _keywords_from_query(q: str) -> list[str]:
toks = re.findall(r"[\w\-./]+|[\u4e00-\u9fff]+", q)
return [t for t in toks if len(t) >= 2][:12]
tokens: list[str] = []
for segment in re.findall(r'[\u4e00-\u9fff]+|[\w\-./]+', q):
if re.match(r'[\u4e00-\u9fff]', segment):
if len(segment) == 1:
tokens.append(segment)
else:
for i in range(len(segment) - 1):
tokens.append(segment[i:i+2])
elif len(segment) >= 2:
tokens.append(segment)
return tokens[:12]


@dataclass(frozen=True, slots=True)
Expand Down
25 changes: 23 additions & 2 deletions src/leapflow/memory/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import json
import logging
import math
import re as _re
from pathlib import Path
from typing import Any, Dict, List, Optional

Expand All @@ -24,6 +25,26 @@
logger = logging.getLogger(__name__)


# ──────────────────────────────────────────────────────────────────────
# CJK-aware tokenization for search
# ──────────────────────────────────────────────────────────────────────

def _tokenize_for_search(text: str) -> list[str]:
"""Split text into search tokens. CJK runs become overlapping bigrams."""
tokens: list[str] = []
for segment in _re.findall(r'[\u4e00-\u9fff]+|[a-zA-Z0-9_\-./]+', text):
if _re.match(r'[\u4e00-\u9fff]', segment):
# CJK: overlapping bigrams (or single char if length 1)
if len(segment) == 1:
tokens.append(segment)
else:
for i in range(len(segment) - 1):
tokens.append(segment[i:i+2])
else:
tokens.append(segment.lower())
return tokens[:8]


# ──────────────────────────────────────────────────────────────────────
# Shared decay formula (avoids circular import)
# ──────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -296,7 +317,7 @@ async def prefetch(
session_scope: str = "",
) -> List[MemoryEntry]:
"""Quick search for LLM context injection with optional project/task/session scope."""
keywords = query_text.split()[:5]
keywords = _tokenize_for_search(query_text)
scope_terms = [term for term in (scope_keywords or []) if term]
query = MemoryQuery(
keywords=[*keywords, *scope_terms[:5]],
Expand Down Expand Up @@ -628,7 +649,7 @@ async def _handle_memory_search(

domains = [SignalDomain(domain_str)] if domain_str else None
mq = MemoryQuery(
keywords=query_text.split()[:8],
keywords=_tokenize_for_search(query_text),
domains=domains,
limit=limit,
workspace_root=workspace_root,
Expand Down
11 changes: 6 additions & 5 deletions src/leapflow/memory/providers/semantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,13 @@ async def search(self, query: MemoryQuery) -> List[MemoryEntry]:
conditions.append("session_id = ?")
params.append(query.session_scope)

# Keyword filter (AND semantics)
keywords = [k.strip() for k in query.keywords if k.strip()] if query.keywords else []
# Keyword filter (OR semantics — broadens recall for CJK bigrams)
keywords = [k.strip() for k in query.keywords if k.strip()][:8] if query.keywords else []
if keywords:
for _ in keywords:
conditions.append("content ILIKE ?")
params.append(f"%{_}%")
or_clause = " OR ".join(["content ILIKE ?"] * len(keywords))
conditions.append(f"({or_clause})")
for kw in keywords:
params.append(f"%{kw}%")

# Kind filter
if query.kinds:
Expand Down
5 changes: 4 additions & 1 deletion src/leapflow/prompts/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ def build_react_system(language: str = "en", skill_catalog: str = "") -> str:
5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.
6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.
7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.
8. **Recall past work**: When the user references prior tasks or asks about past work, use session_search to recall relevant context from previous sessions.
8. **Recall past work**:
- Broad queries ("之前做了什么", "列出任务"): answer from the "Recent Task History" section already in your context. If insufficient, call session_list.
- Specific lookups ("上次那个配置怎么改的"): call session_search with relevant phrases (NOT single characters).
- Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.

## Coding & Verification
When working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an
Expand Down
Loading
Loading