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.
Files changed (109) hide show
  1. {agentproc-0.4.2 → agentproc-0.7.0}/PKG-INFO +2 -1
  2. {agentproc-0.4.2 → agentproc-0.7.0}/pyproject.toml +2 -2
  3. agentproc-0.7.0/src/agentproc/__init__.py +363 -0
  4. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/cli.py +66 -148
  5. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub.py +5 -3
  6. agentproc-0.7.0/src/agentproc/hub_data/PERMISSIONS.md +25 -0
  7. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/README.md +17 -8
  8. agentproc-0.7.0/src/agentproc/hub_data/_shared/README.md +83 -0
  9. agentproc-0.7.0/src/agentproc/hub_data/_shared/stream_utils.js +230 -0
  10. agentproc-0.7.0/src/agentproc/hub_data/_shared/stream_utils.py +277 -0
  11. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/agy/README.md +7 -10
  12. agentproc-0.7.0/src/agentproc/hub_data/agy/bridge.js +41 -0
  13. agentproc-0.7.0/src/agentproc/hub_data/agy/bridge.py +42 -0
  14. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/agy/profile.yaml +5 -3
  15. agentproc-0.7.0/src/agentproc/hub_data/aider/README.md +101 -0
  16. agentproc-0.7.0/src/agentproc/hub_data/aider/bridge.js +46 -0
  17. agentproc-0.7.0/src/agentproc/hub_data/aider/bridge.py +51 -0
  18. agentproc-0.7.0/src/agentproc/hub_data/aider/profile.yaml +35 -0
  19. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/claude-code/README.md +33 -14
  20. agentproc-0.7.0/src/agentproc/hub_data/claude-code/bridge.js +315 -0
  21. agentproc-0.7.0/src/agentproc/hub_data/claude-code/bridge.py +345 -0
  22. agentproc-0.7.0/src/agentproc/hub_data/claude-code/permission_map.test.js +85 -0
  23. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/claude-code/profile.yaml +12 -5
  24. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/codebuddy/README.md +8 -5
  25. agentproc-0.7.0/src/agentproc/hub_data/codebuddy/bridge.js +86 -0
  26. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/codebuddy/bridge.py +21 -3
  27. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/codebuddy/profile.yaml +7 -1
  28. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/codex/README.md +30 -13
  29. agentproc-0.7.0/src/agentproc/hub_data/codex/bridge.js +253 -0
  30. agentproc-0.7.0/src/agentproc/hub_data/codex/bridge.py +351 -0
  31. agentproc-0.7.0/src/agentproc/hub_data/codex/permission_hook.py +122 -0
  32. agentproc-0.7.0/src/agentproc/hub_data/codex/permission_map.js +132 -0
  33. agentproc-0.7.0/src/agentproc/hub_data/codex/permission_map.test.js +165 -0
  34. agentproc-0.7.0/src/agentproc/hub_data/codex/profile.yaml +37 -0
  35. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/cursor/README.md +12 -15
  36. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/cursor/bridge.py +6 -6
  37. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/cursor/profile.yaml +7 -5
  38. agentproc-0.7.0/src/agentproc/hub_data/deepseek/README.md +119 -0
  39. agentproc-0.7.0/src/agentproc/hub_data/deepseek/bridge.js +39 -0
  40. agentproc-0.7.0/src/agentproc/hub_data/deepseek/bridge.py +40 -0
  41. agentproc-0.7.0/src/agentproc/hub_data/deepseek/profile.yaml +36 -0
  42. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/echo-agent/README.md +7 -6
  43. agentproc-0.7.0/src/agentproc/hub_data/echo-agent/bridge.js +25 -0
  44. agentproc-0.7.0/src/agentproc/hub_data/echo-agent/bridge.py +29 -0
  45. agentproc-0.7.0/src/agentproc/hub_data/echo-agent/bridge.sh +7 -0
  46. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/echo-agent/profile.yaml +6 -5
  47. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/gemini-cli/README.md +12 -16
  48. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/gemini-cli/bridge.py +7 -7
  49. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/gemini-cli/profile.yaml +5 -3
  50. agentproc-0.7.0/src/agentproc/hub_data/kimi-code/README.md +99 -0
  51. agentproc-0.7.0/src/agentproc/hub_data/kimi-code/bridge.js +61 -0
  52. agentproc-0.7.0/src/agentproc/hub_data/kimi-code/bridge.py +81 -0
  53. agentproc-0.7.0/src/agentproc/hub_data/kimi-code/profile.yaml +40 -0
  54. agentproc-0.7.0/src/agentproc/hub_data/opencode/README.md +110 -0
  55. agentproc-0.7.0/src/agentproc/hub_data/opencode/bridge.js +63 -0
  56. agentproc-0.7.0/src/agentproc/hub_data/opencode/bridge.py +80 -0
  57. agentproc-0.7.0/src/agentproc/hub_data/opencode/profile.yaml +36 -0
  58. agentproc-0.7.0/src/agentproc/hub_data/pi/README.md +103 -0
  59. agentproc-0.7.0/src/agentproc/hub_data/pi/bridge.js +39 -0
  60. agentproc-0.7.0/src/agentproc/hub_data/pi/bridge.py +42 -0
  61. agentproc-0.7.0/src/agentproc/hub_data/pi/profile.yaml +27 -0
  62. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/qwen-code/README.md +5 -5
  63. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/qwen-code/profile.yaml +3 -1
  64. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/recursive/README.md +25 -5
  65. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/recursive/bridge.js +81 -70
  66. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/recursive/bridge.py +65 -77
  67. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/recursive/profile.yaml +4 -3
  68. agentproc-0.7.0/src/agentproc/hub_data/recursive/tests/parity.json +76 -0
  69. agentproc-0.7.0/src/agentproc/hub_data/recursive/tests/test_bridge_parity.js +87 -0
  70. agentproc-0.7.0/src/agentproc/hub_data/recursive/tests/test_bridge_parity.py +78 -0
  71. agentproc-0.7.0/src/agentproc/runner.py +890 -0
  72. agentproc-0.7.0/src/agentproc/yaml.py +35 -0
  73. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc.egg-info/PKG-INFO +2 -1
  74. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc.egg-info/SOURCES.txt +36 -1
  75. agentproc-0.7.0/src/agentproc.egg-info/requires.txt +1 -0
  76. {agentproc-0.4.2 → agentproc-0.7.0}/tests/test_agentproc.py +126 -125
  77. agentproc-0.7.0/tests/test_bridges.py +811 -0
  78. agentproc-0.7.0/tests/test_diagnostics.py +51 -0
  79. {agentproc-0.4.2 → agentproc-0.7.0}/tests/test_hub.py +19 -0
  80. agentproc-0.7.0/tests/test_runner.py +709 -0
  81. agentproc-0.7.0/tests/test_scenarios.py +80 -0
  82. agentproc-0.7.0/tests/test_sdk.py +70 -0
  83. agentproc-0.7.0/tests/test_yaml.py +57 -0
  84. agentproc-0.4.2/src/agentproc/__init__.py +0 -338
  85. agentproc-0.4.2/src/agentproc/hub_data/_shared/README.md +0 -52
  86. agentproc-0.4.2/src/agentproc/hub_data/_shared/stream_utils.js +0 -130
  87. agentproc-0.4.2/src/agentproc/hub_data/_shared/stream_utils.py +0 -161
  88. agentproc-0.4.2/src/agentproc/hub_data/agy/bridge.js +0 -87
  89. agentproc-0.4.2/src/agentproc/hub_data/agy/bridge.py +0 -75
  90. agentproc-0.4.2/src/agentproc/hub_data/claude-code/bridge.js +0 -65
  91. agentproc-0.4.2/src/agentproc/hub_data/claude-code/bridge.py +0 -76
  92. agentproc-0.4.2/src/agentproc/hub_data/codebuddy/bridge.js +0 -60
  93. agentproc-0.4.2/src/agentproc/hub_data/codex/bridge.js +0 -54
  94. agentproc-0.4.2/src/agentproc/hub_data/codex/bridge.py +0 -59
  95. agentproc-0.4.2/src/agentproc/hub_data/codex/profile.yaml +0 -25
  96. agentproc-0.4.2/src/agentproc/hub_data/echo-agent/bridge.js +0 -12
  97. agentproc-0.4.2/src/agentproc/hub_data/echo-agent/bridge.py +0 -21
  98. agentproc-0.4.2/src/agentproc/hub_data/echo-agent/bridge.sh +0 -4
  99. agentproc-0.4.2/src/agentproc/runner.py +0 -610
  100. agentproc-0.4.2/tests/test_runner.py +0 -374
  101. {agentproc-0.4.2 → agentproc-0.7.0}/setup.cfg +0 -0
  102. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/cursor/bridge.js +0 -0
  103. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/gemini-cli/bridge.js +0 -0
  104. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/qwen-code/bridge.js +0 -0
  105. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc/hub_data/qwen-code/bridge.py +0 -0
  106. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc.egg-info/dependency_links.txt +0 -0
  107. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc.egg-info/entry_points.txt +0 -0
  108. {agentproc-0.4.2 → agentproc-0.7.0}/src/agentproc.egg-info/top_level.txt +0 -0
  109. {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.4.2
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.4.2"
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)