gemcode 0.3.114__tar.gz → 0.3.116__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 (167) hide show
  1. {gemcode-0.3.114/src/gemcode.egg-info → gemcode-0.3.116}/PKG-INFO +1 -1
  2. {gemcode-0.3.114 → gemcode-0.3.116}/pyproject.toml +1 -1
  3. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/invoke.py +9 -2
  4. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/kaira_daemon.py +85 -2
  5. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/kaira_ipc.py +4 -1
  6. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/kaira_job_store.py +5 -1
  7. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/org_tools.py +99 -45
  8. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/subtask.py +101 -1
  9. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tui/scrollback.py +32 -0
  10. {gemcode-0.3.114 → gemcode-0.3.116/src/gemcode.egg-info}/PKG-INFO +1 -1
  11. {gemcode-0.3.114 → gemcode-0.3.116}/LICENSE +0 -0
  12. {gemcode-0.3.114 → gemcode-0.3.116}/MANIFEST.in +0 -0
  13. {gemcode-0.3.114 → gemcode-0.3.116}/README.md +0 -0
  14. {gemcode-0.3.114 → gemcode-0.3.116}/setup.cfg +0 -0
  15. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/__init__.py +0 -0
  16. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/__main__.py +0 -0
  17. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/agent.py +0 -0
  18. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/audit.py +0 -0
  19. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/autocompact.py +0 -0
  20. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/automations.py +0 -0
  21. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/autotune.py +0 -0
  22. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/callbacks.py +0 -0
  23. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/capability_routing.py +0 -0
  24. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/checkpoints.py +0 -0
  25. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/cli.py +0 -0
  26. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/compaction.py +0 -0
  27. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/computer_use/__init__.py +0 -0
  28. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/computer_use/browser_computer.py +0 -0
  29. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/config.py +0 -0
  30. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/context_budget.py +0 -0
  31. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/context_warning.py +0 -0
  32. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/credentials.py +0 -0
  33. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/curated_memory.py +0 -0
  34. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/dynamic_policy.py +0 -0
  35. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/evals/harness.py +0 -0
  36. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/hitl_session.py +0 -0
  37. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/hooks.py +0 -0
  38. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/ide_protocol.py +0 -0
  39. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/ide_stdio.py +0 -0
  40. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/intent_classifier.py +0 -0
  41. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/interactions.py +0 -0
  42. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/kaira_client.py +0 -0
  43. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/learning.py +0 -0
  44. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/limits.py +0 -0
  45. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/live_audio_engine.py +0 -0
  46. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/logging_config.py +0 -0
  47. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/mcp_loader.py +0 -0
  48. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/memory/__init__.py +0 -0
  49. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/memory/embedding_memory_service.py +0 -0
  50. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/memory/file_memory_service.py +0 -0
  51. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/modality_tools.py +0 -0
  52. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/model_errors.py +0 -0
  53. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/model_routing.py +0 -0
  54. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/multimodal_input.py +0 -0
  55. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/openapi_loader.py +0 -0
  56. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/org.py +0 -0
  57. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/output_styles.py +0 -0
  58. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/paths.py +0 -0
  59. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/permissions.py +0 -0
  60. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/plugins/__init__.py +0 -0
  61. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  62. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  63. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/policy_profile.py +0 -0
  64. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/pricing.py +0 -0
  65. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/prompt_suggestions.py +0 -0
  66. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/query/__init__.py +0 -0
  67. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/query/config.py +0 -0
  68. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/query/deps.py +0 -0
  69. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/query/engine.py +0 -0
  70. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/query/stop_hooks.py +0 -0
  71. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/query/token_budget.py +0 -0
  72. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/query/transitions.py +0 -0
  73. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/query_sanitizer.py +0 -0
  74. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/refine.py +0 -0
  75. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/repl_commands.py +0 -0
  76. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/repl_slash.py +0 -0
  77. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/review_agent.py +0 -0
  78. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/rules.py +0 -0
  79. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/session_runtime.py +0 -0
  80. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/session_store.py +0 -0
  81. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/session_summariser.py +0 -0
  82. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/skills.py +0 -0
  83. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/slash_commands.py +0 -0
  84. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/thinking.py +0 -0
  85. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tool_prompt_manifest.py +0 -0
  86. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tool_registry.py +0 -0
  87. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tool_result_store.py +0 -0
  88. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/__init__.py +0 -0
  89. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/automations_tools.py +0 -0
  90. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/bash.py +0 -0
  91. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/browser.py +0 -0
  92. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/compress_memory.py +0 -0
  93. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/curated_memory.py +0 -0
  94. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/edit.py +0 -0
  95. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/filesystem.py +0 -0
  96. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/notebook.py +0 -0
  97. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/notes.py +0 -0
  98. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/repo_map.py +0 -0
  99. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/search.py +0 -0
  100. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/shell.py +0 -0
  101. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/shell_gate.py +0 -0
  102. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/skills.py +0 -0
  103. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/tasks.py +0 -0
  104. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/think.py +0 -0
  105. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/todo.py +0 -0
  106. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/user_choice.py +0 -0
  107. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/veomem_tools.py +0 -0
  108. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/web.py +0 -0
  109. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools/web_search.py +0 -0
  110. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tools_inspector.py +0 -0
  111. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/trust.py +0 -0
  112. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tui/input_handler.py +0 -0
  113. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tui/spinner.py +0 -0
  114. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tui/welcome_banner.py +0 -0
  115. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/tui/welcome_rich.py +0 -0
  116. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/veomem_bridge.py +0 -0
  117. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/version.py +0 -0
  118. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/vertex.py +0 -0
  119. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/wal.py +0 -0
  120. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/web/__init__.py +0 -0
  121. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/web/sse_adapter.py +0 -0
  122. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/web/terminal_repl.py +0 -0
  123. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/web/web_sse_compat.py +0 -0
  124. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode/workspace_hints.py +0 -0
  125. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode.egg-info/SOURCES.txt +0 -0
  126. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode.egg-info/dependency_links.txt +0 -0
  127. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode.egg-info/entry_points.txt +0 -0
  128. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode.egg-info/requires.txt +0 -0
  129. {gemcode-0.3.114 → gemcode-0.3.116}/src/gemcode.egg-info/top_level.txt +0 -0
  130. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_add_dir.py +0 -0
  131. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_agent_instruction.py +0 -0
  132. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_autocompact.py +0 -0
  133. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_automations.py +0 -0
  134. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_capability_routing.py +0 -0
  135. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_checkpoint_diff_command.py +0 -0
  136. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_cli_init.py +0 -0
  137. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_compress_memory_tool.py +0 -0
  138. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_computer_use_permissions.py +0 -0
  139. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_context_budget.py +0 -0
  140. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_context_warning.py +0 -0
  141. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_credentials.py +0 -0
  142. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_eval_harness_layout.py +0 -0
  143. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_ide_stdio_attachments.py +0 -0
  144. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_interactive_permission_ask.py +0 -0
  145. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_kaira_scheduler.py +0 -0
  146. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_modality_tools.py +0 -0
  147. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_model_error_retry.py +0 -0
  148. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_model_errors.py +0 -0
  149. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_model_routing.py +0 -0
  150. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_multimodal_input.py +0 -0
  151. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_output_styles_and_rules.py +0 -0
  152. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_paths.py +0 -0
  153. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_permissions.py +0 -0
  154. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_prompt_suggestions.py +0 -0
  155. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_repl_commands.py +0 -0
  156. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_repl_slash.py +0 -0
  157. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_session_runtime_cache.py +0 -0
  158. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_skills.py +0 -0
  159. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_slash_commands.py +0 -0
  160. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_slash_completion_registry.py +0 -0
  161. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_thinking_config.py +0 -0
  162. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_token_budget.py +0 -0
  163. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_tool_context_circulation.py +0 -0
  164. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_tools.py +0 -0
  165. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_tools_inspector.py +0 -0
  166. {gemcode-0.3.114 → gemcode-0.3.116}/tests/test_web_sse_adapter.py +0 -0
  167. {gemcode-0.3.114 → gemcode-0.3.116}/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.114
3
+ Version: 0.3.116
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.114"
7
+ version = "0.3.116"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -42,7 +42,14 @@ async def _maybe_enqueue_kaira_autopilot(*, cfg: "GemCodeConfig", session_id: st
42
42
  return
43
43
  object.__setattr__(cfg, "_kaira_autopilot_fp", fp)
44
44
 
45
- sock = os.environ.get("GEMCODE_KAIRA_SOCKET") or str(cfg.project_root / ".gemcode" / "ipc.sock")
45
+ try:
46
+ from gemcode.org import resolve_fleet_root
47
+
48
+ fleet_root = resolve_fleet_root(cfg.project_root)
49
+ except Exception:
50
+ fleet_root = cfg.project_root
51
+
52
+ sock = os.environ.get("GEMCODE_KAIRA_SOCKET") or str(fleet_root / ".gemcode" / "ipc.sock")
46
53
  if not Path(sock).exists():
47
54
  return
48
55
  # Heuristic: suggest likely checks, but let the agent choose based on repo.
@@ -70,7 +77,7 @@ async def _maybe_enqueue_kaira_autopilot(*, cfg: "GemCodeConfig", session_id: st
70
77
  )
71
78
  if auto_org:
72
79
  from gemcode.org import find_member
73
- m = find_member(cfg.project_root, "kaira")
80
+ m = find_member(fleet_root, "kaira")
74
81
  if m is not None and m.kind == "kaira_worker":
75
82
  from gemcode.tools.org_tools import make_org_tools
76
83
  tools = make_org_tools(cfg)
@@ -151,6 +151,7 @@ class KairaJob:
151
151
  prompt: str
152
152
  priority: int
153
153
  session_id: str
154
+ meta: dict | None = None
154
155
 
155
156
 
156
157
  class KairaDaemon:
@@ -415,6 +416,7 @@ class KairaDaemon:
415
416
  prompt: str,
416
417
  priority: int | None = None,
417
418
  session_id: str,
419
+ meta: dict | None = None,
418
420
  ) -> str:
419
421
  """Enqueue a new job into the priority queue and return job_id."""
420
422
  job_id = f"job_{uuid.uuid4().hex[:10]}"
@@ -425,9 +427,10 @@ class KairaDaemon:
425
427
  prompt=prompt,
426
428
  priority=pr,
427
429
  session_id=session_id,
430
+ meta=meta or None,
428
431
  )
429
432
  try:
430
- rec = new_job_record(job_id=job_id, session_id=session_id, priority=pr, prompt=prompt)
433
+ rec = new_job_record(job_id=job_id, session_id=session_id, priority=pr, prompt=prompt, meta=meta or None)
431
434
  self._job_records[job_id] = rec
432
435
  self._store.upsert(rec)
433
436
  except Exception:
@@ -469,7 +472,11 @@ class KairaDaemon:
469
472
 
470
473
  try:
471
474
  rec = self._job_records.get(job.job_id) or new_job_record(
472
- job_id=job.job_id, session_id=job.session_id, priority=job.priority, prompt=job.prompt
475
+ job_id=job.job_id,
476
+ session_id=job.session_id,
477
+ priority=job.priority,
478
+ prompt=job.prompt,
479
+ meta=(job.meta if isinstance(getattr(job, "meta", None), dict) else None),
473
480
  )
474
481
  self._job_records[job.job_id] = rec
475
482
  self._store.upsert(mark_running(rec))
@@ -555,6 +562,46 @@ class KairaDaemon:
555
562
  },
556
563
  }
557
564
  )
565
+
566
+ # If this job was enqueued as an org delegation, automatically publish
567
+ # an org.report back to the manager (and any notify_chain).
568
+ try:
569
+ meta = getattr(rec2, "meta", None) if isinstance(rec2, JobRecord) else None
570
+ org = meta.get("org") if isinstance(meta, dict) else None
571
+ if isinstance(org, dict):
572
+ member = org.get("member")
573
+ task = str(org.get("task") or "").strip()
574
+ context = str(org.get("context") or "").strip()
575
+ notify_chain = org.get("notify_chain")
576
+ if not isinstance(notify_chain, list):
577
+ notify_chain = ["manager"]
578
+ payload = {
579
+ "member": member if isinstance(member, dict) else {},
580
+ "capabilities": org.get("capabilities") if isinstance(org.get("capabilities"), dict) else {},
581
+ "status": "finished",
582
+ "task": task,
583
+ "context": context,
584
+ "job_id": job.job_id,
585
+ "error": "",
586
+ "result": {
587
+ "report_json": report_obj,
588
+ "report": (text or "")[:8000],
589
+ },
590
+ "notify_chain": notify_chain,
591
+ }
592
+ for to_addr in notify_chain:
593
+ await self._ipc.broadcast(
594
+ {
595
+ "type": "event",
596
+ "event": "bus_message",
597
+ "topic": "org.report",
598
+ "to": str(to_addr or "manager"),
599
+ "from_addr": "runtime",
600
+ "payload": payload,
601
+ }
602
+ )
603
+ except Exception:
604
+ pass
558
605
  except Exception:
559
606
  pass
560
607
  except Exception:
@@ -618,6 +665,42 @@ class KairaDaemon:
618
665
  )
619
666
  except Exception:
620
667
  pass
668
+ # Also emit org.report failures for org-delegated jobs.
669
+ try:
670
+ recf = self._job_records.get(job.job_id)
671
+ meta = getattr(recf, "meta", None) if recf is not None else None
672
+ org = meta.get("org") if isinstance(meta, dict) else None
673
+ if isinstance(org, dict):
674
+ member = org.get("member")
675
+ task = str(org.get("task") or "").strip()
676
+ context = str(org.get("context") or "").strip()
677
+ notify_chain = org.get("notify_chain")
678
+ if not isinstance(notify_chain, list):
679
+ notify_chain = ["manager"]
680
+ payload = {
681
+ "member": member if isinstance(member, dict) else {},
682
+ "capabilities": org.get("capabilities") if isinstance(org.get("capabilities"), dict) else {},
683
+ "status": "failed",
684
+ "task": task,
685
+ "context": context,
686
+ "job_id": job.job_id,
687
+ "error": f"{type(e).__name__}: {e}",
688
+ "result": None,
689
+ "notify_chain": notify_chain,
690
+ }
691
+ for to_addr in notify_chain:
692
+ await self._ipc.broadcast(
693
+ {
694
+ "type": "event",
695
+ "event": "bus_message",
696
+ "topic": "org.report",
697
+ "to": str(to_addr or "manager"),
698
+ "from_addr": "runtime",
699
+ "payload": payload,
700
+ }
701
+ )
702
+ except Exception:
703
+ pass
621
704
  try:
622
705
  rec3 = self._job_records.get(job.job_id)
623
706
  if rec3 is not None:
@@ -250,7 +250,10 @@ class KairaIpcServer:
250
250
  try:
251
251
  pr = msg.get("priority", None)
252
252
  sid = str(msg.get("session_id") or "").strip()
253
- job_id = self._enqueue_fn(prompt=prompt, priority=pr, session_id=sid)
253
+ meta = msg.get("meta", None)
254
+ if not isinstance(meta, dict):
255
+ meta = None
256
+ job_id = self._enqueue_fn(prompt=prompt, priority=pr, session_id=sid, meta=meta)
254
257
  client.emitter.send(make_response(id=req_id, ok=True, job_id=job_id))
255
258
  except Exception as e:
256
259
  client.emitter.send(
@@ -35,6 +35,7 @@ class JobRecord:
35
35
  finished_ms: int | None = None
36
36
  error: str | None = None
37
37
  last_text: str = ""
38
+ meta: dict[str, Any] | None = None
38
39
 
39
40
  def to_dict(self) -> dict[str, Any]:
40
41
  return {
@@ -42,6 +43,7 @@ class JobRecord:
42
43
  "session_id": self.session_id,
43
44
  "priority": int(self.priority),
44
45
  "prompt": self.prompt,
46
+ "meta": (self.meta or None),
45
47
  "status": self.status,
46
48
  "created_ms": int(self.created_ms),
47
49
  "updated_ms": int(self.updated_ms),
@@ -83,6 +85,7 @@ class KairaJobStore:
83
85
  session_id=str(obj.get("session_id") or ""),
84
86
  priority=int(obj.get("priority") or 0),
85
87
  prompt=str(obj.get("prompt") or ""),
88
+ meta=(obj.get("meta") if isinstance(obj.get("meta"), dict) else None),
86
89
  status=str(obj.get("status") or "queued"), # type: ignore[arg-type]
87
90
  created_ms=int(obj.get("created_ms") or 0),
88
91
  updated_ms=int(obj.get("updated_ms") or 0),
@@ -110,13 +113,14 @@ class KairaJobStore:
110
113
  return out
111
114
 
112
115
 
113
- def new_job_record(*, job_id: str, session_id: str, priority: int, prompt: str) -> JobRecord:
116
+ def new_job_record(*, job_id: str, session_id: str, priority: int, prompt: str, meta: dict[str, Any] | None = None) -> JobRecord:
114
117
  now = _now_ms()
115
118
  return JobRecord(
116
119
  job_id=job_id,
117
120
  session_id=session_id,
118
121
  priority=int(priority),
119
122
  prompt=prompt,
123
+ meta=meta or None,
120
124
  status="queued",
121
125
  created_ms=now,
122
126
  updated_ms=now,
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import os
4
5
  from pathlib import Path
5
6
  from typing import Any
6
7
 
@@ -82,42 +83,60 @@ def make_org_tools(cfg: GemCodeConfig) -> list:
82
83
  ) -> None:
83
84
  if not _bus_enabled():
84
85
  return
85
- try:
86
- from gemcode.kaira_client import KairaIpcClient
87
- import os
86
+ fleet_root = resolve_fleet_root(getattr(cfg, "project_root", Path.cwd()))
88
87
 
89
- sock = os.environ.get("GEMCODE_KAIRA_SOCKET") or str(
90
- getattr(cfg, "project_root", Path.cwd()) / ".gemcode" / "ipc.sock"
91
- )
92
- # Only attempt publish if socket exists (avoid noisy failures).
88
+ def _audit_fallback(payload: dict[str, Any], *, why: str) -> None:
93
89
  try:
94
- if not Path(sock).exists():
95
- return
90
+ from gemcode.audit import append_audit
91
+
92
+ append_audit(
93
+ fleet_root,
94
+ {
95
+ "event": "org.report",
96
+ "why": why,
97
+ "payload": payload,
98
+ },
99
+ )
96
100
  except Exception:
97
- pass
101
+ return
102
+
103
+ from_addr = str(getattr(m, "address", "") or getattr(m, "name", "") or "")
104
+ member_dict = (m.to_dict() if hasattr(m, "to_dict") else {})
105
+ # Capabilities snapshot: keep it small and stable.
106
+ caps = {
107
+ "kind": member_dict.get("kind"),
108
+ "address": member_dict.get("address") or from_addr,
109
+ "workspace_rel": member_dict.get("workspace_rel", ""),
110
+ "reports_to": member_dict.get("reports_to", ""),
111
+ }
112
+ chain = _ancestor_addresses_for(m)
113
+ payload: dict[str, Any] = {
114
+ "member": member_dict,
115
+ "capabilities": caps,
116
+ "status": status,
117
+ "task": task,
118
+ "context": context,
119
+ "job_id": job_id,
120
+ "error": error,
121
+ "result": result,
122
+ "notify_chain": chain,
123
+ }
124
+
125
+ sock = os.environ.get("GEMCODE_KAIRA_SOCKET") or str(fleet_root / ".gemcode" / "ipc.sock")
126
+ # If runtime IPC isn't up, still persist the report locally.
127
+ try:
128
+ if not Path(sock).exists():
129
+ _audit_fallback(payload, why="ipc_socket_missing")
130
+ return
131
+ except Exception:
132
+ _audit_fallback(payload, why="ipc_socket_stat_failed")
133
+ return
134
+
135
+ try:
136
+ from gemcode.kaira_client import KairaIpcClient
137
+
98
138
  c = await KairaIpcClient.connect(socket_path=str(sock))
99
139
  try:
100
- from_addr = str(getattr(m, "address", "") or getattr(m, "name", "") or "")
101
- member_dict = (m.to_dict() if hasattr(m, "to_dict") else {})
102
- # Capabilities snapshot: keep it small and stable.
103
- caps = {
104
- "kind": member_dict.get("kind"),
105
- "address": member_dict.get("address") or from_addr,
106
- "workspace_rel": member_dict.get("workspace_rel", ""),
107
- "reports_to": member_dict.get("reports_to", ""),
108
- }
109
- chain = _ancestor_addresses_for(m)
110
- payload = {
111
- "member": member_dict,
112
- "capabilities": caps,
113
- "status": status,
114
- "task": task,
115
- "context": context,
116
- "job_id": job_id,
117
- "error": error,
118
- "result": result,
119
- "notify_chain": chain,
120
- }
121
140
  # Notify parent, grandparent, ... (and manager).
122
141
  for to_addr in chain:
123
142
  await c.publish(
@@ -128,7 +147,8 @@ def make_org_tools(cfg: GemCodeConfig) -> list:
128
147
  )
129
148
  finally:
130
149
  await c.close()
131
- except Exception:
150
+ except Exception as e:
151
+ _audit_fallback(payload, why=f"ipc_publish_failed: {type(e).__name__}: {e}")
132
152
  return
133
153
 
134
154
  def org_list() -> dict:
@@ -191,21 +211,38 @@ def make_org_tools(cfg: GemCodeConfig) -> list:
191
211
  prompt += "\n\nContext:\n" + ctx
192
212
 
193
213
  if m.kind == "kaira_worker":
194
- # Delegate to Kaira via IPC enqueue (background).
214
+ # Delegate to Kaira via IPC enqueue (background). If IPC is unavailable,
215
+ # fall back to an in-process subagent run so delegation still "just works".
195
216
  try:
196
217
  from gemcode.kaira_client import KairaIpcClient
197
- sock = (
198
- getattr(cfg, "project_root", Path.cwd()) / ".gemcode" / "ipc.sock"
199
- )
200
- sock_s = str(sock)
218
+ fleet_root = resolve_fleet_root(getattr(cfg, "project_root", Path.cwd()))
219
+ sock_s = os.environ.get("GEMCODE_KAIRA_SOCKET") or str(fleet_root / ".gemcode" / "ipc.sock")
201
220
  client = await KairaIpcClient.connect(socket_path=sock_s)
202
221
  try:
203
222
  session_id = str(getattr(cfg, "_active_session_id", "") or "")
223
+ notify_chain = _ancestor_addresses_for(m)
224
+ # Attach org metadata so the runtime can auto-publish org.report when
225
+ # the job finishes/fails (default, no extra prompting needed).
226
+ meta = {
227
+ "org": {
228
+ "member": (m.to_dict() if hasattr(m, "to_dict") else {}),
229
+ "capabilities": {
230
+ "kind": getattr(m, "kind", ""),
231
+ "address": getattr(m, "address", "") or getattr(m, "name", ""),
232
+ "workspace_rel": getattr(m, "workspace_rel", "") or "",
233
+ "reports_to": getattr(m, "reports_to", "") or "",
234
+ },
235
+ "task": task,
236
+ "context": ctx,
237
+ "notify_chain": notify_chain,
238
+ }
239
+ }
204
240
  res = await client.request(
205
241
  action="enqueue",
206
242
  prompt=prompt,
207
243
  priority=0,
208
244
  session_id=session_id,
245
+ meta=meta,
209
246
  )
210
247
  if not res.get("ok"):
211
248
  await _publish_org_report(
@@ -229,14 +266,31 @@ def make_org_tools(cfg: GemCodeConfig) -> list:
229
266
  finally:
230
267
  await client.close()
231
268
  except Exception as e:
232
- await _publish_org_report(
233
- m=m,
234
- status="failed",
235
- task=task,
236
- context=ctx,
237
- error=f"kaira_ipc_unavailable: {type(e).__name__}: {e}",
238
- )
239
- return {"ok": False, "error": f"kaira_ipc_unavailable: {type(e).__name__}: {e}"}
269
+ # Fallback: run as a subagent right now (best-effort). This keeps UX
270
+ # consistent when users think "agent delegation" should always work.
271
+ try:
272
+ from gemcode.tools.subtask import make_run_subtask_tool
273
+
274
+ run_subtask = make_run_subtask_tool(cfg)
275
+ out = await run_subtask(prompt, "")
276
+ result = out.get("result") if isinstance(out, dict) else out
277
+ await _publish_org_report(
278
+ m=m,
279
+ status="finished",
280
+ task=task,
281
+ context=ctx,
282
+ result={"kind": "fallback_subagent", "error": f"kaira_ipc_unavailable: {type(e).__name__}: {e}", "result": result},
283
+ )
284
+ return {"ok": True, "delegated_to": m.to_dict(), "result": result, "fallback": "subagent"}
285
+ except Exception as e2:
286
+ await _publish_org_report(
287
+ m=m,
288
+ status="failed",
289
+ task=task,
290
+ context=ctx,
291
+ error=f"kaira_ipc_unavailable: {type(e).__name__}: {e}; fallback_subagent_failed: {type(e2).__name__}: {e2}",
292
+ )
293
+ return {"ok": False, "error": f"kaira_ipc_unavailable: {type(e).__name__}: {e}"}
240
294
 
241
295
  # Delegate to an in-process isolated subagent.
242
296
  try:
@@ -23,6 +23,9 @@ The sub-agent:
23
23
  from __future__ import annotations
24
24
 
25
25
  import asyncio
26
+ import os
27
+ from pathlib import Path
28
+ from typing import Any
26
29
  from gemcode.config import GemCodeConfig
27
30
 
28
31
 
@@ -70,6 +73,60 @@ def _build_sub_tools(cfg: GemCodeConfig) -> list:
70
73
  return tools
71
74
 
72
75
 
76
+ async def _publish_subtask_report(
77
+ *,
78
+ cfg: GemCodeConfig,
79
+ status: str,
80
+ task: str,
81
+ context: str,
82
+ sub_session_id: str,
83
+ result: object | None = None,
84
+ error: str = "",
85
+ ) -> None:
86
+ """
87
+ Durable report path for *all* in-process subagents:
88
+ - Publish to runtime bus when available
89
+ - Otherwise append to fleet-root `.gemcode/audit.log`
90
+ """
91
+ try:
92
+ from gemcode.org import resolve_fleet_root
93
+
94
+ fleet_root = resolve_fleet_root(getattr(cfg, "project_root", Path.cwd()))
95
+ except Exception:
96
+ fleet_root = Path(getattr(cfg, "project_root", Path.cwd()))
97
+
98
+ payload: dict[str, Any] = {
99
+ "kind": "subagent",
100
+ "status": status,
101
+ "task": task,
102
+ "context": context,
103
+ "sub_session_id": sub_session_id,
104
+ "error": error,
105
+ "result": result,
106
+ }
107
+
108
+ sock = os.environ.get("GEMCODE_KAIRA_SOCKET") or str(fleet_root / ".gemcode" / "ipc.sock")
109
+ try:
110
+ if Path(sock).exists():
111
+ from gemcode.kaira_client import KairaIpcClient
112
+
113
+ c = await KairaIpcClient.connect(socket_path=str(sock))
114
+ try:
115
+ await c.publish(topic="agent.report", to="manager", from_addr="subagent", payload=payload)
116
+ return
117
+ finally:
118
+ await c.close()
119
+ except Exception:
120
+ pass
121
+
122
+ try:
123
+ from gemcode.audit import append_audit
124
+
125
+ append_audit(fleet_root, {"event": "agent.report", "payload": payload})
126
+ except Exception:
127
+ return
128
+
129
+
73
130
  def make_run_subtask_tool(cfg: GemCodeConfig):
74
131
  async def run_subtask(task: str, context: str = "") -> dict:
75
132
  """
@@ -155,8 +212,15 @@ def make_run_subtask_tool(cfg: GemCodeConfig):
155
212
  sub_session_id = str(uuid.uuid4())
156
213
 
157
214
  # Compose the sub-agent prompt.
158
- task_clean = task.strip()
215
+ task_clean = (task or "").strip()
159
216
  ctx_clean = (context or "").strip()
217
+ await _publish_subtask_report(
218
+ cfg=cfg,
219
+ status="started",
220
+ task=task_clean,
221
+ context=ctx_clean,
222
+ sub_session_id=sub_session_id,
223
+ )
160
224
  prompt = task_clean
161
225
  if ctx_clean:
162
226
  prompt = f"{task_clean}\n\nAdditional context:\n{ctx_clean}"
@@ -188,6 +252,14 @@ def make_run_subtask_tool(cfg: GemCodeConfig):
188
252
  cfg=cfg,
189
253
  )
190
254
  except Exception as e:
255
+ await _publish_subtask_report(
256
+ cfg=cfg,
257
+ status="failed",
258
+ task=task_clean,
259
+ context=ctx_clean,
260
+ sub_session_id=sub_session_id,
261
+ error=f"{type(e).__name__}: {e}",
262
+ )
191
263
  return {"error": f"Sub-agent error: {type(e).__name__}: {e}"}
192
264
 
193
265
  # Extract only non-thinking text parts from the sub-agent's output.
@@ -226,6 +298,18 @@ def make_run_subtask_tool(cfg: GemCodeConfig):
226
298
  text=result_text,
227
299
  preview_max_chars=max_chars,
228
300
  )
301
+ await _publish_subtask_report(
302
+ cfg=cfg,
303
+ status="finished",
304
+ task=task_clean,
305
+ context=ctx_clean,
306
+ sub_session_id=sub_session_id,
307
+ result={
308
+ "offloaded": True,
309
+ "ref": ref_obj.get("ref"),
310
+ "preview": ref_obj.get("preview", "") or "",
311
+ },
312
+ )
229
313
  return {
230
314
  "result": ref_obj.get("preview", "") or "",
231
315
  "offloaded": True,
@@ -234,8 +318,24 @@ def make_run_subtask_tool(cfg: GemCodeConfig):
234
318
  }
235
319
  except Exception:
236
320
  result_text = result_text[:max_chars] + "\n… [truncated]"
321
+ await _publish_subtask_report(
322
+ cfg=cfg,
323
+ status="finished",
324
+ task=task_clean,
325
+ context=ctx_clean,
326
+ sub_session_id=sub_session_id,
327
+ result={"truncated": True, "result": result_text},
328
+ )
237
329
  return {"result": result_text, "truncated": True}
238
330
 
331
+ await _publish_subtask_report(
332
+ cfg=cfg,
333
+ status="finished",
334
+ task=task_clean,
335
+ context=ctx_clean,
336
+ sub_session_id=sub_session_id,
337
+ result={"result": result_text},
338
+ )
239
339
  return {"result": result_text}
240
340
 
241
341
  return run_subtask
@@ -446,6 +446,38 @@ async def run_gemcode_scrollback_tui(
446
446
  route = ""
447
447
  if from_addr or to:
448
448
  route = f" {from_addr or '?'}→{to or '*'}"
449
+
450
+ # Human-friendly rendering for common fleet-wide agent events.
451
+ try:
452
+ if topic in ("agent.report", "org.report") and isinstance(payload, dict):
453
+ status = str(payload.get("status") or "")
454
+ task = str(payload.get("task") or "").strip().replace("\n", " ")
455
+ task_short = (task[:160] + "…") if len(task) > 160 else task
456
+ if topic == "org.report":
457
+ mem = payload.get("member") or {}
458
+ mem_name = ""
459
+ try:
460
+ if isinstance(mem, dict):
461
+ mem_name = str(mem.get("name") or mem.get("id") or "")
462
+ except Exception:
463
+ mem_name = ""
464
+ jid = str(payload.get("job_id") or "")[:10]
465
+ prefix = f"{mem_name} " if mem_name else ""
466
+ extra = f" job={jid}" if jid else ""
467
+ await _kaira_print(
468
+ f"{ansi.dim}[{label}{route}]{ansi.reset} {prefix}{status}{extra} — {task_short}".rstrip()
469
+ )
470
+ continue
471
+ # agent.report
472
+ sid = str(payload.get("sub_session_id") or "")[:8]
473
+ extra = f" sub={sid}" if sid else ""
474
+ await _kaira_print(
475
+ f"{ansi.dim}[{label}{route}]{ansi.reset} {status}{extra} — {task_short}".rstrip()
476
+ )
477
+ continue
478
+ except Exception:
479
+ pass
480
+
449
481
  try:
450
482
  if isinstance(payload, (dict, list)):
451
483
  import json as _json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.114
3
+ Version: 0.3.116
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
File without changes
File without changes
File without changes
File without changes
File without changes