gemcode 0.3.94__tar.gz → 0.3.96__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.
Files changed (157) hide show
  1. {gemcode-0.3.94/src/gemcode.egg-info → gemcode-0.3.96}/PKG-INFO +1 -1
  2. {gemcode-0.3.94 → gemcode-0.3.96}/pyproject.toml +1 -1
  3. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/agent.py +2 -0
  4. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/invoke.py +4 -0
  5. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/repl_commands.py +3 -0
  6. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/repl_slash.py +47 -0
  7. gemcode-0.3.96/src/gemcode/session_summariser.py +227 -0
  8. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/__init__.py +26 -0
  9. {gemcode-0.3.94 → gemcode-0.3.96/src/gemcode.egg-info}/PKG-INFO +1 -1
  10. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode.egg-info/SOURCES.txt +1 -0
  11. {gemcode-0.3.94 → gemcode-0.3.96}/LICENSE +0 -0
  12. {gemcode-0.3.94 → gemcode-0.3.96}/MANIFEST.in +0 -0
  13. {gemcode-0.3.94 → gemcode-0.3.96}/README.md +0 -0
  14. {gemcode-0.3.94 → gemcode-0.3.96}/setup.cfg +0 -0
  15. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/__init__.py +0 -0
  16. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/__main__.py +0 -0
  17. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/audit.py +0 -0
  18. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/autocompact.py +0 -0
  19. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/autotune.py +0 -0
  20. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/callbacks.py +0 -0
  21. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/capability_routing.py +0 -0
  22. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/checkpoints.py +0 -0
  23. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/cli.py +0 -0
  24. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/compaction.py +0 -0
  25. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/computer_use/__init__.py +0 -0
  26. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/computer_use/browser_computer.py +0 -0
  27. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/config.py +0 -0
  28. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/context_budget.py +0 -0
  29. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/context_warning.py +0 -0
  30. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/credentials.py +0 -0
  31. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/curated_memory.py +0 -0
  32. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/dynamic_policy.py +0 -0
  33. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/evals/harness.py +0 -0
  34. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/hitl_session.py +0 -0
  35. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/hooks.py +0 -0
  36. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/ide_protocol.py +0 -0
  37. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/ide_stdio.py +0 -0
  38. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/intent_classifier.py +0 -0
  39. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/interactions.py +0 -0
  40. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/kaira_daemon.py +0 -0
  41. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/learning.py +0 -0
  42. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/limits.py +0 -0
  43. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/live_audio_engine.py +0 -0
  44. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/logging_config.py +0 -0
  45. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/mcp_loader.py +0 -0
  46. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/memory/__init__.py +0 -0
  47. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/memory/embedding_memory_service.py +0 -0
  48. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/memory/file_memory_service.py +0 -0
  49. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/modality_tools.py +0 -0
  50. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/model_errors.py +0 -0
  51. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/model_routing.py +0 -0
  52. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/multimodal_input.py +0 -0
  53. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/openapi_loader.py +0 -0
  54. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/output_styles.py +0 -0
  55. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/paths.py +0 -0
  56. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/permissions.py +0 -0
  57. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/plugins/__init__.py +0 -0
  58. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  59. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  60. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/policy_profile.py +0 -0
  61. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/pricing.py +0 -0
  62. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/prompt_suggestions.py +0 -0
  63. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/query/__init__.py +0 -0
  64. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/query/config.py +0 -0
  65. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/query/deps.py +0 -0
  66. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/query/engine.py +0 -0
  67. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/query/stop_hooks.py +0 -0
  68. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/query/token_budget.py +0 -0
  69. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/query/transitions.py +0 -0
  70. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/query_sanitizer.py +0 -0
  71. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/refine.py +0 -0
  72. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/review_agent.py +0 -0
  73. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/rules.py +0 -0
  74. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/session_runtime.py +0 -0
  75. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/session_store.py +0 -0
  76. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/skills.py +0 -0
  77. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/slash_commands.py +0 -0
  78. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/thinking.py +0 -0
  79. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tool_prompt_manifest.py +0 -0
  80. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tool_registry.py +0 -0
  81. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tool_result_store.py +0 -0
  82. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/bash.py +0 -0
  83. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/browser.py +0 -0
  84. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/compress_memory.py +0 -0
  85. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/curated_memory.py +0 -0
  86. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/edit.py +0 -0
  87. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/filesystem.py +0 -0
  88. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/notebook.py +0 -0
  89. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/notes.py +0 -0
  90. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/repo_map.py +0 -0
  91. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/search.py +0 -0
  92. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/shell.py +0 -0
  93. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/shell_gate.py +0 -0
  94. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/skills.py +0 -0
  95. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/subtask.py +0 -0
  96. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/tasks.py +0 -0
  97. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/think.py +0 -0
  98. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/todo.py +0 -0
  99. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/veomem_tools.py +0 -0
  100. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/web.py +0 -0
  101. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools/web_search.py +0 -0
  102. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tools_inspector.py +0 -0
  103. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/trust.py +0 -0
  104. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tui/input_handler.py +0 -0
  105. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tui/scrollback.py +0 -0
  106. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tui/spinner.py +0 -0
  107. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tui/welcome_banner.py +0 -0
  108. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/tui/welcome_rich.py +0 -0
  109. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/veomem_bridge.py +0 -0
  110. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/version.py +0 -0
  111. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/vertex.py +0 -0
  112. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/wal.py +0 -0
  113. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/web/__init__.py +0 -0
  114. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/web/sse_adapter.py +0 -0
  115. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/web/terminal_repl.py +0 -0
  116. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/web/web_sse_compat.py +0 -0
  117. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode/workspace_hints.py +0 -0
  118. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode.egg-info/dependency_links.txt +0 -0
  119. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode.egg-info/entry_points.txt +0 -0
  120. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode.egg-info/requires.txt +0 -0
  121. {gemcode-0.3.94 → gemcode-0.3.96}/src/gemcode.egg-info/top_level.txt +0 -0
  122. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_add_dir.py +0 -0
  123. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_agent_instruction.py +0 -0
  124. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_autocompact.py +0 -0
  125. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_capability_routing.py +0 -0
  126. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_checkpoint_diff_command.py +0 -0
  127. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_cli_init.py +0 -0
  128. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_compress_memory_tool.py +0 -0
  129. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_computer_use_permissions.py +0 -0
  130. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_context_budget.py +0 -0
  131. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_context_warning.py +0 -0
  132. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_credentials.py +0 -0
  133. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_eval_harness_layout.py +0 -0
  134. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_ide_stdio_attachments.py +0 -0
  135. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_interactive_permission_ask.py +0 -0
  136. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_kaira_scheduler.py +0 -0
  137. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_modality_tools.py +0 -0
  138. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_model_error_retry.py +0 -0
  139. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_model_errors.py +0 -0
  140. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_model_routing.py +0 -0
  141. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_multimodal_input.py +0 -0
  142. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_output_styles_and_rules.py +0 -0
  143. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_paths.py +0 -0
  144. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_permissions.py +0 -0
  145. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_prompt_suggestions.py +0 -0
  146. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_repl_commands.py +0 -0
  147. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_repl_slash.py +0 -0
  148. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_skills.py +0 -0
  149. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_slash_commands.py +0 -0
  150. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_slash_completion_registry.py +0 -0
  151. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_thinking_config.py +0 -0
  152. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_token_budget.py +0 -0
  153. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_tool_context_circulation.py +0 -0
  154. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_tools.py +0 -0
  155. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_tools_inspector.py +0 -0
  156. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_web_sse_adapter.py +0 -0
  157. {gemcode-0.3.94 → gemcode-0.3.96}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.94
3
+ Version: 0.3.96
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.3.94"
7
+ version = "0.3.96"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.94
3
+ Version: 0.3.96
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -51,6 +51,7 @@ src/gemcode/review_agent.py
51
51
  src/gemcode/rules.py
52
52
  src/gemcode/session_runtime.py
53
53
  src/gemcode/session_store.py
54
+ src/gemcode/session_summariser.py
54
55
  src/gemcode/skills.py
55
56
  src/gemcode/slash_commands.py
56
57
  src/gemcode/thinking.py
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