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.
- {agentproc-0.7.1 → agentproc-0.9.0}/PKG-INFO +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/pyproject.toml +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/__init__.py +69 -16
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/cli.py +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/README.md +5 -5
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/_shared/README.md +9 -7
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/_shared/stream_utils.js +31 -29
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/_shared/stream_utils.py +41 -37
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/agy/README.md +4 -4
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/agy/bridge.js +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/agy/bridge.py +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/agy/profile.yaml +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/aider/README.md +3 -3
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/aider/bridge.js +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/aider/bridge.py +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/claude-code/README.md +4 -4
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/claude-code/bridge.js +10 -9
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/claude-code/bridge.py +9 -12
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/claude-code/profile.yaml +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codebuddy/README.md +3 -3
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codex/README.md +3 -4
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codex/bridge.js +6 -9
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codex/bridge.py +6 -12
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/cursor/README.md +9 -9
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/cursor/bridge.py +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/cursor/profile.yaml +2 -2
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/deepseek/README.md +5 -5
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/deepseek/bridge.js +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/deepseek/bridge.py +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/deepseek/profile.yaml +2 -2
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/echo-agent/README.md +3 -3
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/echo-agent/bridge.js +4 -4
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/echo-agent/bridge.py +3 -3
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/echo-agent/bridge.sh +3 -3
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/echo-agent/profile.yaml +2 -2
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/gemini-cli/README.md +9 -9
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/gemini-cli/bridge.py +3 -3
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/gemini-cli/profile.yaml +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/kimi-code/README.md +7 -6
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/kimi-code/bridge.js +3 -3
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/kimi-code/bridge.py +3 -3
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/kimi-code/profile.yaml +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/opencode/README.md +8 -8
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/opencode/bridge.py +2 -2
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/opencode/profile.yaml +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/pi/README.md +3 -3
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/pi/bridge.js +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/pi/bridge.py +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/pi/profile.yaml +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/qwen-code/README.md +2 -2
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/recursive/README.md +3 -3
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/recursive/bridge.js +21 -23
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/recursive/bridge.py +25 -24
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/runner.py +162 -73
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc.egg-info/PKG-INFO +1 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc.egg-info/SOURCES.txt +1 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_agentproc.py +17 -18
- {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_bridges.py +23 -19
- agentproc-0.9.0/tests/test_hub_bridge_conformance.py +156 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_runner.py +105 -58
- {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_scenarios.py +15 -5
- {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_sdk.py +5 -1
- {agentproc-0.7.1 → agentproc-0.9.0}/setup.cfg +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub.py +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/PERMISSIONS.md +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/aider/profile.yaml +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/claude-code/permission_map.test.js +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codebuddy/bridge.js +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codebuddy/bridge.py +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codebuddy/profile.yaml +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codex/permission_hook.py +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codex/permission_map.js +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codex/permission_map.test.js +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/codex/profile.yaml +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/cursor/bridge.js +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/gemini-cli/bridge.js +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/opencode/bridge.js +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/qwen-code/bridge.js +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/qwen-code/bridge.py +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/qwen-code/profile.yaml +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/recursive/profile.yaml +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/recursive/tests/parity.json +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/recursive/tests/test_bridge_parity.js +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/hub_data/recursive/tests/test_bridge_parity.py +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc/yaml.py +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc.egg-info/dependency_links.txt +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc.egg-info/entry_points.txt +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc.egg-info/requires.txt +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/src/agentproc.egg-info/top_level.txt +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_conformance.py +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_diagnostics.py +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_hub.py +0 -0
- {agentproc-0.7.1 → agentproc-0.9.0}/tests/test_yaml.py +0 -0
|
@@ -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.
|
|
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":"
|
|
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.
|
|
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
|
-
#
|
|
350
|
-
#
|
|
351
|
-
if result.session_id:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
sys.stdout.
|
|
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.
|
|
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.
|
|
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.
|
|
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":"
|
|
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.
|
|
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
|
+
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 /
|
|
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) +
|
|
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":"
|
|
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":"
|
|
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":"
|
|
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.
|
|
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.
|
|
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"
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - {"type":"
|
|
15
|
-
*
|
|
16
|
-
* - {"type":"
|
|
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 /
|
|
39
|
+
// the common cases use emitPartial / emitResult / emitError.
|
|
39
40
|
emitObj(obj);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
function emitPartial(text) {
|
|
43
|
-
|
|
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
|
|
51
|
-
|
|
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
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- {"type":"
|
|
13
|
-
|
|
14
|
-
- {"type":"
|
|
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":"
|
|
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 /
|
|
60
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
73
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
184
|
-
#
|
|
185
|
-
|
|
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
|
-
|
|
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":"
|
|
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
|
-
|
|
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 `
|
|
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.
|
|
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
|
|
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
|
|
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":"
|
|
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":"
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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":"
|
|
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":"
|
|
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
|
|