agentproc 0.7.1__tar.gz → 0.9.0__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 (93) hide show
  1. {agentproc-0.7.1 → agentproc-0.9.0}/PKG-INFO +1 -1
  2. {agentproc-0.7.1 → agentproc-0.9.0}/pyproject.toml +1 -1
  3. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/__init__.py +69 -16
  4. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/cli.py +1 -1
  5. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/README.md +5 -5
  6. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/_shared/README.md +9 -7
  7. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/_shared/stream_utils.js +31 -29
  8. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/_shared/stream_utils.py +41 -37
  9. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/agy/README.md +4 -4
  10. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/agy/bridge.js +1 -1
  11. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/agy/bridge.py +1 -1
  12. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/agy/profile.yaml +1 -1
  13. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/aider/README.md +3 -3
  14. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/aider/bridge.js +1 -1
  15. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/aider/bridge.py +1 -1
  16. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/claude-code/README.md +4 -4
  17. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/claude-code/bridge.js +10 -9
  18. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/claude-code/bridge.py +9 -12
  19. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/claude-code/profile.yaml +1 -1
  20. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codebuddy/README.md +3 -3
  21. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codex/README.md +3 -4
  22. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codex/bridge.js +6 -9
  23. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codex/bridge.py +6 -12
  24. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/cursor/README.md +9 -9
  25. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/cursor/bridge.py +1 -1
  26. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/cursor/profile.yaml +2 -2
  27. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/deepseek/README.md +5 -5
  28. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/deepseek/bridge.js +1 -1
  29. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/deepseek/bridge.py +1 -1
  30. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/deepseek/profile.yaml +2 -2
  31. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/echo-agent/README.md +3 -3
  32. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/echo-agent/bridge.js +4 -4
  33. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/echo-agent/bridge.py +3 -3
  34. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/echo-agent/bridge.sh +3 -3
  35. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/echo-agent/profile.yaml +2 -2
  36. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/gemini-cli/README.md +9 -9
  37. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/gemini-cli/bridge.py +3 -3
  38. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/gemini-cli/profile.yaml +1 -1
  39. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/kimi-code/README.md +7 -6
  40. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/kimi-code/bridge.js +3 -3
  41. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/kimi-code/bridge.py +3 -3
  42. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/kimi-code/profile.yaml +1 -1
  43. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/opencode/README.md +8 -8
  44. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/opencode/bridge.py +2 -2
  45. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/opencode/profile.yaml +1 -1
  46. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/pi/README.md +3 -3
  47. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/pi/bridge.js +1 -1
  48. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/pi/bridge.py +1 -1
  49. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/pi/profile.yaml +1 -1
  50. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/qwen-code/README.md +2 -2
  51. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/recursive/README.md +3 -3
  52. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/recursive/bridge.js +21 -23
  53. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/recursive/bridge.py +25 -24
  54. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/runner.py +162 -73
  55. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc.egg-info/PKG-INFO +1 -1
  56. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc.egg-info/SOURCES.txt +1 -0
  57. {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_agentproc.py +17 -18
  58. {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_bridges.py +23 -19
  59. agentproc-0.9.0/tests/test_hub_bridge_conformance.py +156 -0
  60. {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_runner.py +105 -58
  61. {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_scenarios.py +15 -5
  62. {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_sdk.py +5 -1
  63. {agentproc-0.7.1 → agentproc-0.9.0}/setup.cfg +0 -0
  64. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub.py +0 -0
  65. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/PERMISSIONS.md +0 -0
  66. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/aider/profile.yaml +0 -0
  67. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/claude-code/permission_map.test.js +0 -0
  68. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codebuddy/bridge.js +0 -0
  69. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codebuddy/bridge.py +0 -0
  70. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codebuddy/profile.yaml +0 -0
  71. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codex/permission_hook.py +0 -0
  72. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codex/permission_map.js +0 -0
  73. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codex/permission_map.test.js +0 -0
  74. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codex/profile.yaml +0 -0
  75. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/cursor/bridge.js +0 -0
  76. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/gemini-cli/bridge.js +0 -0
  77. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/opencode/bridge.js +0 -0
  78. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/qwen-code/bridge.js +0 -0
  79. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/qwen-code/bridge.py +0 -0
  80. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/qwen-code/profile.yaml +0 -0
  81. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/recursive/profile.yaml +0 -0
  82. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/recursive/tests/parity.json +0 -0
  83. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/recursive/tests/test_bridge_parity.js +0 -0
  84. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/recursive/tests/test_bridge_parity.py +0 -0
  85. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/yaml.py +0 -0
  86. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc.egg-info/dependency_links.txt +0 -0
  87. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc.egg-info/entry_points.txt +0 -0
  88. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc.egg-info/requires.txt +0 -0
  89. {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc.egg-info/top_level.txt +0 -0
  90. {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_conformance.py +0 -0
  91. {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_diagnostics.py +0 -0
  92. {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_hub.py +0 -0
  93. {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_yaml.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentproc
3
- Version: 0.7.1
3
+ Version: 0.9.0
4
4
  Summary: AgentProc Protocol SDK + CLI — connect any Agent CLI to a messaging platform
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/jeffkit/agentproc
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentproc"
7
- version = "0.7.1"
7
+ version = "0.9.0"
8
8
  description = "AgentProc Protocol SDK + CLI — connect any Agent CLI to a messaging platform"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -4,15 +4,15 @@ agentproc — AgentProc Protocol SDK (Python)
4
4
  Implements the AgentProc P0 protocol so you can write a single async handler
5
5
  instead of manually reading the turn from stdin and formatting stdout.
6
6
 
7
- Protocol contract (spec/protocol.md, wire protocol 0.3, NDJSON both directions):
7
+ Protocol contract (spec/protocol.md, wire protocol 0.4, NDJSON both directions):
8
8
  Input — stdin: one {"type":"turn",...} line (message, session_id,
9
9
  session_name, from_user, attachments, permission,
10
10
  protocol_version). Secrets/config stay in env.
11
11
  Output — stdout (one JSON object per line, discriminated by `type`):
12
12
  {"type":"partial","text":...} — streaming chunk
13
- {"type":"text","text":...} final reply body
14
- {"type":"session","id":...} — declare session id (last wins)
13
+ {"type":"result","text":...} terminal success body (at most one)
15
14
  {"type":"error","message":...} — error message to forward to user
15
+ Optional ``session_id`` field on events (bridge persists first non-empty).
16
16
  Exit — 0 success, 1 error, 124 timeout, 130 SIGINT, 143 SIGTERM
17
17
 
18
18
  Example::
@@ -166,6 +166,64 @@ class AgentContext:
166
166
  sys.stdout.flush()
167
167
  return _NoopAwaitable()
168
168
 
169
+ def send_permission_request(self, req: Dict[str, Any]) -> None:
170
+ """Send a tool-permission request to the bridge.
171
+
172
+ Only valid when ``ctx.permission`` is True (the profile set
173
+ ``permission: true`` and the bridge enabled the channel). The bridge
174
+ surfaces the request to the user; the matching decision arrives on
175
+ stdin as a ``{"type":"permission_response",...}`` frame that
176
+ :meth:`read_permission_response` decodes.
177
+
178
+ ``req`` MUST include ``request_id`` (unique within the turn),
179
+ ``tool_name``, and ``input`` (object). ``description`` and
180
+ ``tool_use_id`` are optional.
181
+ """
182
+ if not self.permission:
183
+ raise RuntimeError(
184
+ "ctx.send_permission_request() requires profile.permission: "
185
+ "true — the bridge would otherwise not honor the request"
186
+ )
187
+ if not isinstance(req, dict):
188
+ raise TypeError("req must be a dict")
189
+ rid = req.get("request_id")
190
+ if not isinstance(rid, str) or not rid:
191
+ raise ValueError("req['request_id'] is required")
192
+ payload: Dict[str, Any] = {"type": "permission_request"}
193
+ for k in ("request_id", "tool_name", "input"):
194
+ payload[k] = req.get(k)
195
+ for k in ("description", "tool_use_id"):
196
+ if k in req:
197
+ payload[k] = req[k]
198
+ sys.stdout.write(
199
+ json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n"
200
+ )
201
+ sys.stdout.flush()
202
+
203
+ def read_permission_response(self) -> Optional[Dict[str, Any]]:
204
+ """Read the next ``{"type":"permission_response",...}`` frame from stdin.
205
+
206
+ Blocks until a frame arrives (or EOF). Returns the parsed object, or
207
+ ``None`` at EOF.
208
+
209
+ Only meaningful when ``ctx.permission`` is True — in the default
210
+ ``permission: false`` mode the bridge closes stdin after the turn
211
+ line, so this returns ``None`` immediately.
212
+ """
213
+ try:
214
+ line = sys.stdin.readline()
215
+ except Exception:
216
+ return None
217
+ if not line:
218
+ return None
219
+ try:
220
+ v = json.loads(line.rstrip("\r\n"))
221
+ except json.JSONDecodeError:
222
+ return None
223
+ if isinstance(v, dict):
224
+ return v
225
+ return None
226
+
169
227
 
170
228
  @dataclass
171
229
  class AgentResult:
@@ -195,7 +253,7 @@ def session_file_path(session_id: str, base_dir: Optional[str] = None) -> Path:
195
253
  Returns the path even if the file does not yet exist. Raises ``ValueError``
196
254
  when ``session_id`` is empty — callers should guard with ``if session_id``.
197
255
 
198
- In wire 0.3 a session id is an arbitrary JSON string on the wire, but the
256
+ In wire 0.4 a session id is an arbitrary JSON string on the wire, but the
199
257
  SDK stores each session as ``<id>.jsonl``; an id that is not a storage-safe
200
258
  filename component (path separators, control chars, ``.``/``..``) would
201
259
  path-traverse out of the sessions directory and is rejected here. See
@@ -346,18 +404,13 @@ def create_profile(handler: Handler) -> None:
346
404
  if isinstance(result, str):
347
405
  result = AgentResult(response=result)
348
406
 
349
- # session event emitted last in the typical "I just learned it" flow,
350
- # but the spec says last wins, so emitting it at the end is correct.
351
- if result.session_id:
352
- sys.stdout.write(
353
- json.dumps({"type": "session", "id": result.session_id}, ensure_ascii=False, separators=(',', ':')) + "\n"
354
- )
355
- sys.stdout.flush()
356
-
357
- if result.response:
358
- sys.stdout.write(
359
- json.dumps({"type": "text", "text": result.response}, ensure_ascii=False, separators=(',', ':')) + "\n"
360
- )
407
+ # Emit a single {"type":"result"} carrying the reply body and optional
408
+ # session_id. Wire 0.4 removed dedicated session/text events.
409
+ if result.response or result.session_id:
410
+ evt: Dict[str, Any] = {"type": "result", "text": result.response or ""}
411
+ if result.session_id:
412
+ evt["session_id"] = result.session_id
413
+ sys.stdout.write(json.dumps(evt, ensure_ascii=False, separators=(',', ':')) + "\n")
361
414
  sys.stdout.flush()
362
415
 
363
416
  sys.exit(0)
@@ -58,7 +58,7 @@ PKG_VERSION = _read_pkg_version()
58
58
  def _build_attachments(opts) -> List[Dict[str, str]]:
59
59
  """Build the turn's `attachments` array from --image-url / --file-url.
60
60
 
61
- Wire 0.3 carries attachments as a single `attachments` list; there are no
61
+ Wire 0.4 carries attachments as a single `attachments` list; there are no
62
62
  separate AGENT_IMAGE_URL / AGENT_FILE_URL channels. Returns an empty list
63
63
  when no attachment flags were given (the runner omits the key when empty).
64
64
  """
@@ -74,18 +74,18 @@ NDJSON-based profiles (`claude-code`, `codex`, `codebuddy`, `gemini-cli`, `qwen-
74
74
  3. **Adjust** `cwd:` in the profile to point at your project.
75
75
  4. **Point your bridge** at the profile YAML. The exact command depends on your bridge.
76
76
 
77
- Example with the Python SDK's bare runner (wire 0.3 — the turn object arrives on stdin as one NDJSON line):
77
+ Example with the Python SDK's bare runner (wire 0.4 — the turn object arrives on stdin as one NDJSON line):
78
78
 
79
79
  ```bash
80
80
  cd hub/claude-code
81
- echo '{"type":"turn","message":"hello","session_id":"","from_user":"u1","protocol_version":"0.3"}' | python3 bridge.py
81
+ echo '{"type":"turn","message":"hello","session_id":"","from_user":"u1","protocol_version":"0.4"}' | python3 bridge.py
82
82
  ```
83
83
 
84
84
  You should see AgentProc protocol output on stdout (one NDJSON event per line):
85
85
 
86
86
  ```
87
- {"type":"partial","text":"Hi! How can I help?"}
88
- {"type":"session","id":"cli-sess-9f3a2c1e-4b8d-4a2f-b6c1-2e8d4f5a7b9c"}
87
+ {"type":"partial","text":"Hi! How can I help?","session_id":"cli-sess-9f3a2c1e-4b8d-4a2f-b6c1-2e8d4f5a7b9c"}
88
+ {"type":"result","text":"","session_id":"cli-sess-9f3a2c1e-4b8d-4a2f-b6c1-2e8d4f5a7b9c"}
89
89
  ```
90
90
 
91
91
  ## Design principles
@@ -116,7 +116,7 @@ description: <one-line>
116
116
  cli: <command-name> # the executable this wraps
117
117
  cli_install: | # how to install the CLI itself
118
118
  npm install -g ...
119
- agentproc: # the actual AgentProc P0 profile (wire 0.3)
119
+ agentproc: # the actual AgentProc P0 profile (wire 0.4)
120
120
  command: python3 # argv[0] — always a single token, never split
121
121
  args: ["{{PROFILE_DIR}}/bridge.py"] # argv[1..]; or: ["{{PROFILE_DIR}}/bridge.js"] for node
122
122
  # cwd intentionally omitted: `hub run` defaults it to the user's
@@ -1,6 +1,6 @@
1
1
  # hub/_shared
2
2
 
3
- Shared bridge utilities used by the hub profiles (wire 0.3). The Python and
3
+ Shared bridge utilities used by the hub profiles (wire 0.4). The Python and
4
4
  Node implementations stay at parity: same input contract, same output contract.
5
5
 
6
6
  ## What it does
@@ -11,9 +11,10 @@ NDJSON-speaking CLI:
11
11
  - reading the `{"type":"turn",...}` object from stdin (one NDJSON line)
12
12
  - subprocess spawn + `FileNotFoundError` → `{"type":"error"}`
13
13
  - line-by-line stdout reading + JSON decoding
14
- - partial / final / session / error classification → NDJSON events
14
+ - partial / result / error classification → NDJSON events (`session_id` stamped on events once known)
15
15
  - always-emit-partial policy (the runner forwards `{"type":"partial"}` only
16
- when the profile's `streaming` is true) + dedup
16
+ when the profile's `streaming` is true) + stamping `session_id` on partials
17
+ once known
17
18
  - exit-code mapping + stderr capture
18
19
 
19
20
  A bridge that wraps an NDJSON-speaking CLI only needs to provide two functions:
@@ -32,9 +33,10 @@ function parseEvent(event) { return { partialText?, finalText?, sessionId?, erro
32
33
 
33
34
  `run_bridge` / `runBridge` then: read the turn from stdin, spawn the CLI built
34
35
  by `build_args`, call `parse_event` on each stdout NDJSON line, and emit
35
- AgentProc NDJSON events on stdout. A final `{"type":"text"}` event is always
36
+ AgentProc NDJSON events on stdout. A final `{"type":"result"}` event is always
36
37
  emitted at the end (the `final_text`, or the last `partial_text` as fallback) —
37
- that is the reply body.
38
+ that is the reply body. `session_id` rides on partial / result / error events
39
+ (not a separate session event).
38
40
 
39
41
  That keeps each bridge under ~50 lines. See `gemini-cli/bridge.py` for a
40
42
  minimal example.
@@ -49,7 +51,7 @@ assembled" event that follows the delta stream — see `cursor/bridge.py`'s
49
51
  CLIs that return the full reply as plain stdout (no streaming, no session id) —
50
52
  `aider`, `pi`, `deepseek`, `agy` — use `run_plain_cli` / `runPlainCli` instead.
51
53
  It reads the turn, runs the CLI with a timeout, and emits the trimmed stdout as
52
- a single `{"type":"text"}` event (or `{"type":"error"}` on failure). The bridge
54
+ a single `{"type":"result"}` event (or `{"type":"error"}` on failure). The bridge
53
55
  only supplies `build_args(message)`.
54
56
 
55
57
  ## When NOT to use it
@@ -72,7 +74,7 @@ The shared module distinguishes two text channels:
72
74
  - `partial_text` — incremental chunks emitted mid-turn as `{"type":"partial"}`
73
75
  (the bridge always emits these; the runner forwards them only when the
74
76
  profile's `streaming` is true).
75
- - `final_text` — terminal text, emitted as the final `{"type":"text"}` reply
77
+ - `final_text` — terminal text, emitted as the final `{"type":"result"}` reply
76
78
  event (used as-is when present, otherwise the last `partial_text` is the
77
79
  fallback).
78
80
 
@@ -1,19 +1,20 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Shared bridge utilities for AgentProc hub profiles (wire 0.3).
4
+ * Shared bridge utilities for AgentProc hub profiles (wire 0.4).
5
5
  *
6
6
  * A bridge wraps a CLI that emits NDJSON (one JSON object per line) on stdout.
7
7
  * The bridge reads the {"type":"turn",...} object from its own stdin, spawns
8
- * the CLI, and translates the CLI's NDJSON stream into AgentProc wire-0.3
8
+ * the CLI, and translates the CLI's NDJSON stream into AgentProc wire-0.4
9
9
  * output (one JSON event per line on stdout):
10
10
  *
11
- * - {"type":"partial","text":...} live streaming chunk (always emitted;
12
- * the runner forwards it only when the
13
- * profile's streaming is true)
14
- * - {"type":"session","id":...} declare session id (last wins)
15
- * - {"type":"error","message":...} error message (always honored, exit 1)
16
- * - {"type":"text","text":...} final reply body (emitted once at end)
11
+ * - {"type":"partial","text":...,"session_id"?} live streaming chunk
12
+ * (always emitted; the runner forwards it only when the profile's
13
+ * streaming is true). session_id is stamped when already known.
14
+ * - {"type":"result","text":...,"session_id"?} single terminal reply
15
+ * (emitted once at end; text may be "" if the body was already streamed)
16
+ * - {"type":"error","message":...,"session_id"?} error (exit 1); may
17
+ * carry session_id so the session survives an error-terminated turn
17
18
  *
18
19
  * A profile supplies:
19
20
  *
@@ -35,24 +36,26 @@ function emitObj(obj) {
35
36
 
36
37
  function emit(obj) {
37
38
  // Emit one NDJSON event dict. Bridges may pass a pre-built event dict; for
38
- // the common cases use emitPartial / emitText / emitSession / emitError.
39
+ // the common cases use emitPartial / emitResult / emitError.
39
40
  emitObj(obj);
40
41
  }
41
42
 
42
- function emitPartial(text) {
43
- emitObj({ type: 'partial', text });
44
- }
45
-
46
- function emitText(text) {
47
- emitObj({ type: 'text', text });
43
+ function emitPartial(text, sessionId) {
44
+ const obj = { type: 'partial', text };
45
+ if (sessionId) obj.session_id = sessionId;
46
+ emitObj(obj);
48
47
  }
49
48
 
50
- function emitError(text) {
51
- emitObj({ type: 'error', message: text });
49
+ function emitResult(text, sessionId) {
50
+ const obj = { type: 'result', text };
51
+ if (sessionId) obj.session_id = sessionId;
52
+ emitObj(obj);
52
53
  }
53
54
 
54
- function emitSession(sessionId) {
55
- emitObj({ type: 'session', id: sessionId });
55
+ function emitError(text, sessionId) {
56
+ const obj = { type: 'error', message: text };
57
+ if (sessionId) obj.session_id = sessionId;
58
+ emitObj(obj);
56
59
  }
57
60
 
58
61
  function hasAnyAttachment(turn) {
@@ -129,15 +132,17 @@ async function runBridge({ cliName, cliInstallHint, buildArgs, parseEvent, turn
129
132
  const result = parseEvent(event);
130
133
  if (!result) continue;
131
134
 
135
+ // Capture sessionId before emitting partials so same-event session
136
+ // stamps the partial (runner first-non-empty wins if it arrives later).
132
137
  if (result.sessionId) foundSessionId = result.sessionId;
133
138
  if (result.error) errorMessage = result.error;
134
139
  if (result.partialText) {
135
140
  // Always emit partials; the runner forwards them only when the profile's
136
141
  // streaming is true (and drops them otherwise).
137
- emitPartial(result.partialText);
142
+ emitPartial(result.partialText, foundSessionId);
138
143
  lastPartialText = result.partialText;
139
144
  }
140
- if (result.finalText) {
145
+ if (result.finalText !== undefined && result.finalText !== null) {
141
146
  lastFinalText = result.finalText;
142
147
  }
143
148
  }
@@ -145,8 +150,7 @@ async function runBridge({ cliName, cliInstallHint, buildArgs, parseEvent, turn
145
150
  const code = await new Promise(resolve => child.on('close', resolve));
146
151
 
147
152
  if (errorMessage) {
148
- if (foundSessionId) emitSession(foundSessionId);
149
- emitError(errorMessage);
153
+ emitError(errorMessage, foundSessionId);
150
154
  process.exit(1);
151
155
  }
152
156
  if (code !== 0 && !foundSessionId) {
@@ -157,16 +161,15 @@ async function runBridge({ cliName, cliInstallHint, buildArgs, parseEvent, turn
157
161
  process.exit(1);
158
162
  }
159
163
 
160
- if (foundSessionId) emitSession(foundSessionId);
161
164
  const replyText = (lastFinalText !== null) ? lastFinalText : lastPartialText;
162
- if (replyText) emitText(replyText);
165
+ emitResult(replyText || '', foundSessionId);
163
166
  process.exit(0);
164
167
  }
165
168
 
166
169
  async function runPlainCli({ cliName, cliInstallHint, buildArgs, timeoutEnv = 'CLI_TIMEOUT', defaultTimeout = 600 }) {
167
170
  // Drive a one-shot CLI that returns the full reply as plain stdout text
168
171
  // (no streaming, no session id). Reads the turn from stdin, runs the CLI
169
- // with a timeout, and emits the trimmed stdout as a single {"type":"text"}
172
+ // with a timeout, and emits the trimmed stdout as a single {"type":"result"}
170
173
  // event (or {"type":"error"} on failure). buildArgs(message) builds the
171
174
  // argv; per-CLI config is read from process.env inside buildArgs.
172
175
  const turn = await readTurn();
@@ -214,7 +217,7 @@ async function runPlainCli({ cliName, cliInstallHint, buildArgs, timeoutEnv = 'C
214
217
  emitError(`${cliName} returned empty output`);
215
218
  process.exit(1);
216
219
  }
217
- emitText(text);
220
+ emitResult(text);
218
221
  process.exit(0);
219
222
  }
220
223
 
@@ -224,7 +227,6 @@ module.exports = {
224
227
  readTurn,
225
228
  emit,
226
229
  emitPartial,
227
- emitText,
230
+ emitResult,
228
231
  emitError,
229
- emitSession,
230
232
  };
@@ -1,17 +1,18 @@
1
1
  """
2
- Shared bridge utilities for AgentProc hub profiles (wire 0.3).
2
+ Shared bridge utilities for AgentProc hub profiles (wire 0.4).
3
3
 
4
4
  A bridge wraps a CLI that emits NDJSON (one JSON object per line) on stdout.
5
5
  The bridge reads the {"type":"turn",...} object from its own stdin, spawns the
6
- CLI, and translates the CLI's NDJSON stream into AgentProc wire-0.3 output
6
+ CLI, and translates the CLI's NDJSON stream into AgentProc wire-0.4 output
7
7
  (one JSON event per line on stdout):
8
8
 
9
- - {"type":"partial","text":...} — live streaming chunk (always emitted;
10
- the runner forwards it only when the
11
- profile's streaming is true)
12
- - {"type":"session","id":...} declare session id (last wins)
13
- - {"type":"error","message":...} — error message (always honored, exit 1)
14
- - {"type":"text","text":...} final reply body (emitted once at end)
9
+ - {"type":"partial","text":...,"session_id"?} — live streaming chunk
10
+ (always emitted; the runner forwards it only when the profile's
11
+ streaming is true). session_id is stamped when already known.
12
+ - {"type":"result","text":...,"session_id"?} single terminal reply
13
+ (emitted once at end; text may be "" if the body was already streamed)
14
+ - {"type":"error","message":...,"session_id"?} error (exit 1); may
15
+ carry session_id so the session survives an error-terminated turn
15
16
 
16
17
  A profile supplies:
17
18
 
@@ -40,7 +41,7 @@ from typing import Any, Callable, Dict, Optional
40
41
  class EventResult:
41
42
  # Incremental text streamed mid-turn → emitted as a {"type":"partial"} event.
42
43
  partial_text: Optional[str] = None
43
- # Terminal text — the final assembled reply → emitted as a {"type":"text"}
44
+ # Terminal text — the final assembled reply → emitted as a {"type":"result"}
44
45
  # event at end. When a CLI only produces a single completed message (no
45
46
  # deltas), set this; if a bridge marks it partial_text instead, the last
46
47
  # partial_text is used as the reply fallback.
@@ -56,25 +57,30 @@ def _emit_obj(obj: Dict[str, Any]) -> None:
56
57
 
57
58
  def emit(obj: Dict[str, Any]) -> None:
58
59
  """Emit one NDJSON event dict on stdout. (Bridges may pass a pre-built
59
- event dict; for the common cases use emit_partial / emit_text /
60
- emit_session / emit_error instead.)"""
60
+ event dict; for the common cases use emit_partial / emit_result /
61
+ emit_error instead.)"""
61
62
  _emit_obj(obj)
62
63
 
63
64
 
64
- def emit_partial(text: str) -> None:
65
- _emit_obj({"type": "partial", "text": text})
66
-
67
-
68
- def emit_text(text: str) -> None:
69
- _emit_obj({"type": "text", "text": text})
65
+ def emit_partial(text: str, session_id: Optional[str] = None) -> None:
66
+ obj: Dict[str, Any] = {"type": "partial", "text": text}
67
+ if session_id:
68
+ obj["session_id"] = session_id
69
+ _emit_obj(obj)
70
70
 
71
71
 
72
- def emit_session(session_id: str) -> None:
73
- _emit_obj({"type": "session", "id": session_id})
72
+ def emit_result(text: str, session_id: Optional[str] = None) -> None:
73
+ obj: Dict[str, Any] = {"type": "result", "text": text}
74
+ if session_id:
75
+ obj["session_id"] = session_id
76
+ _emit_obj(obj)
74
77
 
75
78
 
76
- def emit_error(text: str) -> None:
77
- _emit_obj({"type": "error", "message": text})
79
+ def emit_error(text: str, session_id: Optional[str] = None) -> None:
80
+ obj: Dict[str, Any] = {"type": "error", "message": text}
81
+ if session_id:
82
+ obj["session_id"] = session_id
83
+ _emit_obj(obj)
78
84
 
79
85
 
80
86
  def _read_turn() -> Dict[str, Any]:
@@ -87,10 +93,11 @@ def _read_turn() -> Dict[str, Any]:
87
93
  return {}
88
94
  try:
89
95
  v = json.loads(line.rstrip("\r\n"))
90
- if isinstance(v, dict):
91
- return v
92
96
  except json.JSONDecodeError:
93
97
  pass
98
+ else:
99
+ if isinstance(v, dict):
100
+ return v
94
101
  return {}
95
102
 
96
103
 
@@ -113,7 +120,7 @@ def run_bridge(
113
120
  turn: Optional[Dict[str, Any]] = None,
114
121
  ) -> int:
115
122
  """
116
- Drive a CLI as an AgentProc agent (wire 0.3).
123
+ Drive a CLI as an AgentProc agent (wire 0.4).
117
124
 
118
125
  Reads the turn object from stdin (unless ``turn`` is passed — used by
119
126
  bridges that need to inspect the turn, e.g. to refuse ``permission: true``
@@ -164,6 +171,8 @@ def run_bridge(
164
171
  if result is None:
165
172
  continue
166
173
 
174
+ # Capture session_id before emitting partials so same-event session
175
+ # stamps the partial (runner first-non-empty wins if it arrives later).
167
176
  if result.session_id:
168
177
  found_session_id = result.session_id
169
178
  if result.error:
@@ -171,20 +180,18 @@ def run_bridge(
171
180
  if result.partial_text:
172
181
  # Always emit partials; the runner forwards them only when the
173
182
  # profile's streaming is true (and drops them otherwise).
174
- emit_partial(result.partial_text)
183
+ emit_partial(result.partial_text, session_id=found_session_id)
175
184
  last_partial_text = result.partial_text
176
- if result.final_text:
185
+ if result.final_text is not None:
177
186
  last_final_text = result.final_text
178
187
 
179
188
  proc.wait()
180
189
  stderr_output = proc.stderr.read() if proc.stderr else ""
181
190
 
182
191
  if error_message:
183
- # Persist the session for the next turn BEFORE emitting the error
184
- # the error terminates this turn but does not invalidate the session.
185
- if found_session_id:
186
- emit_session(found_session_id)
187
- emit_error(error_message)
192
+ # Persist the session on the error event the error terminates this
193
+ # turn but does not invalidate the session.
194
+ emit_error(error_message, session_id=found_session_id)
188
195
  return 1
189
196
  if proc.returncode != 0 and not found_session_id:
190
197
  msg = f"{cli_name} exited with {proc.returncode}"
@@ -193,11 +200,8 @@ def run_bridge(
193
200
  emit_error(msg)
194
201
  return 1
195
202
 
196
- if found_session_id:
197
- emit_session(found_session_id)
198
203
  reply_text = last_final_text if last_final_text is not None else last_partial_text
199
- if reply_text:
200
- emit_text(reply_text)
204
+ emit_result(reply_text or "", session_id=found_session_id)
201
205
  return 0
202
206
 
203
207
 
@@ -231,7 +235,7 @@ def run_plain_cli(
231
235
  """Drive a one-shot CLI that returns the full reply as plain stdout text
232
236
  (no streaming, no session id). Reads the turn from stdin, runs the CLI
233
237
  with a timeout, and emits the trimmed stdout as a single
234
- ``{"type":"text"}`` event (or ``{"type":"error"}`` on failure).
238
+ ``{"type":"result"}`` event (or ``{"type":"error"}`` on failure).
235
239
 
236
240
  ``build_args(message) -> list[str]`` builds the argv; per-CLI config
237
241
  (model, api key, …) is read from ``os.environ`` inside ``build_args`` as
@@ -271,7 +275,7 @@ def run_plain_cli(
271
275
 
272
276
  text = (proc.stdout or "").strip()
273
277
  if text:
274
- emit_text(text)
278
+ emit_result(text)
275
279
  return 0
276
280
  emit_error(f"{cli_name} returned empty output")
277
281
  return 1
@@ -61,14 +61,14 @@ Expected output (on stdout):
61
61
  agy ok
62
62
  ```
63
63
 
64
- (No `{"type":"session"}` line — see "Session continuity" below.)
64
+ (No `session_id` on events — see "Session continuity" below.)
65
65
 
66
66
  <details>
67
67
  <summary>Drive the bridge script directly (without the CLI)</summary>
68
68
 
69
69
  ```bash
70
70
  cd hub/agy
71
- echo '{"type":"turn","message":"reply with exactly: agy ok","session_id":"","from_user":"u1","protocol_version":"0.3"}' | python3 bridge.py
71
+ echo '{"type":"turn","message":"reply with exactly: agy ok","session_id":"","from_user":"u1","protocol_version":"0.4"}' | python3 bridge.py
72
72
  ```
73
73
 
74
74
  </details>
@@ -83,12 +83,12 @@ bridge.py / bridge.js
83
83
  agy CLI
84
84
  ↓ plain text reply on stdout (after the turn completes)
85
85
  bridge.py / bridge.js
86
- ↓ reply body (no {"type":"session"} line, no {"type":"partial"} lines)
86
+ ↓ reply body (no session_id on events, no {"type":"partial"} lines)
87
87
  ```
88
88
 
89
89
  ## Session continuity
90
90
 
91
- **agy's `--print` mode does not expose a session id on stdout.** The bridge therefore emits no `{"type":"session"}` line. Each AgentProc turn spawns a fresh agy process.
91
+ **agy's `--print` mode does not expose a session id on stdout.** The bridge therefore omits `session_id` on events. Each AgentProc turn spawns a fresh agy process.
92
92
 
93
93
  If your messaging bridge needs multi-turn context, use the [AgentProc SDK](https://agentproc.dev/sdk/python)'s history helpers (`load_history` / `append_history`) to maintain context in a JSONL file keyed by the messaging bridge's own session id. The [Python SDK history example](https://agentproc.dev/sdk/python#conversation-history) shows the pattern.
94
94
 
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * agy's --print mode returns the full reply as plain text — no streaming, no
7
7
  * exposed session id. The bridge forwards the text as the reply body (a single
8
- * {"type":"text"} event).
8
+ * {"type":"result"} event).
9
9
  *
10
10
  * Per-CLI config (read from the process env the runner injects):
11
11
  * AGY_MODEL Optional model override
@@ -4,7 +4,7 @@ AgentProc bridge for the `agy` CLI (wire 0.3).
4
4
 
5
5
  agy's --print mode returns the full reply as plain text — no streaming, no
6
6
  exposed session id. The bridge forwards the text as the reply body (a single
7
- {"type":"text"} event).
7
+ {"type":"result"} event).
8
8
 
9
9
  Per-CLI config (read from the process env the runner injects):
10
10
  AGY_MODEL Optional model override
@@ -20,7 +20,7 @@ notes: |
20
20
  completes — it does not stream chunks and does not expose a resumable
21
21
  session id on stdout. As a result:
22
22
 
23
- - No {"type":"session"} line is emitted (AgentProc allows this).
23
+ - No session_id is stamped on events (AgentProc allows this).
24
24
  - Each turn is a fresh agy process; multi-turn continuity must be
25
25
  maintained by the messaging bridge (e.g. via the AgentProc SDK's
26
26
  load_history / append_history helpers) rather than by agy itself.
@@ -54,7 +54,7 @@ env_allowlist: [AIDER_MODEL, ANTHROPIC_API_KEY, OPENAI_API_KEY]
54
54
 
55
55
  ```bash
56
56
  cd hub/aider
57
- echo '{"type":"turn","message":"reply with exactly: aider ok (do not edit any files)","session_id":"","from_user":"u1","protocol_version":"0.3"}' | python3 bridge.py
57
+ echo '{"type":"turn","message":"reply with exactly: aider ok (do not edit any files)","session_id":"","from_user":"u1","protocol_version":"0.4"}' | python3 bridge.py
58
58
  ```
59
59
 
60
60
  Expected: stdout contains `aider ok` (plus aider's usual banner/summary output).
@@ -70,7 +70,7 @@ aider CLI
70
70
  ↓ reads cwd via repo map → LLM → edits files → optional git commit
71
71
  ↓ human-readable summary on stdout (after the turn completes)
72
72
  bridge.py / bridge.js
73
- ↓ reply body (no {"type":"session"} line, no {"type":"partial"} lines)
73
+ ↓ reply body (no session_id on events, no {"type":"partial"} lines)
74
74
  ```
75
75
 
76
76
  ## Session continuity
@@ -92,7 +92,7 @@ For projects without git, there is no automatic context continuity. Use the [Age
92
92
 
93
93
  - **Modifies files.** aider edits your working directory. Run it in a git repo so you can review and revert changes.
94
94
  - **No streaming.** `--no-stream` returns the full response at the end.
95
- - **No {"type":"session"}.** Session continuity is git-based, not AgentProc-native. The bridge emits no `{"type":"session"}` line.
95
+ - **No `session_id` on events.** Session continuity is git-based, not AgentProc-native. The bridge omits `session_id` on events.
96
96
  - **No explicit file targeting.** The bridge does not pass specific files; aider uses its repo-map heuristic to find relevant files. Add files to aider's context by including them in your message (e.g. `"edit src/utils.py: add type hints"`).
97
97
  - **Auto-confirm.** `--yes-always` bypasses all aider confirmation prompts. Aider will edit and commit without asking.
98
98
 
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * aider modifies files in cwd and may make git commits. Its stdout (a
10
10
  * human-readable summary) is forwarded as the reply body (a single
11
- * {"type":"text"} event). No session id is emitted.
11
+ * {"type":"result"} event). No session id is emitted.
12
12
  *
13
13
  * Per-CLI config (read from the process env the runner injects):
14
14
  * AIDER_MODEL Optional model override (e.g. "claude-opus-4-5")
@@ -8,7 +8,7 @@ Invokes:
8
8
 
9
9
  aider modifies files in the working directory and may make git commits.
10
10
  The stdout output (a human-readable summary of what was done) is forwarded
11
- as the AgentProc reply body (a single {"type":"text"} event). No session id
11
+ as the AgentProc reply body (a single {"type":"result"} event). No session id
12
12
  is emitted — aider uses git history for context continuity, not an explicit
13
13
  session id.
14
14