agentproc 0.4.2__tar.gz → 0.7.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.4.2 → agentproc-0.7.0}/PKG-INFO +2 -1
- {agentproc-0.4.2 → agentproc-0.7.0}/pyproject.toml +2 -2
- agentproc-0.7.0/src/agentproc/__init__.py +363 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/cli.py +66 -148
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub.py +5 -3
- agentproc-0.7.0/src/agentproc/hub_data/PERMISSIONS.md +25 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/README.md +17 -8
- agentproc-0.7.0/src/agentproc/hub_data/_shared/README.md +83 -0
- agentproc-0.7.0/src/agentproc/hub_data/_shared/stream_utils.js +230 -0
- agentproc-0.7.0/src/agentproc/hub_data/_shared/stream_utils.py +277 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/agy/README.md +7 -10
- agentproc-0.7.0/src/agentproc/hub_data/agy/bridge.js +41 -0
- agentproc-0.7.0/src/agentproc/hub_data/agy/bridge.py +42 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/agy/profile.yaml +5 -3
- agentproc-0.7.0/src/agentproc/hub_data/aider/README.md +101 -0
- agentproc-0.7.0/src/agentproc/hub_data/aider/bridge.js +46 -0
- agentproc-0.7.0/src/agentproc/hub_data/aider/bridge.py +51 -0
- agentproc-0.7.0/src/agentproc/hub_data/aider/profile.yaml +35 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/claude-code/README.md +33 -14
- agentproc-0.7.0/src/agentproc/hub_data/claude-code/bridge.js +315 -0
- agentproc-0.7.0/src/agentproc/hub_data/claude-code/bridge.py +345 -0
- agentproc-0.7.0/src/agentproc/hub_data/claude-code/permission_map.test.js +85 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/claude-code/profile.yaml +12 -5
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/codebuddy/README.md +8 -5
- agentproc-0.7.0/src/agentproc/hub_data/codebuddy/bridge.js +86 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/codebuddy/bridge.py +21 -3
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/codebuddy/profile.yaml +7 -1
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/codex/README.md +30 -13
- agentproc-0.7.0/src/agentproc/hub_data/codex/bridge.js +253 -0
- agentproc-0.7.0/src/agentproc/hub_data/codex/bridge.py +351 -0
- agentproc-0.7.0/src/agentproc/hub_data/codex/permission_hook.py +122 -0
- agentproc-0.7.0/src/agentproc/hub_data/codex/permission_map.js +132 -0
- agentproc-0.7.0/src/agentproc/hub_data/codex/permission_map.test.js +165 -0
- agentproc-0.7.0/src/agentproc/hub_data/codex/profile.yaml +37 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/cursor/README.md +12 -15
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/cursor/bridge.py +6 -6
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/cursor/profile.yaml +7 -5
- agentproc-0.7.0/src/agentproc/hub_data/deepseek/README.md +119 -0
- agentproc-0.7.0/src/agentproc/hub_data/deepseek/bridge.js +39 -0
- agentproc-0.7.0/src/agentproc/hub_data/deepseek/bridge.py +40 -0
- agentproc-0.7.0/src/agentproc/hub_data/deepseek/profile.yaml +36 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/echo-agent/README.md +7 -6
- agentproc-0.7.0/src/agentproc/hub_data/echo-agent/bridge.js +25 -0
- agentproc-0.7.0/src/agentproc/hub_data/echo-agent/bridge.py +29 -0
- agentproc-0.7.0/src/agentproc/hub_data/echo-agent/bridge.sh +7 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/echo-agent/profile.yaml +6 -5
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/gemini-cli/README.md +12 -16
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/gemini-cli/bridge.py +7 -7
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/gemini-cli/profile.yaml +5 -3
- agentproc-0.7.0/src/agentproc/hub_data/kimi-code/README.md +99 -0
- agentproc-0.7.0/src/agentproc/hub_data/kimi-code/bridge.js +61 -0
- agentproc-0.7.0/src/agentproc/hub_data/kimi-code/bridge.py +81 -0
- agentproc-0.7.0/src/agentproc/hub_data/kimi-code/profile.yaml +40 -0
- agentproc-0.7.0/src/agentproc/hub_data/opencode/README.md +110 -0
- agentproc-0.7.0/src/agentproc/hub_data/opencode/bridge.js +63 -0
- agentproc-0.7.0/src/agentproc/hub_data/opencode/bridge.py +80 -0
- agentproc-0.7.0/src/agentproc/hub_data/opencode/profile.yaml +36 -0
- agentproc-0.7.0/src/agentproc/hub_data/pi/README.md +103 -0
- agentproc-0.7.0/src/agentproc/hub_data/pi/bridge.js +39 -0
- agentproc-0.7.0/src/agentproc/hub_data/pi/bridge.py +42 -0
- agentproc-0.7.0/src/agentproc/hub_data/pi/profile.yaml +27 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/qwen-code/README.md +5 -5
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/qwen-code/profile.yaml +3 -1
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/recursive/README.md +25 -5
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/recursive/bridge.js +81 -70
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/recursive/bridge.py +65 -77
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/recursive/profile.yaml +4 -3
- agentproc-0.7.0/src/agentproc/hub_data/recursive/tests/parity.json +76 -0
- agentproc-0.7.0/src/agentproc/hub_data/recursive/tests/test_bridge_parity.js +87 -0
- agentproc-0.7.0/src/agentproc/hub_data/recursive/tests/test_bridge_parity.py +78 -0
- agentproc-0.7.0/src/agentproc/runner.py +890 -0
- agentproc-0.7.0/src/agentproc/yaml.py +35 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc.egg-info/PKG-INFO +2 -1
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc.egg-info/SOURCES.txt +36 -1
- agentproc-0.7.0/src/agentproc.egg-info/requires.txt +1 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/tests/test_agentproc.py +126 -125
- agentproc-0.7.0/tests/test_bridges.py +811 -0
- agentproc-0.7.0/tests/test_diagnostics.py +51 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/tests/test_hub.py +19 -0
- agentproc-0.7.0/tests/test_runner.py +709 -0
- agentproc-0.7.0/tests/test_scenarios.py +80 -0
- agentproc-0.7.0/tests/test_sdk.py +70 -0
- agentproc-0.7.0/tests/test_yaml.py +57 -0
- agentproc-0.4.2/src/agentproc/__init__.py +0 -338
- agentproc-0.4.2/src/agentproc/hub_data/_shared/README.md +0 -52
- agentproc-0.4.2/src/agentproc/hub_data/_shared/stream_utils.js +0 -130
- agentproc-0.4.2/src/agentproc/hub_data/_shared/stream_utils.py +0 -161
- agentproc-0.4.2/src/agentproc/hub_data/agy/bridge.js +0 -87
- agentproc-0.4.2/src/agentproc/hub_data/agy/bridge.py +0 -75
- agentproc-0.4.2/src/agentproc/hub_data/claude-code/bridge.js +0 -65
- agentproc-0.4.2/src/agentproc/hub_data/claude-code/bridge.py +0 -76
- agentproc-0.4.2/src/agentproc/hub_data/codebuddy/bridge.js +0 -60
- agentproc-0.4.2/src/agentproc/hub_data/codex/bridge.js +0 -54
- agentproc-0.4.2/src/agentproc/hub_data/codex/bridge.py +0 -59
- agentproc-0.4.2/src/agentproc/hub_data/codex/profile.yaml +0 -25
- agentproc-0.4.2/src/agentproc/hub_data/echo-agent/bridge.js +0 -12
- agentproc-0.4.2/src/agentproc/hub_data/echo-agent/bridge.py +0 -21
- agentproc-0.4.2/src/agentproc/hub_data/echo-agent/bridge.sh +0 -4
- agentproc-0.4.2/src/agentproc/runner.py +0 -610
- agentproc-0.4.2/tests/test_runner.py +0 -374
- {agentproc-0.4.2 → agentproc-0.7.0}/setup.cfg +0 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/cursor/bridge.js +0 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/gemini-cli/bridge.js +0 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/qwen-code/bridge.js +0 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/qwen-code/bridge.py +0 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc.egg-info/dependency_links.txt +0 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc.egg-info/entry_points.txt +0 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc.egg-info/top_level.txt +0 -0
- {agentproc-0.4.2 → agentproc-0.7.0}/tests/test_conformance.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentproc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -9,3 +9,4 @@ Project-URL: Repository, https://github.com/jeffkit/agentproc
|
|
|
9
9
|
Project-URL: Issues, https://github.com/jeffkit/agentproc/issues
|
|
10
10
|
Requires-Python: >=3.9
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: PyYAML>=5.1
|
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentproc"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.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" }
|
|
11
11
|
requires-python = ">=3.9"
|
|
12
|
-
dependencies = []
|
|
12
|
+
dependencies = ["PyYAML>=5.1"]
|
|
13
13
|
|
|
14
14
|
[project.scripts]
|
|
15
15
|
agentproc = "agentproc.cli:main"
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agentproc — AgentProc Protocol SDK (Python)
|
|
3
|
+
|
|
4
|
+
Implements the AgentProc P0 protocol so you can write a single async handler
|
|
5
|
+
instead of manually reading the turn from stdin and formatting stdout.
|
|
6
|
+
|
|
7
|
+
Protocol contract (spec/protocol.md, wire protocol 0.3, NDJSON both directions):
|
|
8
|
+
Input — stdin: one {"type":"turn",...} line (message, session_id,
|
|
9
|
+
session_name, from_user, attachments, permission,
|
|
10
|
+
protocol_version). Secrets/config stay in env.
|
|
11
|
+
Output — stdout (one JSON object per line, discriminated by `type`):
|
|
12
|
+
{"type":"partial","text":...} — streaming chunk
|
|
13
|
+
{"type":"text","text":...} — final reply body
|
|
14
|
+
{"type":"session","id":...} — declare session id (last wins)
|
|
15
|
+
{"type":"error","message":...} — error message to forward to user
|
|
16
|
+
Exit — 0 success, 1 error, 124 timeout, 130 SIGINT, 143 SIGTERM
|
|
17
|
+
|
|
18
|
+
Example::
|
|
19
|
+
|
|
20
|
+
from agentproc import create_profile
|
|
21
|
+
|
|
22
|
+
async def handler(ctx):
|
|
23
|
+
reply = await my_llm(ctx.message)
|
|
24
|
+
return reply
|
|
25
|
+
|
|
26
|
+
create_profile(handler)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import asyncio
|
|
32
|
+
import inspect
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import sys
|
|
36
|
+
from dataclasses import dataclass, field
|
|
37
|
+
from datetime import datetime, timezone
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional, Sequence, Union
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"AgentContext",
|
|
43
|
+
"AgentResult",
|
|
44
|
+
"HistoryEntry",
|
|
45
|
+
"ProtocolError",
|
|
46
|
+
"create_profile",
|
|
47
|
+
"load_history",
|
|
48
|
+
"append_history",
|
|
49
|
+
"session_file_path",
|
|
50
|
+
"__version__",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _read_version() -> str:
|
|
55
|
+
try:
|
|
56
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
57
|
+
try:
|
|
58
|
+
return version("agentproc")
|
|
59
|
+
except PackageNotFoundError:
|
|
60
|
+
pass
|
|
61
|
+
except ImportError:
|
|
62
|
+
pass
|
|
63
|
+
try:
|
|
64
|
+
from pathlib import Path
|
|
65
|
+
toml_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
|
66
|
+
if toml_path.exists():
|
|
67
|
+
import re
|
|
68
|
+
text = toml_path.read_text(encoding="utf-8")
|
|
69
|
+
m = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
|
|
70
|
+
if m:
|
|
71
|
+
return m.group(1)
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
return "0.0.0+unknown"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__version__ = _read_version()
|
|
78
|
+
|
|
79
|
+
# Single source of truth: the wire-protocol version lives in runner.py
|
|
80
|
+
# (the canonical bridge-side engine). Re-exported here so
|
|
81
|
+
# `agentproc.PROTOCOL_VERSION` stays in lockstep without copy-pasted literals.
|
|
82
|
+
from .runner import PROTOCOL_VERSION, is_valid_session_id # noqa: E402
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ProtocolError(Exception):
|
|
86
|
+
"""Raised by the handler to signal an error that should reach the user.
|
|
87
|
+
|
|
88
|
+
The SDK emits a ``{"type":"error",...}`` line with the message and exits
|
|
89
|
+
non-zero.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class _NoopAwaitable:
|
|
94
|
+
"""An awaitable that does nothing. Returned by the sync ``send_partial`` /
|
|
95
|
+
``send_error`` so ``await ctx.send_partial(...)`` keeps working in async
|
|
96
|
+
handlers while a bare ``ctx.send_partial(...)`` (sync handler) also writes
|
|
97
|
+
— the write happens at call time, the await is a no-op."""
|
|
98
|
+
|
|
99
|
+
def __await__(self):
|
|
100
|
+
return
|
|
101
|
+
yield # pragma: no cover — makes this a generator-based awaitable
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class AgentContext:
|
|
106
|
+
"""Input context passed to the agent handler."""
|
|
107
|
+
|
|
108
|
+
message: str
|
|
109
|
+
"""User message text (turn.message)."""
|
|
110
|
+
|
|
111
|
+
session_id: str
|
|
112
|
+
"""Session ID from the previous turn (turn.session_id). Empty = new session."""
|
|
113
|
+
|
|
114
|
+
session_name: str
|
|
115
|
+
"""Human-readable session name (turn.session_name)."""
|
|
116
|
+
|
|
117
|
+
from_user: str
|
|
118
|
+
"""Sender identifier (turn.from_user)."""
|
|
119
|
+
|
|
120
|
+
protocol_version: str
|
|
121
|
+
"""Protocol version the bridge implements (turn.protocol_version)."""
|
|
122
|
+
|
|
123
|
+
attachments: List[Dict[str, Any]]
|
|
124
|
+
"""Attachment list (turn.attachments). Empty list = no attachments."""
|
|
125
|
+
|
|
126
|
+
permission: bool
|
|
127
|
+
"""True when the bridge enabled the optional permission channel."""
|
|
128
|
+
|
|
129
|
+
def send_partial(self, text: str, role: Optional[str] = None):
|
|
130
|
+
"""Send a streaming chunk to the user immediately.
|
|
131
|
+
|
|
132
|
+
Writes a ``{"type":"partial","text":...}`` line to stdout and flushes
|
|
133
|
+
at call time. The bridge forwards it to the user without waiting for
|
|
134
|
+
the process to exit. ``role`` (e.g. ``"output"`` or ``"thinking"``)
|
|
135
|
+
is optional and MAY be rendered differently by the bridge.
|
|
136
|
+
|
|
137
|
+
Returns a no-op awaitable so ``await ctx.send_partial(...)`` keeps
|
|
138
|
+
working in async handlers; a sync handler may call it bare. Either way
|
|
139
|
+
the write happens at call time.
|
|
140
|
+
"""
|
|
141
|
+
if not text:
|
|
142
|
+
return _NoopAwaitable()
|
|
143
|
+
evt: Dict[str, Any] = {"type": "partial", "text": text}
|
|
144
|
+
if role:
|
|
145
|
+
evt["role"] = role
|
|
146
|
+
sys.stdout.write(json.dumps(evt, ensure_ascii=False, separators=(',', ':')) + "\n")
|
|
147
|
+
sys.stdout.flush()
|
|
148
|
+
return _NoopAwaitable()
|
|
149
|
+
|
|
150
|
+
def send_error(self, text: str):
|
|
151
|
+
"""Send an error message to the user.
|
|
152
|
+
|
|
153
|
+
Writes a ``{"type":"error","message":...}`` line to stdout and flushes
|
|
154
|
+
at call time. Honored regardless of ``streaming`` mode. After calling
|
|
155
|
+
this, the handler should typically raise ProtocolError or return — any
|
|
156
|
+
reply body produced alongside will be discarded by the bridge.
|
|
157
|
+
|
|
158
|
+
Returns a no-op awaitable so ``await ctx.send_error(...)`` keeps working
|
|
159
|
+
in async handlers; a sync handler may call it bare.
|
|
160
|
+
"""
|
|
161
|
+
if not text:
|
|
162
|
+
return _NoopAwaitable()
|
|
163
|
+
sys.stdout.write(
|
|
164
|
+
json.dumps({"type": "error", "message": text}, ensure_ascii=False, separators=(',', ':')) + "\n"
|
|
165
|
+
)
|
|
166
|
+
sys.stdout.flush()
|
|
167
|
+
return _NoopAwaitable()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass
|
|
171
|
+
class AgentResult:
|
|
172
|
+
"""Return value from the agent handler."""
|
|
173
|
+
|
|
174
|
+
response: str = ""
|
|
175
|
+
"""Final reply text. Can be empty if all content was sent via send_partial."""
|
|
176
|
+
|
|
177
|
+
session_id: str = ""
|
|
178
|
+
"""Session ID to persist. Bridge will pass it back next turn as session_id."""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass
|
|
182
|
+
class HistoryEntry:
|
|
183
|
+
role: str
|
|
184
|
+
content: str
|
|
185
|
+
timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# History helpers (JSONL, stored per session)
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
def session_file_path(session_id: str, base_dir: Optional[str] = None) -> Path:
|
|
193
|
+
"""Resolve the JSONL history file path for a session.
|
|
194
|
+
|
|
195
|
+
Returns the path even if the file does not yet exist. Raises ``ValueError``
|
|
196
|
+
when ``session_id`` is empty — callers should guard with ``if session_id``.
|
|
197
|
+
|
|
198
|
+
In wire 0.3 a session id is an arbitrary JSON string on the wire, but the
|
|
199
|
+
SDK stores each session as ``<id>.jsonl``; an id that is not a storage-safe
|
|
200
|
+
filename component (path separators, control chars, ``.``/``..``) would
|
|
201
|
+
path-traverse out of the sessions directory and is rejected here. See
|
|
202
|
+
``is_valid_session_id`` in runner.py.
|
|
203
|
+
"""
|
|
204
|
+
if not session_id:
|
|
205
|
+
raise ValueError("session_id must be non-empty")
|
|
206
|
+
if not is_valid_session_id(session_id):
|
|
207
|
+
raise ValueError(f"session_id is not a safe filename component: {session_id!r}")
|
|
208
|
+
root = Path(base_dir) if base_dir else Path.home() / ".agentproc" / "sessions"
|
|
209
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
return root / f"{session_id}.jsonl"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def load_history(session_id: str, base_dir: Optional[str] = None) -> List[HistoryEntry]:
|
|
214
|
+
"""Load conversation history for a session. Returns ``[]`` if no session
|
|
215
|
+
or no file exists."""
|
|
216
|
+
if not session_id:
|
|
217
|
+
return []
|
|
218
|
+
try:
|
|
219
|
+
path = session_file_path(session_id, base_dir)
|
|
220
|
+
except ValueError:
|
|
221
|
+
return []
|
|
222
|
+
if not path.exists():
|
|
223
|
+
return []
|
|
224
|
+
entries: List[HistoryEntry] = []
|
|
225
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
226
|
+
line = line.strip()
|
|
227
|
+
if not line:
|
|
228
|
+
continue
|
|
229
|
+
try:
|
|
230
|
+
d = json.loads(line)
|
|
231
|
+
except json.JSONDecodeError:
|
|
232
|
+
continue
|
|
233
|
+
entries.append(HistoryEntry(
|
|
234
|
+
role=d.get("role", ""),
|
|
235
|
+
content=d.get("content", ""),
|
|
236
|
+
timestamp=d.get("timestamp", ""),
|
|
237
|
+
))
|
|
238
|
+
return entries
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def append_history(
|
|
242
|
+
session_id: str,
|
|
243
|
+
entries: Sequence[HistoryEntry],
|
|
244
|
+
base_dir: Optional[str] = None,
|
|
245
|
+
) -> None:
|
|
246
|
+
"""Append entries to a session's JSONL history file. No-op if no session_id."""
|
|
247
|
+
if not session_id or not entries:
|
|
248
|
+
return
|
|
249
|
+
path = session_file_path(session_id, base_dir)
|
|
250
|
+
with path.open("a", encoding="utf-8") as f:
|
|
251
|
+
for e in entries:
|
|
252
|
+
f.write(json.dumps(
|
|
253
|
+
{"role": e.role, "content": e.content, "timestamp": e.timestamp},
|
|
254
|
+
ensure_ascii=False,
|
|
255
|
+
))
|
|
256
|
+
f.write("\n")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
# Turn parsing — read the {"type":"turn",...} line from stdin
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
def _read_turn() -> Dict[str, Any]:
|
|
264
|
+
"""Read exactly one line from stdin (the turn object) and JSON-decode it.
|
|
265
|
+
|
|
266
|
+
Returns an empty dict on any failure (best-effort, fail-soft per spec).
|
|
267
|
+
"""
|
|
268
|
+
try:
|
|
269
|
+
line = sys.stdin.readline()
|
|
270
|
+
except Exception:
|
|
271
|
+
return {}
|
|
272
|
+
if not line:
|
|
273
|
+
return {}
|
|
274
|
+
line = line.rstrip("\r\n")
|
|
275
|
+
try:
|
|
276
|
+
v = json.loads(line)
|
|
277
|
+
if isinstance(v, dict):
|
|
278
|
+
return v
|
|
279
|
+
except json.JSONDecodeError:
|
|
280
|
+
pass
|
|
281
|
+
return {}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _context_from_turn() -> AgentContext:
|
|
285
|
+
t = _read_turn()
|
|
286
|
+
return AgentContext(
|
|
287
|
+
message=t.get("message") if isinstance(t.get("message"), str) else "",
|
|
288
|
+
session_id=t.get("session_id") if isinstance(t.get("session_id"), str) else "",
|
|
289
|
+
session_name=t.get("session_name") if isinstance(t.get("session_name"), str) else "default",
|
|
290
|
+
from_user=t.get("from_user") if isinstance(t.get("from_user"), str) else "",
|
|
291
|
+
protocol_version=t.get("protocol_version") if isinstance(t.get("protocol_version"), str) else PROTOCOL_VERSION,
|
|
292
|
+
attachments=t.get("attachments") if isinstance(t.get("attachments"), list) else [],
|
|
293
|
+
permission=t.get("permission") is True,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
# Core entrypoint
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
Handler = Callable[[AgentContext], Union[Awaitable[Union[str, AgentResult, None]], Union[str, AgentResult, None]]]
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def create_profile(handler: Handler) -> None:
|
|
305
|
+
"""Run the handler as an AgentProc-compliant process.
|
|
306
|
+
|
|
307
|
+
Reads the turn object from stdin, calls the handler, and writes the P0
|
|
308
|
+
output to stdout. Call this at the bottom of your script — it blocks until
|
|
309
|
+
the handler completes and then exits the process.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
handler: A function taking an :class:`AgentContext` and returning either
|
|
313
|
+
a plain string (treated as ``AgentResult(response=...)``), an
|
|
314
|
+
:class:`AgentResult`, or ``None`` (when everything was signalled
|
|
315
|
+
via ``send_partial`` / ``send_error``). The handler may be ``async``
|
|
316
|
+
or a plain sync function — a sync handler is run directly, an async
|
|
317
|
+
one (or any handler returning a coroutine) is awaited via
|
|
318
|
+
:func:`asyncio.run`. This mirrors the Node SDK, which accepts both.
|
|
319
|
+
"""
|
|
320
|
+
ctx = _context_from_turn()
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
ret = handler(ctx)
|
|
324
|
+
# An async handler (or a sync one returning a coroutine) is awaited to
|
|
325
|
+
# completion; a sync handler's plain return value is used as-is.
|
|
326
|
+
result = asyncio.run(ret) if inspect.iscoroutine(ret) else ret
|
|
327
|
+
except ProtocolError as e:
|
|
328
|
+
# Handler already-signaled error surfaced via exception.
|
|
329
|
+
sys.stdout.write(
|
|
330
|
+
json.dumps(
|
|
331
|
+
{"type": "error", "message": str(e) or "unknown error"},
|
|
332
|
+
ensure_ascii=False, separators=(',', ':'),
|
|
333
|
+
) + "\n"
|
|
334
|
+
)
|
|
335
|
+
sys.stdout.flush()
|
|
336
|
+
sys.exit(1)
|
|
337
|
+
except Exception as e:
|
|
338
|
+
sys.stderr.write(f"[agentproc] handler error: {e}\n")
|
|
339
|
+
sys.exit(1)
|
|
340
|
+
|
|
341
|
+
# Handler may return None when it has already signaled everything via
|
|
342
|
+
# send_partial / send_error. Treat None as an empty AgentResult.
|
|
343
|
+
if result is None:
|
|
344
|
+
sys.exit(0)
|
|
345
|
+
|
|
346
|
+
if isinstance(result, str):
|
|
347
|
+
result = AgentResult(response=result)
|
|
348
|
+
|
|
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
|
+
)
|
|
361
|
+
sys.stdout.flush()
|
|
362
|
+
|
|
363
|
+
sys.exit(0)
|