gemcode 0.3.88__tar.gz → 0.3.90__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 (156) hide show
  1. {gemcode-0.3.88/src/gemcode.egg-info → gemcode-0.3.90}/PKG-INFO +1 -1
  2. {gemcode-0.3.88 → gemcode-0.3.90}/pyproject.toml +1 -1
  3. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/invoke.py +17 -1
  4. gemcode-0.3.90/src/gemcode/tools/veomem_tools.py +252 -0
  5. {gemcode-0.3.88 → gemcode-0.3.90/src/gemcode.egg-info}/PKG-INFO +1 -1
  6. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode.egg-info/SOURCES.txt +1 -0
  7. {gemcode-0.3.88 → gemcode-0.3.90}/LICENSE +0 -0
  8. {gemcode-0.3.88 → gemcode-0.3.90}/MANIFEST.in +0 -0
  9. {gemcode-0.3.88 → gemcode-0.3.90}/README.md +0 -0
  10. {gemcode-0.3.88 → gemcode-0.3.90}/setup.cfg +0 -0
  11. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/__init__.py +0 -0
  12. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/__main__.py +0 -0
  13. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/agent.py +0 -0
  14. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/audit.py +0 -0
  15. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/autocompact.py +0 -0
  16. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/autotune.py +0 -0
  17. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/callbacks.py +0 -0
  18. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/capability_routing.py +0 -0
  19. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/checkpoints.py +0 -0
  20. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/cli.py +0 -0
  21. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/compaction.py +0 -0
  22. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/computer_use/__init__.py +0 -0
  23. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/computer_use/browser_computer.py +0 -0
  24. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/config.py +0 -0
  25. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/context_budget.py +0 -0
  26. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/context_warning.py +0 -0
  27. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/credentials.py +0 -0
  28. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/curated_memory.py +0 -0
  29. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/dynamic_policy.py +0 -0
  30. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/evals/harness.py +0 -0
  31. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/hitl_session.py +0 -0
  32. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/hooks.py +0 -0
  33. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/ide_protocol.py +0 -0
  34. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/ide_stdio.py +0 -0
  35. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/intent_classifier.py +0 -0
  36. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/interactions.py +0 -0
  37. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/kaira_daemon.py +0 -0
  38. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/learning.py +0 -0
  39. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/limits.py +0 -0
  40. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/live_audio_engine.py +0 -0
  41. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/logging_config.py +0 -0
  42. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/mcp_loader.py +0 -0
  43. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/memory/__init__.py +0 -0
  44. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/memory/embedding_memory_service.py +0 -0
  45. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/memory/file_memory_service.py +0 -0
  46. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/modality_tools.py +0 -0
  47. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/model_errors.py +0 -0
  48. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/model_routing.py +0 -0
  49. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/multimodal_input.py +0 -0
  50. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/openapi_loader.py +0 -0
  51. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/output_styles.py +0 -0
  52. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/paths.py +0 -0
  53. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/permissions.py +0 -0
  54. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/plugins/__init__.py +0 -0
  55. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  56. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  57. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/policy_profile.py +0 -0
  58. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/pricing.py +0 -0
  59. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/prompt_suggestions.py +0 -0
  60. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/query/__init__.py +0 -0
  61. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/query/config.py +0 -0
  62. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/query/deps.py +0 -0
  63. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/query/engine.py +0 -0
  64. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/query/stop_hooks.py +0 -0
  65. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/query/token_budget.py +0 -0
  66. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/query/transitions.py +0 -0
  67. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/query_sanitizer.py +0 -0
  68. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/refine.py +0 -0
  69. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/repl_commands.py +0 -0
  70. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/repl_slash.py +0 -0
  71. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/review_agent.py +0 -0
  72. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/rules.py +0 -0
  73. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/session_runtime.py +0 -0
  74. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/session_store.py +0 -0
  75. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/skills.py +0 -0
  76. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/slash_commands.py +0 -0
  77. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/thinking.py +0 -0
  78. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tool_prompt_manifest.py +0 -0
  79. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tool_registry.py +0 -0
  80. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tool_result_store.py +0 -0
  81. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/__init__.py +0 -0
  82. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/bash.py +0 -0
  83. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/browser.py +0 -0
  84. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/compress_memory.py +0 -0
  85. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/curated_memory.py +0 -0
  86. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/edit.py +0 -0
  87. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/filesystem.py +0 -0
  88. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/notebook.py +0 -0
  89. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/notes.py +0 -0
  90. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/repo_map.py +0 -0
  91. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/search.py +0 -0
  92. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/shell.py +0 -0
  93. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/shell_gate.py +0 -0
  94. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/skills.py +0 -0
  95. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/subtask.py +0 -0
  96. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/tasks.py +0 -0
  97. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/think.py +0 -0
  98. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/todo.py +0 -0
  99. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/web.py +0 -0
  100. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools/web_search.py +0 -0
  101. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tools_inspector.py +0 -0
  102. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/trust.py +0 -0
  103. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tui/input_handler.py +0 -0
  104. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tui/scrollback.py +0 -0
  105. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tui/spinner.py +0 -0
  106. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tui/welcome_banner.py +0 -0
  107. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/tui/welcome_rich.py +0 -0
  108. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/veomem_bridge.py +0 -0
  109. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/version.py +0 -0
  110. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/vertex.py +0 -0
  111. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/wal.py +0 -0
  112. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/web/__init__.py +0 -0
  113. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/web/sse_adapter.py +0 -0
  114. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/web/terminal_repl.py +0 -0
  115. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/web/web_sse_compat.py +0 -0
  116. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode/workspace_hints.py +0 -0
  117. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode.egg-info/dependency_links.txt +0 -0
  118. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode.egg-info/entry_points.txt +0 -0
  119. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode.egg-info/requires.txt +0 -0
  120. {gemcode-0.3.88 → gemcode-0.3.90}/src/gemcode.egg-info/top_level.txt +0 -0
  121. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_add_dir.py +0 -0
  122. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_agent_instruction.py +0 -0
  123. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_autocompact.py +0 -0
  124. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_capability_routing.py +0 -0
  125. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_checkpoint_diff_command.py +0 -0
  126. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_cli_init.py +0 -0
  127. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_compress_memory_tool.py +0 -0
  128. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_computer_use_permissions.py +0 -0
  129. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_context_budget.py +0 -0
  130. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_context_warning.py +0 -0
  131. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_credentials.py +0 -0
  132. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_eval_harness_layout.py +0 -0
  133. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_ide_stdio_attachments.py +0 -0
  134. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_interactive_permission_ask.py +0 -0
  135. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_kaira_scheduler.py +0 -0
  136. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_modality_tools.py +0 -0
  137. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_model_error_retry.py +0 -0
  138. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_model_errors.py +0 -0
  139. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_model_routing.py +0 -0
  140. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_multimodal_input.py +0 -0
  141. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_output_styles_and_rules.py +0 -0
  142. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_paths.py +0 -0
  143. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_permissions.py +0 -0
  144. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_prompt_suggestions.py +0 -0
  145. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_repl_commands.py +0 -0
  146. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_repl_slash.py +0 -0
  147. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_skills.py +0 -0
  148. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_slash_commands.py +0 -0
  149. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_slash_completion_registry.py +0 -0
  150. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_thinking_config.py +0 -0
  151. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_token_budget.py +0 -0
  152. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_tool_context_circulation.py +0 -0
  153. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_tools.py +0 -0
  154. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_tools_inspector.py +0 -0
  155. {gemcode-0.3.88 → gemcode-0.3.90}/tests/test_web_sse_adapter.py +0 -0
  156. {gemcode-0.3.88 → gemcode-0.3.90}/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.88
3
+ Version: 0.3.90
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.88"
7
+ version = "0.3.90"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -175,10 +175,26 @@ async def run_turn(
175
175
  if attachment_paths:
176
176
  from gemcode.multimodal_input import build_user_content
177
177
 
178
+ # Optional HITL gate for local attachment reads/upload materialization.
179
+ # This is separate from workspace trust: the OS may still require
180
+ # user-granted "Files and Folders" permissions on first access.
181
+ # If approved once, we don't re-prompt for the rest of this session.
182
+ attach_allow = True
183
+ if cfg is not None:
184
+ attach_allow = bool(getattr(cfg, "interactive_permission_ask", False))
185
+ attach_allow = attach_allow and hasattr(sys.stdin, "isatty") and sys.stdin.isatty()
186
+ if attach_allow and not bool(getattr(cfg, "_attachments_allowed", False)):
187
+ attach_allow = _prompt_yes_no(
188
+ "Allow GemCode to read and upload the attached file(s) from disk? (y/n) "
189
+ )
190
+ if attach_allow:
191
+ object.__setattr__(cfg, "_attachments_allowed", True)
192
+ effective_attachments = attachment_paths if attach_allow else None
193
+
178
194
  root = cfg.project_root if cfg is not None else Path.cwd()
179
195
  current_message, attach_warn = build_user_content(
180
196
  prompt,
181
- attachment_paths,
197
+ effective_attachments,
182
198
  project_root=root,
183
199
  )
184
200
  for w in attach_warn:
@@ -0,0 +1,252 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ def _enabled() -> bool:
9
+ return os.environ.get("GEMCODE_VEOMEM", "").strip().lower() in ("1", "true", "yes", "on")
10
+
11
+
12
+ def make_veomem_tools(cfg) -> list[Any]:
13
+ """
14
+ VeoMem recall tools (token-efficient, 3-step retrieval workflow).
15
+
16
+ Exposes a compact flow to the agent:
17
+ 1) `veomem_search` returns a small index of matching observation IDs
18
+ 2) `veomem_timeline` fetches compact context around a specific anchor ID
19
+ 3) `veomem_get_observations` fetches full text for a small set of IDs
20
+ """
21
+ if not _enabled():
22
+ return []
23
+
24
+ project_root = Path(getattr(cfg, "project_root"))
25
+
26
+ try:
27
+ from veomem.store import get_observations as _get_observations
28
+ from veomem.store import search as _search
29
+ from veomem.store import timeline as _timeline
30
+ except Exception:
31
+ return []
32
+
33
+ def veomem_search(
34
+ query: str,
35
+ limit: int = 10,
36
+ kind: str | None = None,
37
+ wing: str | None = None,
38
+ room: str | None = None,
39
+ ) -> dict[str, Any]:
40
+ """
41
+ Search VeoMem for relevant prior tool observations.
42
+
43
+ Returns a small index of hits (IDs + snippets). Typical usage:
44
+ - call `veomem_timeline()` for one promising anchor
45
+ - then call `veomem_get_observations()` for a small set of IDs
46
+ """
47
+ if not isinstance(query, str) or not query.strip():
48
+ return {"error": "query must be a non-empty string"}
49
+ return _search(
50
+ project_root,
51
+ query=str(query),
52
+ limit=int(limit),
53
+ kind=str(kind) if kind else None,
54
+ wing=str(wing) if wing else None,
55
+ room=str(room) if room else None,
56
+ )
57
+
58
+ def veomem_timeline(
59
+ id: int,
60
+ window_ms: int = 120000,
61
+ limit: int = 40,
62
+ ) -> dict[str, Any]:
63
+ """
64
+ Fetch compact context around a specific VeoMem observation id.
65
+
66
+ This is the "timeline" step: it returns neighbors around an anchor.
67
+ The agent then decides which IDs to fetch in full.
68
+ """
69
+ return _timeline(
70
+ project_root,
71
+ observation_id=int(id),
72
+ window_ms=int(window_ms),
73
+ limit=int(limit),
74
+ )
75
+
76
+ def veomem_get_observations(
77
+ ids: str,
78
+ max_chars: int = 8000,
79
+ ) -> dict[str, Any]:
80
+ """
81
+ Fetch full observation text for selected VeoMem observation IDs.
82
+
83
+ ids:
84
+ Comma-separated list, e.g. "12,13,21"
85
+
86
+ The returned `text` is truncated to `max_chars` per observation.
87
+ """
88
+ if not isinstance(ids, str) or not ids.strip():
89
+ return {"error": "ids must be a non-empty comma-separated string"}
90
+
91
+ parts = [x.strip() for x in ids.split(",") if x.strip()]
92
+ id_list: list[int] = []
93
+ for p in parts:
94
+ try:
95
+ id_list.append(int(p))
96
+ except Exception:
97
+ pass
98
+
99
+ if not id_list:
100
+ return {"error": "no valid integer ids found"}
101
+
102
+ rows = _get_observations(project_root, ids=id_list)
103
+ out: list[dict[str, Any]] = []
104
+ for o in rows:
105
+ t = (o.text or "").strip().replace("\n", " ")
106
+ truncated = False
107
+ if len(t) > int(max_chars):
108
+ t = t[: int(max_chars)].rstrip() + "…"
109
+ truncated = True
110
+ out.append(
111
+ {
112
+ "id": int(o.id),
113
+ "kind": str(o.kind),
114
+ "title": str(o.title),
115
+ "wing": str(o.wing),
116
+ "room": str(o.room),
117
+ "text": t,
118
+ "truncated": truncated,
119
+ }
120
+ )
121
+
122
+ return {"ok": True, "results": out, "count": len(out)}
123
+
124
+ veomem_search.__name__ = "veomem_search"
125
+ veomem_timeline.__name__ = "veomem_timeline"
126
+ veomem_get_observations.__name__ = "veomem_get_observations"
127
+
128
+ return [veomem_search, veomem_timeline, veomem_get_observations]
129
+ import os
130
+ from pathlib import Path
131
+ from typing import Any
132
+
133
+
134
+ def _enabled() -> bool:
135
+ return os.environ.get("GEMCODE_VEOMEM", "").strip().lower() in ("1", "true", "yes", "on")
136
+
137
+
138
+ def make_veomem_tools(cfg) -> list[Any]:
139
+ """
140
+ VeoMem recall tools (compact 3-step retrieval workflow).
141
+
142
+ Exposes a token-efficient 3-step flow to the agent:
143
+ 1) `veomem_search` returns an index of matching observation IDs
144
+ 2) `veomem_timeline` fetches compact context around a specific anchor ID
145
+ 3) `veomem_get_observations` fetches full text for a small set of IDs
146
+ """
147
+ if not _enabled():
148
+ return []
149
+
150
+ project_root = Path(getattr(cfg, "project_root"))
151
+
152
+ try:
153
+ from veomem.store import get_observations as _get_observations
154
+ from veomem.store import search as _search
155
+ from veomem.store import timeline as _timeline
156
+ except Exception:
157
+ return []
158
+
159
+ def veomem_search(
160
+ query: str,
161
+ limit: int = 10,
162
+ kind: str | None = None,
163
+ wing: str | None = None,
164
+ room: str | None = None,
165
+ ) -> dict[str, Any]:
166
+ """
167
+ Search VeoMem for relevant prior tool observations (FTS5/BM25).
168
+
169
+ Returns a small "index" of hits (IDs + snippets). Prefer:
170
+ - call `veomem_timeline()` for one promising anchor
171
+ - then call `veomem_get_observations()` for a small set of IDs
172
+ """
173
+ if not isinstance(query, str) or not query.strip():
174
+ return {"error": "query must be a non-empty string"}
175
+ return _search(
176
+ project_root,
177
+ query=str(query),
178
+ limit=int(limit),
179
+ kind=str(kind) if kind else None,
180
+ wing=str(wing) if wing else None,
181
+ room=str(room) if room else None,
182
+ )
183
+
184
+ def veomem_timeline(
185
+ id: int,
186
+ window_ms: int = 120000,
187
+ limit: int = 40,
188
+ ) -> dict[str, Any]:
189
+ """
190
+ Fetch compact context around a specific VeoMem observation id.
191
+
192
+ This is the "timeline" step: it returns neighbors around an anchor
193
+ (usually the agent should then decide which IDs to fetch in full).
194
+ """
195
+ return _timeline(
196
+ project_root,
197
+ observation_id=int(id),
198
+ window_ms=int(window_ms),
199
+ limit=int(limit),
200
+ )
201
+
202
+ def veomem_get_observations(
203
+ ids: str,
204
+ max_chars: int = 8000,
205
+ ) -> dict[str, Any]:
206
+ """
207
+ Fetch full observation text for selected VeoMem observation IDs.
208
+
209
+ ids:
210
+ Comma-separated list, e.g. "12,13,21"
211
+
212
+ The returned `text` is truncated to `max_chars` per observation.
213
+ """
214
+ if not isinstance(ids, str) or not ids.strip():
215
+ return {"error": "ids must be a non-empty comma-separated string"}
216
+ parts = [x.strip() for x in ids.split(",") if x.strip()]
217
+ id_list: list[int] = []
218
+ for p in parts:
219
+ try:
220
+ id_list.append(int(p))
221
+ except Exception:
222
+ pass
223
+ if not id_list:
224
+ return {"error": "no valid integer ids found"}
225
+
226
+ rows = _get_observations(project_root, ids=id_list)
227
+ out: list[dict[str, Any]] = []
228
+ for o in rows:
229
+ t = (o.text or "").strip().replace("\n", " ")
230
+ truncated = False
231
+ if len(t) > int(max_chars):
232
+ t = t[: int(max_chars)].rstrip() + "…"
233
+ truncated = True
234
+ out.append(
235
+ {
236
+ "id": int(o.id),
237
+ "kind": str(o.kind),
238
+ "title": str(o.title),
239
+ "wing": str(o.wing),
240
+ "room": str(o.room),
241
+ "text": t,
242
+ "truncated": truncated,
243
+ }
244
+ )
245
+ return {"ok": True, "results": out, "count": len(out)}
246
+
247
+ veomem_search.__name__ = "veomem_search"
248
+ veomem_timeline.__name__ = "veomem_timeline"
249
+ veomem_get_observations.__name__ = "veomem_get_observations"
250
+
251
+ return [veomem_search, veomem_timeline, veomem_get_observations]
252
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.88
3
+ Version: 0.3.90
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -104,6 +104,7 @@ src/gemcode/tools/subtask.py
104
104
  src/gemcode/tools/tasks.py
105
105
  src/gemcode/tools/think.py
106
106
  src/gemcode/tools/todo.py
107
+ src/gemcode/tools/veomem_tools.py
107
108
  src/gemcode/tools/web.py
108
109
  src/gemcode/tools/web_search.py
109
110
  src/gemcode/tui/input_handler.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
File without changes