gemcode 0.3.95__tar.gz → 0.3.97__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gemcode-0.3.95/src/gemcode.egg-info → gemcode-0.3.97}/PKG-INFO +1 -1
- {gemcode-0.3.95 → gemcode-0.3.97}/pyproject.toml +1 -1
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/agent.py +2 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/invoke.py +4 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/repl_commands.py +3 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/repl_slash.py +47 -0
- gemcode-0.3.97/src/gemcode/session_summariser.py +227 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/__init__.py +26 -0
- {gemcode-0.3.95 → gemcode-0.3.97/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode.egg-info/SOURCES.txt +1 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/LICENSE +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/MANIFEST.in +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/README.md +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/setup.cfg +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/autotune.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/checkpoints.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/cli.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/config.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/curated_memory.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/evals/harness.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/hooks.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/ide_protocol.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/ide_stdio.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/kaira_daemon.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/learning.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/multimodal_input.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/output_styles.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/policy_profile.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/pricing.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/query_sanitizer.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/refine.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/rules.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/session_store.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/skills.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tool_result_store.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/compress_memory.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/curated_memory.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/skills.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/veomem_tools.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tui/scrollback.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/veomem_bridge.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/version.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/wal.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/web/sse_adapter.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/web/web_sse_compat.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_add_dir.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_checkpoint_diff_command.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_compress_memory_tool.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_credentials.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_eval_harness_layout.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_ide_stdio_attachments.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_kaira_scheduler.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_multimodal_input.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_output_styles_and_rules.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_paths.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_permissions.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_skills.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_slash_completion_registry.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_tools.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_web_sse_adapter.py +0 -0
- {gemcode-0.3.95 → gemcode-0.3.97}/tests/test_workspace_hints.py +0 -0
|
@@ -934,6 +934,8 @@ You have two tools to persist project insights across sessions (auto-memory styl
|
|
|
934
934
|
Notes are loaded at session start so future sessions inherit this knowledge.
|
|
935
935
|
|
|
936
936
|
- **`read_project_notes()`** — read current notes **only when starting a real engineering task** (editing, debugging, building). Do NOT call this for greetings or general questions. If notes exist and you're about to work on a task, read them once to avoid re-discovering known information.
|
|
937
|
+
|
|
938
|
+
- **`summarise_session(focus="")`** — use this when the active session has become large or noisy and you want to preserve the important work before continuing. It writes a compact session summary, extracts durable facts into memory, and updates notes so a fresh follow-up session can stay lightweight.
|
|
937
939
|
"""
|
|
938
940
|
|
|
939
941
|
# Inject capability-specific strategy sections only when those caps are on.
|
|
@@ -73,6 +73,10 @@ async def run_turn(
|
|
|
73
73
|
# Dynamic risk score: updated each user message; later refined by tool outcomes.
|
|
74
74
|
# This is intentionally heuristic but configurable via env knobs.
|
|
75
75
|
if cfg is not None:
|
|
76
|
+
try:
|
|
77
|
+
object.__setattr__(cfg, "_active_session_id", session_id)
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
76
80
|
try:
|
|
77
81
|
import re
|
|
78
82
|
p = (prompt or "")[:20_000]
|
|
@@ -254,6 +254,7 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
|
|
|
254
254
|
("skills", "List GemSkills"),
|
|
255
255
|
("status", "Model, capabilities, thinking, limits"),
|
|
256
256
|
("style", "Output styles · /style <name>|off"),
|
|
257
|
+
("summarise", "Summarise current session, persist key points, then reset · /summarize same"),
|
|
257
258
|
("thinking", "Thinking verbose/brief/off, budget, level"),
|
|
258
259
|
("tools", "Tool inventory · /tools smoke"),
|
|
259
260
|
("trust", "Workspace trust · /trust on|off"),
|
|
@@ -364,6 +365,8 @@ def slash_help_lines() -> list[str]:
|
|
|
364
365
|
" /clear Alias for /session new",
|
|
365
366
|
" /compact Force context compaction now (summarize history)",
|
|
366
367
|
" /compact <focus> Compact with custom focus, e.g. /compact test output",
|
|
368
|
+
" /summarise [focus] Save a durable session summary, persist key facts, then start fresh",
|
|
369
|
+
" /summarize [focus] Alias of /summarise",
|
|
367
370
|
" /review Parallel code review: security + style + correctness",
|
|
368
371
|
" /review <path> Review a specific file or directory",
|
|
369
372
|
" /context Show context pressure + last prompt tokens",
|
|
@@ -36,6 +36,7 @@ from gemcode.slash_commands import parse_slash_command
|
|
|
36
36
|
from gemcode.skills import discover_skill_metas, expand_skill_text, list_supporting_files, load_skill
|
|
37
37
|
from gemcode.output_styles import discover_output_styles, load_output_style
|
|
38
38
|
from gemcode.rules import load_rules as _load_rules
|
|
39
|
+
from gemcode.session_summariser import summarise_session
|
|
39
40
|
from gemcode.trust import is_trusted_root, trust_json_path, trust_root
|
|
40
41
|
|
|
41
42
|
|
|
@@ -1556,6 +1557,52 @@ async def process_repl_slash(
|
|
|
1556
1557
|
model_prompt=compact_prompt,
|
|
1557
1558
|
)
|
|
1558
1559
|
|
|
1560
|
+
if name in ("summarise", "summarize"):
|
|
1561
|
+
focus = (sc.args or "").strip()
|
|
1562
|
+
out("Summarising current session into durable memory…")
|
|
1563
|
+
if focus:
|
|
1564
|
+
out(f"Focus: {focus}")
|
|
1565
|
+
out()
|
|
1566
|
+
try:
|
|
1567
|
+
model = (
|
|
1568
|
+
getattr(cfg, "adk_compaction_summarizer_model", None)
|
|
1569
|
+
or getattr(cfg, "model", "")
|
|
1570
|
+
or "gemini-2.5-flash"
|
|
1571
|
+
)
|
|
1572
|
+
result = summarise_session(
|
|
1573
|
+
cfg.project_root,
|
|
1574
|
+
session_id=session_id,
|
|
1575
|
+
model=model,
|
|
1576
|
+
focus=focus,
|
|
1577
|
+
)
|
|
1578
|
+
except Exception as e:
|
|
1579
|
+
out(f"[gemcode] session summarise failed: {e}")
|
|
1580
|
+
out()
|
|
1581
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
1582
|
+
|
|
1583
|
+
if result.get("error"):
|
|
1584
|
+
out(f"[gemcode] {result['error']}")
|
|
1585
|
+
out()
|
|
1586
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
1587
|
+
|
|
1588
|
+
out(f"Saved summary: {result.get('summary_path')}")
|
|
1589
|
+
mem_saved = len(result.get("memory_facts_saved") or [])
|
|
1590
|
+
user_saved = len(result.get("user_facts_saved") or [])
|
|
1591
|
+
open_items = len(result.get("open_items") or [])
|
|
1592
|
+
out(f"Curated memory saved: project={mem_saved}, user={user_saved}, open_items={open_items}")
|
|
1593
|
+
if result.get("notes_status"):
|
|
1594
|
+
out(f"Notes: {result.get('notes_status')}")
|
|
1595
|
+
out("Starting a fresh session so the next turn stays lightweight.")
|
|
1596
|
+
out()
|
|
1597
|
+
_clear_session_loaded_skills(cfg)
|
|
1598
|
+
cfg.pending_attachment_paths.clear()
|
|
1599
|
+
new_id = str(uuid.uuid4())
|
|
1600
|
+
return ReplSlashResult(
|
|
1601
|
+
skip_model_turn=True,
|
|
1602
|
+
new_session_id=new_id,
|
|
1603
|
+
force_rebuild_runner=True,
|
|
1604
|
+
)
|
|
1605
|
+
|
|
1559
1606
|
if name in ("exit", "quit"):
|
|
1560
1607
|
return ReplSlashResult(exit_repl=True)
|
|
1561
1608
|
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sqlite3
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from google.genai import Client
|
|
11
|
+
from google.genai import types
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _session_summary_dir(project_root: Path) -> Path:
|
|
15
|
+
return project_root / ".gemcode" / "session-summaries"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _session_summary_path(project_root: Path, session_id: str) -> Path:
|
|
19
|
+
safe_id = (session_id or "unknown").strip().replace("/", "_")
|
|
20
|
+
return _session_summary_dir(project_root) / f"{safe_id}.md"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_session_transcript(project_root: Path, session_id: str, *, max_events: int = 120) -> list[str]:
|
|
24
|
+
db = project_root / ".gemcode" / "sessions.sqlite"
|
|
25
|
+
if not db.is_file():
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
con = sqlite3.connect(str(db), timeout=5)
|
|
29
|
+
cur = con.cursor()
|
|
30
|
+
cur.execute(
|
|
31
|
+
"""
|
|
32
|
+
SELECT event_data
|
|
33
|
+
FROM events
|
|
34
|
+
WHERE session_id=?
|
|
35
|
+
ORDER BY timestamp ASC
|
|
36
|
+
LIMIT ?
|
|
37
|
+
""",
|
|
38
|
+
(session_id, int(max_events)),
|
|
39
|
+
)
|
|
40
|
+
rows = cur.fetchall()
|
|
41
|
+
con.close()
|
|
42
|
+
|
|
43
|
+
lines: list[str] = []
|
|
44
|
+
for (raw,) in rows:
|
|
45
|
+
try:
|
|
46
|
+
event = json.loads(raw)
|
|
47
|
+
except Exception:
|
|
48
|
+
continue
|
|
49
|
+
if not isinstance(event, dict):
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
author = str(event.get("author") or "").strip().lower()
|
|
53
|
+
content = event.get("content") if isinstance(event.get("content"), dict) else {}
|
|
54
|
+
parts = content.get("parts") if isinstance(content.get("parts"), list) else []
|
|
55
|
+
texts: list[str] = []
|
|
56
|
+
for p in parts:
|
|
57
|
+
if isinstance(p, dict):
|
|
58
|
+
t = p.get("text")
|
|
59
|
+
if isinstance(t, str) and t.strip():
|
|
60
|
+
texts.append(t.strip())
|
|
61
|
+
if not texts:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
joined = "\n".join(texts)
|
|
65
|
+
if len(joined) > 4000:
|
|
66
|
+
joined = joined[:4000].rstrip() + "\n… [truncated]"
|
|
67
|
+
|
|
68
|
+
who = "User" if author == "user" else "GemCode"
|
|
69
|
+
lines.append(f"{who}: {joined}")
|
|
70
|
+
return lines
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _build_prompt(transcript_lines: list[str], *, focus: str = "") -> str:
|
|
74
|
+
transcript = "\n\n".join(transcript_lines)
|
|
75
|
+
if len(transcript) > 120_000:
|
|
76
|
+
transcript = transcript[:120_000] + "\n\n… [older transcript truncated]"
|
|
77
|
+
|
|
78
|
+
focus_line = f"- Extra focus: {focus}\n" if focus.strip() else ""
|
|
79
|
+
return (
|
|
80
|
+
"You are a session summariser for GemCode.\n"
|
|
81
|
+
"Summarise the session into compact, reusable memory for future runs.\n"
|
|
82
|
+
"Return STRICT JSON only with this schema:\n"
|
|
83
|
+
"{\n"
|
|
84
|
+
' "title": "short title",\n'
|
|
85
|
+
' "summary_markdown": "markdown summary",\n'
|
|
86
|
+
' "memory_facts": ["durable project facts"],\n'
|
|
87
|
+
' "user_facts": ["durable user preferences"],\n'
|
|
88
|
+
' "notes_markdown": "compact markdown note for .gemcode/notes.md",\n'
|
|
89
|
+
' "open_items": ["open tasks or blockers"]\n'
|
|
90
|
+
"}\n"
|
|
91
|
+
"Rules:\n"
|
|
92
|
+
"- Keep summary_markdown concise but high-signal.\n"
|
|
93
|
+
"- Preserve decisions, file paths, commands, errors, fixes, and next steps.\n"
|
|
94
|
+
"- memory_facts/user_facts: 0 to 5 each, only durable non-sensitive facts.\n"
|
|
95
|
+
"- notes_markdown should be compact and useful for the next session.\n"
|
|
96
|
+
"- Never include secrets, API keys, passwords, or tokens.\n"
|
|
97
|
+
f"{focus_line}"
|
|
98
|
+
"\nTranscript:\n"
|
|
99
|
+
f"{transcript}\n"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _call_summary_model(*, model: str, prompt: str) -> dict[str, Any]:
|
|
104
|
+
api_key = os.environ.get("GOOGLE_API_KEY")
|
|
105
|
+
if not api_key:
|
|
106
|
+
raise RuntimeError("GOOGLE_API_KEY not set")
|
|
107
|
+
|
|
108
|
+
client = Client(api_key=api_key)
|
|
109
|
+
resp = client.models.generate_content(
|
|
110
|
+
model=model,
|
|
111
|
+
contents=[types.Content(role="user", parts=[types.Part(text=prompt)])],
|
|
112
|
+
config=types.GenerateContentConfig(temperature=0.2),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
out_parts: list[str] = []
|
|
116
|
+
try:
|
|
117
|
+
if resp.candidates:
|
|
118
|
+
c0 = resp.candidates[0]
|
|
119
|
+
content = getattr(c0, "content", None)
|
|
120
|
+
for p in getattr(content, "parts", None) or []:
|
|
121
|
+
t = getattr(p, "text", None)
|
|
122
|
+
if isinstance(t, str) and t:
|
|
123
|
+
out_parts.append(t)
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
text = "".join(out_parts).strip()
|
|
128
|
+
if not text:
|
|
129
|
+
raise RuntimeError("session summariser returned empty text")
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
data = json.loads(text)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
raise RuntimeError(f"session summariser returned invalid JSON: {e}") from e
|
|
135
|
+
if not isinstance(data, dict):
|
|
136
|
+
raise RuntimeError("session summariser returned non-object JSON")
|
|
137
|
+
return data
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def summarise_session(
|
|
141
|
+
project_root: Path,
|
|
142
|
+
*,
|
|
143
|
+
session_id: str,
|
|
144
|
+
model: str,
|
|
145
|
+
focus: str = "",
|
|
146
|
+
) -> dict[str, Any]:
|
|
147
|
+
transcript_lines = _load_session_transcript(project_root, session_id)
|
|
148
|
+
if not transcript_lines:
|
|
149
|
+
return {"error": "session transcript is empty", "session_id": session_id}
|
|
150
|
+
|
|
151
|
+
prompt = _build_prompt(transcript_lines, focus=focus)
|
|
152
|
+
data = _call_summary_model(model=model, prompt=prompt)
|
|
153
|
+
|
|
154
|
+
title = str(data.get("title") or f"Session {session_id[:8]}").strip()[:120]
|
|
155
|
+
summary_markdown = str(data.get("summary_markdown") or "").strip()
|
|
156
|
+
notes_markdown = str(data.get("notes_markdown") or "").strip()
|
|
157
|
+
memory_facts = [str(x).strip() for x in (data.get("memory_facts") or []) if str(x).strip()][:5]
|
|
158
|
+
user_facts = [str(x).strip() for x in (data.get("user_facts") or []) if str(x).strip()][:5]
|
|
159
|
+
open_items = [str(x).strip() for x in (data.get("open_items") or []) if str(x).strip()][:10]
|
|
160
|
+
|
|
161
|
+
out_path = _session_summary_path(project_root, session_id)
|
|
162
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
164
|
+
|
|
165
|
+
body_parts = [
|
|
166
|
+
f"# {title}",
|
|
167
|
+
"",
|
|
168
|
+
f"- session_id: `{session_id}`",
|
|
169
|
+
f"- generated_at: {ts}",
|
|
170
|
+
]
|
|
171
|
+
if focus.strip():
|
|
172
|
+
body_parts.append(f"- focus: {focus}")
|
|
173
|
+
body_parts.extend(["", "## Summary", summary_markdown or "- (empty)"])
|
|
174
|
+
|
|
175
|
+
if open_items:
|
|
176
|
+
body_parts.extend(["", "## Open items", *[f"- {x}" for x in open_items]])
|
|
177
|
+
if memory_facts:
|
|
178
|
+
body_parts.extend(["", "## Durable project facts", *[f"- {x}" for x in memory_facts]])
|
|
179
|
+
if user_facts:
|
|
180
|
+
body_parts.extend(["", "## Durable user facts", *[f"- {x}" for x in user_facts]])
|
|
181
|
+
|
|
182
|
+
out_path.write_text("\n".join(body_parts).rstrip() + "\n", encoding="utf-8")
|
|
183
|
+
|
|
184
|
+
saved_memory: list[str] = []
|
|
185
|
+
saved_user: list[str] = []
|
|
186
|
+
try:
|
|
187
|
+
from gemcode.curated_memory import append_fact
|
|
188
|
+
for fact in memory_facts:
|
|
189
|
+
res = append_fact(project_root, target="memory", text=fact)
|
|
190
|
+
if "error" not in res:
|
|
191
|
+
saved_memory.append(fact)
|
|
192
|
+
for fact in user_facts:
|
|
193
|
+
res = append_fact(project_root, target="user", text=fact)
|
|
194
|
+
if "error" not in res:
|
|
195
|
+
saved_user.append(fact)
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
note_status: str | None = None
|
|
200
|
+
if notes_markdown:
|
|
201
|
+
try:
|
|
202
|
+
from gemcode.tools.notes import build_notes_tools
|
|
203
|
+
append_note, _read_note = build_notes_tools(project_root)
|
|
204
|
+
note_text = (
|
|
205
|
+
f"## Session summary — {title}\n"
|
|
206
|
+
f"- Session: `{session_id}`\n"
|
|
207
|
+
f"- Summary file: `{out_path}`\n\n"
|
|
208
|
+
f"{notes_markdown}"
|
|
209
|
+
)
|
|
210
|
+
res = append_note(note_text)
|
|
211
|
+
if isinstance(res, dict):
|
|
212
|
+
note_status = str(res.get("status") or "")
|
|
213
|
+
except Exception:
|
|
214
|
+
note_status = None
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
"ok": True,
|
|
218
|
+
"session_id": session_id,
|
|
219
|
+
"summary_path": str(out_path),
|
|
220
|
+
"title": title,
|
|
221
|
+
"summary_markdown": summary_markdown,
|
|
222
|
+
"memory_facts_saved": saved_memory,
|
|
223
|
+
"user_facts_saved": saved_user,
|
|
224
|
+
"notes_status": note_status,
|
|
225
|
+
"open_items": open_items,
|
|
226
|
+
}
|
|
227
|
+
|
|
@@ -21,6 +21,7 @@ from gemcode.tools.curated_memory import make_curated_memory_tools
|
|
|
21
21
|
from gemcode.tools.compress_memory import make_compress_memory_tool
|
|
22
22
|
from gemcode.tools.skills import make_skill_tools
|
|
23
23
|
from gemcode.tools.veomem_tools import make_veomem_tools
|
|
24
|
+
from gemcode.session_summariser import summarise_session
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
def _get_load_memory_tool():
|
|
@@ -92,6 +93,30 @@ def build_function_tools(cfg: GemCodeConfig, *, include_subtask: bool = True) ->
|
|
|
92
93
|
compress_memory_file = make_compress_memory_tool(cfg)
|
|
93
94
|
list_skills, load_skill, skills_manifest = make_skill_tools(cfg)
|
|
94
95
|
|
|
96
|
+
def summarise_session_tool(focus: str = "") -> dict:
|
|
97
|
+
"""
|
|
98
|
+
Summarise the current session into compact reusable memory.
|
|
99
|
+
|
|
100
|
+
Use this when the working session has grown large and you want GemCode to
|
|
101
|
+
extract key points into durable notes + curated memory before continuing.
|
|
102
|
+
"""
|
|
103
|
+
session_id = str(getattr(cfg, "_active_session_id", "") or "").strip()
|
|
104
|
+
if not session_id:
|
|
105
|
+
return {"error": "no active session id is available"}
|
|
106
|
+
model = (
|
|
107
|
+
getattr(cfg, "adk_compaction_summarizer_model", None)
|
|
108
|
+
or getattr(cfg, "model", "")
|
|
109
|
+
or "gemini-2.5-flash"
|
|
110
|
+
)
|
|
111
|
+
return summarise_session(
|
|
112
|
+
cfg.project_root,
|
|
113
|
+
session_id=session_id,
|
|
114
|
+
model=model,
|
|
115
|
+
focus=focus,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
summarise_session_tool.__name__ = "summarise_session"
|
|
119
|
+
|
|
95
120
|
def checkpoints_list(limit: int = 20) -> dict:
|
|
96
121
|
"""List recent checkpoints created by mutating tools."""
|
|
97
122
|
return {"checkpoints": _list_checkpoints(cfg.project_root, limit=limit)}
|
|
@@ -154,6 +179,7 @@ def build_function_tools(cfg: GemCodeConfig, *, include_subtask: bool = True) ->
|
|
|
154
179
|
read_curated_memory,
|
|
155
180
|
# Optional: compress memory files (markdown only; safe guards apply)
|
|
156
181
|
compress_memory_file,
|
|
182
|
+
summarise_session_tool,
|
|
157
183
|
# Optional: VeoMem recall tools (3-step search/timeline/fetch).
|
|
158
184
|
# Enabled via GEMCODE_VEOMEM=1.
|
|
159
185
|
# GemSkills (on-demand playbooks)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|