symphony-code 0.0.1__tar.gz → 0.0.2__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 (181) hide show
  1. {symphony_code-0.0.1 → symphony_code-0.0.2}/PKG-INFO +13 -7
  2. {symphony_code-0.0.1 → symphony_code-0.0.2}/README.md +12 -6
  3. {symphony_code-0.0.1 → symphony_code-0.0.2}/pyproject.toml +4 -4
  4. {symphony_code-0.0.1 → symphony_code-0.0.2}/pyproject.toml.orig +4 -4
  5. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/approvals.py +22 -2
  6. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/learning/store.py +10 -2
  7. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/persistence/active.py +37 -1
  8. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/persistence/jsonl.py +16 -2
  9. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/agent.py +7 -2
  10. symphony_code-0.0.2/src/coding_agent/cli.py +49 -0
  11. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/config/__init__.py +2 -0
  12. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/config/config.py +13 -0
  13. symphony_code-0.0.2/src/coding_agent/protocols/stdio/README.md +109 -0
  14. symphony_code-0.0.2/src/coding_agent/protocols/stdio/requests.py +112 -0
  15. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/protocols/stdio/server.py +45 -12
  16. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/protocols/stdio/transport.py +24 -9
  17. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/run/detach.py +1 -1
  18. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tools/ask_user.py +16 -3
  19. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tools/bash.py +83 -32
  20. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tools/patch.py +34 -2
  21. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tools/read_file.py +8 -0
  22. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tools/search.py +36 -10
  23. symphony_code-0.0.2/src/coding_agent/tools/summary.py +82 -0
  24. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/__main__.py +9 -27
  25. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/app.py +1 -0
  26. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/runtime/events.py +18 -4
  27. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/runtime/sink.py +7 -1
  28. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/runtime/subagent.py +4 -1
  29. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/screens/ask.py +3 -0
  30. symphony_code-0.0.2/src/coding_agent/tui/screens/update.py +72 -0
  31. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/tools/calls.py +143 -31
  32. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/tools/labels.py +10 -4
  33. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/transcript/surface.py +6 -3
  34. symphony_code-0.0.2/src/coding_agent/updates.py +264 -0
  35. symphony_code-0.0.2/src/coding_agent/web/__init__.py +1 -0
  36. symphony_code-0.0.2/src/coding_agent/web/bundle.py +267 -0
  37. symphony_code-0.0.2/src/coding_agent/web/server.py +363 -0
  38. symphony_code-0.0.2/src/coding_agent/web/sessions.py +306 -0
  39. symphony_code-0.0.2/src/coding_agent/web/worker.py +38 -0
  40. symphony_code-0.0.2/src/coding_agent/widget_fields.py +134 -0
  41. symphony_code-0.0.1/src/coding_agent/protocols/stdio/README.md +0 -36
  42. {symphony_code-0.0.1 → symphony_code-0.0.2}/LICENSE +0 -0
  43. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/__init__.py +0 -0
  44. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/_lazy.py +0 -0
  45. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/__init__.py +0 -0
  46. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/compaction/__init__.py +0 -0
  47. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/compaction/addon.py +0 -0
  48. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/compaction/compactor.py +0 -0
  49. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/compaction/prompts.py +0 -0
  50. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/compaction/transcript.py +0 -0
  51. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/evaluation/__init__.py +0 -0
  52. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/evaluation/addon.py +0 -0
  53. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/evaluation/evaluators.py +0 -0
  54. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/evaluation/handlers.py +0 -0
  55. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/evaluation/policy.py +0 -0
  56. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/evaluation/protocol.py +0 -0
  57. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/evaluation/state.py +0 -0
  58. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/langfuse/__init__.py +0 -0
  59. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/langfuse/addon.py +0 -0
  60. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/langfuse/install.py +0 -0
  61. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/langfuse/serialize.py +0 -0
  62. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/learning/__init__.py +0 -0
  63. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/learning/addon.py +0 -0
  64. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/learning/loop.py +0 -0
  65. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/learning/prompts.py +0 -0
  66. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/learning/sanitize.py +0 -0
  67. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/manager/__init__.py +0 -0
  68. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/manager/bridge.py +0 -0
  69. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/manager/discovery.py +0 -0
  70. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/manager/report.py +0 -0
  71. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/manager/runtime.py +0 -0
  72. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/manager/tool_changes.py +0 -0
  73. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/persistence/__init__.py +0 -0
  74. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/persistence/artifacts.py +0 -0
  75. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/persistence/collection.py +0 -0
  76. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/persistence/compatibility.py +0 -0
  77. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/persistence/images.py +0 -0
  78. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/persistence/presentation.py +0 -0
  79. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/plugins/__init__.py +0 -0
  80. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/plugins/install.py +0 -0
  81. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/plugins/manager.py +0 -0
  82. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/plugins/models.py +0 -0
  83. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/skills/__init__.py +0 -0
  84. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/skills/addon.py +0 -0
  85. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/skills/args.py +0 -0
  86. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/skills/bundled_skills/addon-authoring/SKILL.md +0 -0
  87. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/skills/bundled_skills/context-budget/SKILL.md +0 -0
  88. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/skills/bundled_skills/orient-first/SKILL.md +0 -0
  89. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/skills/bundled_skills/security-boundary/SKILL.md +0 -0
  90. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/skills/bundled_skills/skill-discipline/SKILL.md +0 -0
  91. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/skills/bundled_skills/surgical-edit/SKILL.md +0 -0
  92. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/skills/bundled_skills/systematic-debug/SKILL.md +0 -0
  93. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/skills/bundled_skills/verify-before-done/SKILL.md +0 -0
  94. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/skills/frontmatter.py +0 -0
  95. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/skills/models.py +0 -0
  96. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/skills/registry.py +0 -0
  97. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/skills/tool.py +0 -0
  98. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/voice/__init__.py +0 -0
  99. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/voice/addon.py +0 -0
  100. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/voice/credentials.py +0 -0
  101. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/voice/playback.py +0 -0
  102. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/voice/speak.py +0 -0
  103. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/voice/stream.py +0 -0
  104. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/addons/voice/summary.py +0 -0
  105. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/bench/__init__.py +0 -0
  106. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/bench/cli.py +0 -0
  107. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/bench/patch.py +0 -0
  108. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/commands/__init__.py +0 -0
  109. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/commands/catalog.py +0 -0
  110. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/commands/jev_argument.py +0 -0
  111. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/config/credentials.py +0 -0
  112. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/config/personalities.json +0 -0
  113. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/config/personalities.py +0 -0
  114. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/config/prompts.py +0 -0
  115. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/protocols/__init__.py +0 -0
  116. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/protocols/acp/README.md +0 -0
  117. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/protocols/acp/__init__.py +0 -0
  118. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/protocols/acp/mapping.py +0 -0
  119. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/protocols/acp/server.py +0 -0
  120. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/protocols/acp/transport.py +0 -0
  121. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/protocols/stdio/__init__.py +0 -0
  122. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/protocols/stdio/catalog.py +0 -0
  123. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/protocols/stdio/commands.py +0 -0
  124. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/py.typed +0 -0
  125. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/resources/__init__.py +0 -0
  126. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/resources/usage.py +0 -0
  127. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/run/__init__.py +0 -0
  128. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/run/cli.py +0 -0
  129. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/run/interrupted.py +0 -0
  130. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/run/log_sink.py +0 -0
  131. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tools/__init__.py +0 -0
  132. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tools/base.py +0 -0
  133. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tools/bash_jobs.py +0 -0
  134. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tools/generate_image.py +0 -0
  135. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tools/memory.py +0 -0
  136. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tools/web_search.py +0 -0
  137. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tools/write_file.py +0 -0
  138. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/EFFICIENCY_PLAN.md +0 -0
  139. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/__init__.py +0 -0
  140. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/chrome/__init__.py +0 -0
  141. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/chrome/footer.py +0 -0
  142. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/chrome/topbar.py +0 -0
  143. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/commands/__init__.py +0 -0
  144. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/commands/catalog.py +0 -0
  145. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/commands/manager.py +0 -0
  146. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/composer/__init__.py +0 -0
  147. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/composer/input.py +0 -0
  148. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/composer/slash_menu.py +0 -0
  149. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/composer/surface.py +0 -0
  150. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/composer/voice.py +0 -0
  151. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/composer/voice_mode.py +0 -0
  152. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/runtime/__init__.py +0 -0
  153. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/runtime/state.py +0 -0
  154. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/runtime/turn.py +0 -0
  155. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/screens/__init__.py +0 -0
  156. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/screens/context.py +0 -0
  157. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/screens/dashboard.py +0 -0
  158. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/screens/diff.py +0 -0
  159. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/screens/extensions.py +0 -0
  160. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/screens/file_selector.py +0 -0
  161. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/screens/history.py +0 -0
  162. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/screens/langfuse.py +0 -0
  163. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/screens/learning.py +0 -0
  164. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/screens/modal.py +0 -0
  165. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/screens/onboard.py +0 -0
  166. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/screens/resume.py +0 -0
  167. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/theme/__init__.py +0 -0
  168. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/theme/load.py +0 -0
  169. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/theme/style.py +0 -0
  170. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/theme/theme.toml +0 -0
  171. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/tools/__init__.py +0 -0
  172. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/tools/activity.py +0 -0
  173. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/tools/code_view.py +0 -0
  174. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/tools/images.py +0 -0
  175. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/tools/modal.py +0 -0
  176. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/tools/partial_args.py +0 -0
  177. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/tools/snapshots.py +0 -0
  178. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/transcript/__init__.py +0 -0
  179. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/transcript/messages.py +0 -0
  180. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/transcript/process.py +0 -0
  181. {symphony_code-0.0.1 → symphony_code-0.0.2}/src/coding_agent/tui/transcript/thinking_markdown.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: symphony-code
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: Symphony coding agent: workspace tools, JSONL sessions, and a Textual TUI on top of symphony-harness (import as coding_agent; run as `symphony`)
5
5
  Author: Sarthak Chauhan
6
6
  Author-email: Sarthak Chauhan <sarthakchauhann@gmail.com>
@@ -27,15 +27,15 @@ Description-Content-Type: text/markdown
27
27
 
28
28
  Run an AI coding agent in your terminal, or import the coding-agent API from the Symphony workspace. Package import name: `coding_agent`.
29
29
 
30
- **Availability:** this package is not currently published on PyPI. To run it today, use a checkout of the repository:
30
+ **Availability:** version 0.0.1 is on PyPI.
31
31
 
32
32
  ```sh
33
- git clone https://github.com/Sarthakischauhan/symphony.git
34
- cd symphony
35
- uv sync
36
- uv run --package symphony-code symphony --workspace /path/to/project
33
+ python -m pip install symphony-code
34
+ symphony --workspace /path/to/project
37
35
  ```
38
36
 
37
+ To run the current `main` branch instead of the published release, clone the repository, run `uv sync`, then `uv run --package symphony-code symphony --workspace /path/to/project`.
38
+
39
39
  `symphony`, `symphony-code`, and `coding-agent-tui` are aliases for the same terminal application. Configure a provider on first launch, or set credentials beforehand. See the [installation guide](../docs/getting-started/installation.md) and [quickstart](../docs/getting-started/quickstart.md).
40
40
 
41
41
  ## Use the library
@@ -63,4 +63,10 @@ print(result.output_text)
63
63
  - [Sessions and resume](../docs/user-guide/sessions.md)
64
64
  - [Package API guide](../docs/packages/symphony-code.md)
65
65
 
66
- **Safety:** tools run with the current user's permissions; approval prompts do not sandbox commands or restrict paths. Read the [security policy](../SECURITY.md) before use.
66
+ **Safety:** tools run with the current user's permissions; approval prompts do not sandbox commands or restrict paths. Read the [security policy](../SECURITY.md) before use.
67
+
68
+ ### Browser interface
69
+
70
+ Run `symphony web --workspace /path/to/project` to launch Symphony Web. Installed
71
+ wheels include the React frontend; Node.js is not needed by end users. See the
72
+ [Web guide](../docs/developer-guide/web.md) for development and security details.
@@ -2,15 +2,15 @@
2
2
 
3
3
  Run an AI coding agent in your terminal, or import the coding-agent API from the Symphony workspace. Package import name: `coding_agent`.
4
4
 
5
- **Availability:** this package is not currently published on PyPI. To run it today, use a checkout of the repository:
5
+ **Availability:** version 0.0.1 is on PyPI.
6
6
 
7
7
  ```sh
8
- git clone https://github.com/Sarthakischauhan/symphony.git
9
- cd symphony
10
- uv sync
11
- uv run --package symphony-code symphony --workspace /path/to/project
8
+ python -m pip install symphony-code
9
+ symphony --workspace /path/to/project
12
10
  ```
13
11
 
12
+ To run the current `main` branch instead of the published release, clone the repository, run `uv sync`, then `uv run --package symphony-code symphony --workspace /path/to/project`.
13
+
14
14
  `symphony`, `symphony-code`, and `coding-agent-tui` are aliases for the same terminal application. Configure a provider on first launch, or set credentials beforehand. See the [installation guide](../docs/getting-started/installation.md) and [quickstart](../docs/getting-started/quickstart.md).
15
15
 
16
16
  ## Use the library
@@ -38,4 +38,10 @@ print(result.output_text)
38
38
  - [Sessions and resume](../docs/user-guide/sessions.md)
39
39
  - [Package API guide](../docs/packages/symphony-code.md)
40
40
 
41
- **Safety:** tools run with the current user's permissions; approval prompts do not sandbox commands or restrict paths. Read the [security policy](../SECURITY.md) before use.
41
+ **Safety:** tools run with the current user's permissions; approval prompts do not sandbox commands or restrict paths. Read the [security policy](../SECURITY.md) before use.
42
+
43
+ ### Browser interface
44
+
45
+ Run `symphony web --workspace /path/to/project` to launch Symphony Web. Installed
46
+ wheels include the React frontend; Node.js is not needed by end users. See the
47
+ [Web guide](../docs/developer-guide/web.md) for development and security details.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "symphony-code"
3
- version = "0.0.1"
3
+ version = "0.0.2"
4
4
  description = "Symphony coding agent: workspace tools, JSONL sessions, and a Textual TUI on top of symphony-harness (import as coding_agent; run as `symphony`)"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -31,9 +31,9 @@ Documentation = "https://github.com/Sarthakischauhan/symphony/blob/main/docs/pac
31
31
  Source = "https://github.com/Sarthakischauhan/symphony/tree/main/coding_agent"
32
32
 
33
33
  [project.scripts]
34
- symphony = "coding_agent.tui.__main__:main"
35
- symphony-code = "coding_agent.tui.__main__:main"
36
- coding-agent-tui = "coding_agent.tui.__main__:main"
34
+ symphony = "coding_agent.cli:main"
35
+ symphony-code = "coding_agent.cli:main"
36
+ coding-agent-tui = "coding_agent.cli:main"
37
37
 
38
38
  [tool.uv.sources.symphony-core]
39
39
  workspace = true
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "symphony-code"
3
- version = "0.0.1"
3
+ version = "0.0.2"
4
4
  description = "Symphony coding agent: workspace tools, JSONL sessions, and a Textual TUI on top of symphony-harness (import as coding_agent; run as `symphony`)"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -31,9 +31,9 @@ Source = "https://github.com/Sarthakischauhan/symphony/tree/main/coding_agent"
31
31
  [project.scripts]
32
32
  # `symphony` is the primary command; the other two are aliases kept for
33
33
  # compatibility and all resolve to the same entry point.
34
- symphony = "coding_agent.tui.__main__:main"
35
- symphony-code = "coding_agent.tui.__main__:main"
36
- coding-agent-tui = "coding_agent.tui.__main__:main"
34
+ symphony = "coding_agent.cli:main"
35
+ symphony-code = "coding_agent.cli:main"
36
+ coding-agent-tui = "coding_agent.cli:main"
37
37
 
38
38
  [tool.uv.sources]
39
39
  symphony-core = { workspace = true }
@@ -16,6 +16,7 @@ from pathlib import Path
16
16
  from typing import Any, Dict, Optional, Sequence, Union
17
17
 
18
18
  from core_harness import Addon, EventSink
19
+ from core_harness.models import ToolCall
19
20
 
20
21
  from coding_agent.config import ApprovalConfig, ensure_spawn_settings
21
22
 
@@ -52,6 +53,22 @@ def matching_rule(
52
53
  return next((p for p in patterns if any(fnmatchcase(s, p) for s in subjects)), None)
53
54
 
54
55
 
56
+ def approval_metadata(tool_name: str, arguments: Dict[str, Any], question: str, call_id: str) -> Dict[str, Any]:
57
+ """What an approval card shows: the subject, the question and the size of the write."""
58
+ field = RULE_SUBJECTS.get(tool_name)
59
+ target = str(arguments.get(field) or "").strip() if field else ""
60
+ written = arguments.get("content") if tool_name == "write_file" else (
61
+ arguments.get("new_str") if tool_name == "patch" else None
62
+ )
63
+ return {
64
+ "tool_name": tool_name,
65
+ "tool_call_id": call_id,
66
+ "target": target,
67
+ "detail": question,
68
+ "bytes": len(written.encode("utf-8")) if isinstance(written, str) else None,
69
+ }
70
+
71
+
55
72
  class ApprovalPolicy:
56
73
  """Path-aware rules. Callers pass the current ``ApprovalConfig``."""
57
74
 
@@ -160,7 +177,7 @@ class ApprovalAddon(Addon):
160
177
  arguments: Dict[str, Any],
161
178
  sink: Optional[EventSink] = None,
162
179
  emit: Any = None,
163
- tool_call: Any = None,
180
+ tool_call: Optional[ToolCall] = None,
164
181
  **_: Any,
165
182
  ) -> Optional[str]:
166
183
  approvals = self.approvals
@@ -187,7 +204,9 @@ class ApprovalAddon(Addon):
187
204
  choices=APPROVAL_CHOICES,
188
205
  default=ALLOW_ONCE,
189
206
  kind="approval",
190
- metadata={"tool_name": tool_name, "tool_call_id": str(getattr(tool_call, "id", "") or "")},
207
+ metadata=approval_metadata(tool_name, arguments, prompt, tool_call.id if tool_call is not None else ""),
208
+ # The harness emit journals the question, so history keeps target, detail and bytes.
209
+ emit=emit,
191
210
  )
192
211
  allowed, promote = self.policy.interpret(approvals, answer)
193
212
  if promote:
@@ -219,6 +238,7 @@ __all__ = [
219
238
  "AUTO_DECISION",
220
239
  "ApprovalAddon",
221
240
  "ApprovalPolicy",
241
+ "approval_metadata",
222
242
  "DENY",
223
243
  "matching_rule",
224
244
  ]
@@ -82,7 +82,11 @@ class LearningStore:
82
82
  else:
83
83
  self.memory_dir = safe_path(self.workspace, ".symphony/memories")
84
84
  # Do not resolve away symlinks before validating paths.
85
- self.global_root = Path(global_root).absolute() if global_root is not None else safe_path(Path.home(), ".symphony/memories/global")
85
+ override = os.environ.get("SYMPHONY_HOME", "").strip()
86
+ # Keep the unresolved path so the ancestor symlink checks below still
87
+ # protect memory roots, including a configured Symphony home.
88
+ global_home = Path(override).expanduser().absolute() if override else Path.home() / ".symphony"
89
+ self.global_root = Path(global_root).absolute() if global_root is not None else safe_path(global_home, "memories/global")
86
90
  self.roots = {"workspace": self.memory_dir, "global": self.global_root}
87
91
  self.path = safe_path(self.memory_dir, "lessons.jsonl")
88
92
  # Markdown fact files. These are the session (and global) record.
@@ -93,8 +97,12 @@ class LearningStore:
93
97
  self._lock_depth = 0
94
98
  for root in self.roots.values():
95
99
  # Validate every ancestor, including a supplied global root.
100
+ # macOS system directories such as /var may themselves be symlinks;
101
+ # those are outside the configured home and are not attacker-created
102
+ # memory roots. Reject the root and every ancestor at or below home.
103
+ home = Path.home().absolute()
96
104
  for parent in (root, *root.parents):
97
- if parent.is_symlink():
105
+ if parent.is_symlink() and (parent == root or parent == home or home in parent.parents):
98
106
  raise ValueError("unsafe memory root")
99
107
  for relative in ("topics", "observations/_inbox", "observations/archive", "jobs"):
100
108
  safe_path(root, relative).mkdir(parents=True, exist_ok=True)
@@ -7,6 +7,7 @@ removes it on a clean exit. A file whose process is gone is stale and ignored.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import hashlib
10
11
  import json
11
12
  import os
12
13
  from dataclasses import dataclass
@@ -16,7 +17,9 @@ from typing import Any
16
17
 
17
18
  def active_dir() -> Path:
18
19
  """Directory that holds one registry file per live Symphony process."""
19
- target = (Path.home() / ".symphony" / "active").expanduser()
20
+ from coding_agent.config import symphony_dir
21
+
22
+ target = symphony_dir() / "active"
20
23
  target.mkdir(parents=True, exist_ok=True)
21
24
  return target
22
25
 
@@ -47,6 +50,32 @@ class ActiveSession:
47
50
  }
48
51
 
49
52
 
53
+ # Advisory lifetime locks coordinate all local hosts, including the TUI. Registry
54
+ # files alone have a check-then-write race. The kernel releases locks on crashes.
55
+ _owned_lock = None
56
+ _owned_session = None
57
+
58
+
59
+ def _claim(session_id: str) -> None:
60
+ global _owned_lock, _owned_session
61
+ import fcntl
62
+
63
+ if _owned_session == session_id:
64
+ return
65
+ digest = hashlib.sha256(session_id.encode()).hexdigest()
66
+ handle = (active_dir() / f"{digest}.lock").open("a")
67
+ try:
68
+ fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
69
+ if any(item.session_id == session_id and item.pid != os.getpid() for item in list_active()):
70
+ raise RuntimeError("Session is owned by another live Symphony process")
71
+ except (OSError, RuntimeError) as exc:
72
+ handle.close()
73
+ raise RuntimeError("Session is owned by another live Symphony process") from exc
74
+ if _owned_lock is not None:
75
+ _owned_lock.close()
76
+ _owned_lock, _owned_session = handle, session_id
77
+
78
+
50
79
  def register_active(
51
80
  session_id: str,
52
81
  *,
@@ -57,6 +86,8 @@ def register_active(
57
86
  pid: int | None = None,
58
87
  ) -> ActiveSession:
59
88
  """Record that this process owns ``session_id``. Replaces any prior record."""
89
+ if pid is None or pid == os.getpid():
90
+ _claim(session_id)
60
91
  record = ActiveSession(
61
92
  session_id=session_id,
62
93
  pid=os.getpid() if pid is None else pid,
@@ -74,6 +105,11 @@ def register_active(
74
105
 
75
106
  def release_active(pid: int | None = None) -> None:
76
107
  """Drop this process's registry file. Missing files are fine."""
108
+ global _owned_lock, _owned_session
109
+ if pid is None or pid == os.getpid():
110
+ if _owned_lock is not None:
111
+ _owned_lock.close()
112
+ _owned_lock = _owned_session = None
77
113
  path = _path(os.getpid() if pid is None else pid)
78
114
  try:
79
115
  path.unlink()
@@ -19,7 +19,7 @@ from contextlib import contextmanager
19
19
  from dataclasses import dataclass
20
20
  from datetime import datetime, timezone
21
21
  from pathlib import Path
22
- from typing import Any, Callable, Dict, Iterator, List, Mapping, Optional, Sequence, Union
22
+ from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Mapping, Optional, Sequence, Union
23
23
 
24
24
  from coding_agent.addons.persistence.artifacts import (
25
25
  append_bytes,
@@ -42,6 +42,10 @@ from coding_agent.addons.persistence.collection import (
42
42
  )
43
43
 
44
44
  _SESSION_ID = re.compile(r"^[A-Za-z0-9._-]+$")
45
+
46
+ if TYPE_CHECKING:
47
+ from coding_agent.widget_fields import EventFields
48
+
45
49
  _SKIP_EVENTS = frozenset({"text_delta", "reasoning_delta", "tool_call_delta"})
46
50
  _MESSAGE_FIELDS = ("role", "content", "tool_calls", "tool_call_id", "tool_call_metadata")
47
51
  _MESSAGE_TYPES = frozenset({"system", "user", "assistant", "tool_result", "message"})
@@ -90,7 +94,9 @@ def sessions_dir(
90
94
  first use. Migration is file-by-file and only removes the legacy directory
91
95
  after every move has succeeded, so an interrupted migration remains resumable.
92
96
  """
93
- root = (Path.home() / ".symphony").expanduser().resolve()
97
+ from coding_agent.config import symphony_dir
98
+
99
+ root = symphony_dir()
94
100
  if client == "tui":
95
101
  target = root / "tui" / "sessions"
96
102
  shared = root / "sessions"
@@ -323,6 +329,9 @@ class JsonlPersistence:
323
329
  # Product fields (goal, todo, live background jobs) merged into every
324
330
  # checkpoint so an interrupted run can be continued later.
325
331
  self.checkpoint_metadata: Callable[[], Dict[str, Any]] = dict
332
+ # Protocol-3 widget fields journaled with each event, so reloads and
333
+ # session/history keep them. ``build_agent`` sets it.
334
+ self.fields: EventFields | None = None
326
335
 
327
336
  def session_dir(self, session_id: str) -> Path:
328
337
  """Return a validated session artifact directory (without creating it)."""
@@ -858,6 +867,11 @@ class JsonlPersistence:
858
867
  async def append_event(self, *, event_type: str, payload: dict[str, Any]) -> None:
859
868
  if event_type in _SKIP_EVENTS:
860
869
  return
870
+ fields = self.fields
871
+ for name, body in fields.apply(event_type, payload) if fields is not None else [(event_type, payload)]:
872
+ self._journal(name, body)
873
+
874
+ def _journal(self, event_type: str, payload: dict[str, Any]) -> None:
861
875
  session_id = str(payload.get("session_id") or "")
862
876
  if not session_id:
863
877
  return
@@ -53,6 +53,7 @@ from coding_agent.config.prompts import SYSTEM_PROMPT
53
53
  from coding_agent.addons.skills import SkillRegistry, bundled_skills_root
54
54
  from coding_agent.addons.manager import UserAddonRuntime, load_user_addons
55
55
  from core_harness.extensions import EMPTY_SNAPSHOT
56
+ from coding_agent.widget_fields import EventFields
56
57
  from coding_agent.tools import BashJobs, build_tools
57
58
 
58
59
 
@@ -510,6 +511,7 @@ def build_agent(
510
511
  config=loaded,
511
512
  overrides=overrides or None,
512
513
  )
514
+ store = JsonlPersistence(sessions_dir(workspace, client=client))
513
515
  agent = CodingAgent(
514
516
  registry=registry,
515
517
  model_id=default_model_id(registry, model_id or resolved.last_model),
@@ -517,7 +519,10 @@ def build_agent(
517
519
  sink=sink,
518
520
  session_id=session_id,
519
521
  config=resolved,
520
- persistence=JsonlPersistence(sessions_dir(workspace, client=client)),
522
+ persistence=store,
521
523
  )
522
- agent.persistence.client = client
524
+ store.client = client
525
+ # Journal the same widget fields the hosts add (protocol 3).
526
+ store.fields = EventFields()
527
+ store.fields.bind(agent)
523
528
  return agent
@@ -0,0 +1,49 @@
1
+ """The ``symphony`` command: route subcommands, else open the TUI.
2
+
3
+ Each subcommand is imported only when chosen, so ``web``, ``stdio``, ``acp``,
4
+ ``bench`` and ``run`` never load Textual or ``coding_agent.tui``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+
11
+
12
+ def main() -> None:
13
+ command = sys.argv[1] if len(sys.argv) > 1 else None
14
+ if command in {"--version", "-V"}:
15
+ from importlib.metadata import PackageNotFoundError, version
16
+
17
+ try:
18
+ value = version("symphony-code")
19
+ except PackageNotFoundError:
20
+ value = "unknown"
21
+ print(f"symphony {value}")
22
+ return
23
+ if command == "web":
24
+ from coding_agent.web.server import main as web_main
25
+
26
+ raise SystemExit(web_main(sys.argv[2:]))
27
+ if command == "stdio":
28
+ from coding_agent.protocols.stdio.server import main as stdio_main
29
+
30
+ raise SystemExit(stdio_main(sys.argv[2:]))
31
+ if command == "acp":
32
+ from coding_agent.protocols.acp.server import main as acp_main
33
+
34
+ raise SystemExit(acp_main(sys.argv[2:]))
35
+ if command == "bench":
36
+ from coding_agent.bench.cli import main as bench_main
37
+
38
+ raise SystemExit(bench_main(sys.argv[2:]))
39
+ if command == "run":
40
+ from coding_agent.run.cli import main as run_main
41
+
42
+ raise SystemExit(run_main(sys.argv[2:]))
43
+ from coding_agent.tui.__main__ import main as tui_main
44
+
45
+ tui_main()
46
+
47
+
48
+ if __name__ == "__main__":
49
+ main()
@@ -16,6 +16,7 @@ from coding_agent.config.config import (
16
16
  SkillsConfig,
17
17
  SettingsSource,
18
18
  ToolsConfig,
19
+ UpdatesConfig,
19
20
  WebSearchConfig,
20
21
  default_coding_agent_harness,
21
22
  ensure_spawn_settings,
@@ -41,6 +42,7 @@ __all__ = [
41
42
  "SkillsConfig",
42
43
  "SettingsSource",
43
44
  "ToolsConfig",
45
+ "UpdatesConfig",
44
46
  "WebSearchConfig",
45
47
  "default_coding_agent_harness",
46
48
  "ensure_spawn_settings",
@@ -182,6 +182,17 @@ def default_coding_agent_harness() -> HarnessConfig:
182
182
  )
183
183
 
184
184
 
185
+ class UpdatesConfig(BaseModel):
186
+ """Pre-start comparison of the installed coding agent with its PyPI release."""
187
+
188
+ model_config = ConfigDict(extra="forbid")
189
+
190
+ enabled: bool = True
191
+ # False asks before replacing the installed package. True installs the
192
+ # newer published release without a prompt.
193
+ auto: bool = False
194
+
195
+
185
196
  class CodingAgentConfig(BaseModel):
186
197
  model_config = ConfigDict(extra="forbid")
187
198
 
@@ -194,6 +205,7 @@ class CodingAgentConfig(BaseModel):
194
205
  plugins: PluginsConfig = Field(default_factory=PluginsConfig)
195
206
  langfuse: LangfuseConfig = Field(default_factory=LangfuseConfig)
196
207
  evaluation: EvaluationConfig = Field(default_factory=EvaluationConfig)
208
+ updates: UpdatesConfig = Field(default_factory=UpdatesConfig)
197
209
  last_model: str | None = None
198
210
  # Default "direct" inserts that catalog segment. null = stock prompt only.
199
211
  personality: str | None = "direct"
@@ -368,6 +380,7 @@ __all__ = [
368
380
  "SettingsSource",
369
381
  "SkillsConfig",
370
382
  "ToolsConfig",
383
+ "UpdatesConfig",
371
384
  "WebSearchConfig",
372
385
  "ensure_spawn_settings",
373
386
  "load_coding_agent_config",
@@ -0,0 +1,109 @@
1
+ # Coding agent host protocols
2
+
3
+ `stdio.py` is the versioned JSONL boundary for desktop and other external
4
+ hosts. The coding agent owns execution and persisted checkpoints. A host owns
5
+ its composer queue and submits a single prompt per active turn.
6
+
7
+ The `ready` frame carries `session_id`, `model`, `workspace`, `branch`,
8
+ `client`, `protocol_version` (currently `3`) and `capabilities`. Protocol 2
9
+ advertised `runs`, `resume`, `interrupt`, `input`, `steer`, `models`, `images`,
10
+ `subagents`, and `commands`; protocol 3 adds `sessions`, `model_switch` and
11
+ `widget_fields`. A host sends `run`, `answer`, `interrupt`, or `steer`
12
+ commands, or a request with a `request_id` (below), and reads `event`,
13
+ `input_requested`, `error`, `done`, and reply frames. `answer`, `interrupt`,
14
+ and `steer` are passed to `CoreHarness.submit` as `Answer`, `Interrupt`, and
15
+ `UserInput` operations. `{"type":"steer","prompt":"..."}` adds a user message
16
+ to the active run before its next model call and is an error when no run is
17
+ active.
18
+
19
+ A question arrives as the ordinary `question_asked` event, the same as in
20
+ every other host, followed by the protocol 2 `input_requested` frame with the
21
+ same `request_id`. Reply to either with one `answer` command.
22
+ Unknown control-plane events can be ignored without losing the terminal
23
+ `done` frame. A later version must change the version number when it changes
24
+ required frame semantics.
25
+
26
+ After `ready`, send `{"type":"model/list","request_id":"catalog-1"}` on stdin.
27
+ Symphony responds with a `models` frame carrying the same `request_id`. Each
28
+ row has a qualified
29
+ `id` (`provider:model`), display `label`, `provider`, `description`,
30
+ `context_limit` (tokens, or null), and `reasoning_levels`. The default ID is
31
+ also returned as `default`; hosts should keep the qualified ID when selecting
32
+ a model. The provider is the API route, so gateway models identify the gateway
33
+ even when its upstream model has another vendor.
34
+
35
+ ## Requests
36
+
37
+ Each request carries a `request_id` and gets exactly one reply frame with the
38
+ same `request_id`, or an `error` frame with it.
39
+
40
+ | Request | Reply | Capability |
41
+ | --- | --- | --- |
42
+ | `{"type":"model/list"}` | `models`: `models` rows and `default` | `models` |
43
+ | `{"type":"command/list"}` | `commands`: the slash-command catalog | `commands` |
44
+ | `{"type":"session/list"}` | `sessions`: this client's parent sessions, newest first, each with `session_id`, `updated_at`, `message_count`, `first_message`, `client`, `active` | `sessions` |
45
+ | `{"type":"session/history","session_id":"..."}` | `session_history`: `session_id`, `messages` (the transcript; assistant `tool_calls` are `{id, type: "function", function: {name, arguments}}` with `arguments` a JSON string), `events` (`[event, payload]` journal pairs) and `children` | `sessions` |
46
+ | `{"type":"model","model":"provider:model"}` | `model_selected`: `model`, `context_limit`; an error during an active run | `model_switch` |
47
+
48
+ ## Subagents
49
+
50
+ Subagents use the ordinary `event` envelope: the parent emits
51
+ `agent_spawned` with `child_id` and `tool_call_id`; child events carry that
52
+ `child_id` as `agent_id`; the parent emits `agent_completed` or `agent_failed`
53
+ with `child_id` when it settles. Hosts can route child traffic into a nested
54
+ view using the spawning tool call ID.
55
+
56
+ ## Protocol 3: widget fields
57
+
58
+ `ready` reports `"protocol_version": 3` and the `widget_fields` capability. Every
59
+ protocol 2 key is unchanged; hosts that ignore unknown keys keep working.
60
+ The stdio server and the TUI add these fields through one shared
61
+ `coding_agent.widget_fields.EventFields`. `parent_call_id` is the
62
+ `tool_call_id` of the `spawn_agent` call that started the emitting subagent,
63
+ and `null` for top-level events.
64
+
65
+ | Event | Added fields |
66
+ | --- | --- |
67
+ | `tool_execution_started` | `call_id`, `parent_call_id` (`started_at` was already present) |
68
+ | `tool_execution_output` | `call_id`, `stream` (`"stdout"` or `"stderr"`), `parent_call_id` (from the call's `tool_execution_started`) |
69
+ | `tool_execution_completed` | `call_id`, `exit_code`, `summary`, `search_results`, `parent_call_id` (`status`, `result` and `duration_ms` were already present) |
70
+ | `question_asked` (approval) | `target`, `detail`, `bytes` (`tool_call_id`, `choices`, `default` were already present) |
71
+ | `question_asked` (`ask_user`) | `tool_call_id`: the asking call |
72
+ | `input_requested` (approval) | top-level `target`, `detail`, `bytes` (also kept in `metadata`) |
73
+ | `agent_spawned` | `parent_call_id` (same value as `tool_call_id`) |
74
+ | `subagent_step` (new, derived) | `session_id` (the parent session), `agent_id`, `parent_id`, `parent_call_id`, `turn`, `started_at`; follows each subagent `turn_started` |
75
+
76
+ - `exit_code` is the bash process's exit status; it is `null` for other tools,
77
+ background jobs, and timeouts. A non-zero exit is `status: "error"` and the
78
+ result starts with `exit=N`, but consumers should read `exit_code`.
79
+ - `summary` is `{lines, matches, added, removed, start_line}`, each an
80
+ integer or `null`: `lines` for bash and read_file, `matches` for search,
81
+ `lines` and `added` for write_file, `added`, `removed` and `start_line` for
82
+ patch, and `start_line` for read_file too. Failed non-bash calls report all
83
+ `null`.
84
+ - `start_line` is recorded per call by the tool, never parsed from `result`.
85
+ For patch it is the 1-based line, in the file as it was before the edit,
86
+ where `old_str` first matched. For read_file it is the number of the first
87
+ line of a text read (its `offset`); each result line is that number plus its
88
+ index, right-aligned to one width, then `|`. It is `null` for image reads
89
+ and reads that returned no lines.
90
+ - `search_results` (search only, else `null`) lists the search tool's results
91
+ in order: `{path, line, text}`, where `line` is `null` and `text` is empty
92
+ in files mode. `summary.matches` is its length.
93
+ - Answer a question or an approval with the `answer` command:
94
+ `{"type":"answer","request_id":"...","value":"<one of choices>"}`.
95
+ - `bytes` is the UTF-8 size of a write_file `content` or patch `new_str`, else `null`.
96
+ - The session journal stores the same fields: `build_agent` gives the
97
+ JSONL store its own `EventFields`, approval questions are emitted through
98
+ the harness so they are journaled, and `subagent_step` is journaled in the
99
+ parent session. `session/history` events and resumed sessions therefore
100
+ carry `call_id`, `parent_call_id`, `duration_ms`, `exit_code`, `summary`,
101
+ `search_results`,
102
+ the approval `target`/`detail`/`bytes`, and `subagent_step`.
103
+ - `tool_execution_output` chunks are live only. Bash emits them on the sink,
104
+ outside `CoreHarness.emit`, so they are not stamped or journaled; the
105
+ completed event's `result` is the stored output. Journaling `stream` per
106
+ chunk needs a core change: route chunks through `CoreHarness.emit` (or hand
107
+ tools a stamped emit).
108
+ - Sessions recorded before protocol 3 lack these fields. Clients render those
109
+ calls without the facts; they do not derive them from `result` text.