orcha-sdk 0.1.1__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.
@@ -0,0 +1,130 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib64/
14
+ parts/
15
+ sdist/
16
+ var/
17
+ wheels/
18
+ *.egg-info/
19
+ .installed.cfg
20
+ *.egg
21
+ MANIFEST
22
+
23
+ # Virtual Environments
24
+ .env.*
25
+ .env
26
+ .venv/
27
+ env/
28
+ venv/
29
+ ENV/
30
+
31
+ # uv
32
+ .uv/
33
+
34
+ # IDEs
35
+ .vscode/
36
+ .idea/
37
+ *.swp
38
+ *.swo
39
+ *~
40
+ .DS_Store
41
+
42
+ # Testing
43
+ .pytest_cache/
44
+ .coverage
45
+ coverage.xml
46
+ htmlcov/
47
+ .tox/
48
+ *.cover
49
+ *.log
50
+
51
+ # Prisma
52
+ node_modules/
53
+ common/database/src/generated_client/
54
+ *.db
55
+ *.db-journal
56
+
57
+ # gRPC Generated
58
+ common/proto/src/*_pb2.py
59
+ common/proto/src/*_pb2_grpc.py
60
+ common/proto/src/*_pb2.pyi
61
+
62
+ # Logs
63
+ *.log
64
+ logs/
65
+ dist/
66
+
67
+ # Database
68
+ *.sqlite
69
+ *.sqlite3
70
+
71
+ # Environment
72
+ .env.local
73
+ .env.*.local
74
+ .env.production
75
+ .env.*
76
+ !.env.example
77
+ !.env.sandbox.example
78
+
79
+ # Docker
80
+ .dockerignore
81
+
82
+ # SSH keys / certificates / credentials (never commit these)
83
+ *.pem
84
+ *.key
85
+ *.p12
86
+ *.pfx
87
+ *_SERV_ACC*.json
88
+ *service-account*.json
89
+ *service_account*.json
90
+ GOOGLE_SERV_ACC*.json
91
+ *credentials*.json
92
+ *-credentials.json
93
+ gcp-*.json
94
+ startup.log
95
+
96
+ # OS
97
+ Thumbs.db
98
+
99
+ # Temporary files
100
+ *.tmp
101
+ *.temp
102
+ *.bak
103
+ .ruff_cache
104
+ .logs
105
+ ctx/
106
+
107
+ # Local-only docs — internal moat/strategy/pricing notes, PDFs, recordings.
108
+ # Populated by scripts/mirror-local-docs.sh; never meant to leave this machine.
109
+ docs-local/
110
+
111
+ # DAN / vision / network material — kept locally, not in the public repo
112
+ INCEPTION.md
113
+ VISION.md
114
+ docs/dev_docs/dan/
115
+ docs/dev_docs/EmergeOS-DAN.pdf
116
+ node/src/emerge_node/gossip.py
117
+ node/src/emerge_node/cli.py
118
+ node/tests/test_gossip_spike.py
119
+ .cdv-runs/
120
+ .wrangler/
121
+ kimi files/
122
+ kimi-export-*.md
123
+ .superpowers/
124
+
125
+ # Editor/harness rule files and local font cache — machine-local, never the repo's
126
+ .cursor/
127
+ .fonts/
128
+
129
+ # Landing site lives in solvent-labs-org/orcha-landing; local checkout kept here for convenience.
130
+ deploy/landing-site/
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.5
2
+ Name: orcha-sdk
3
+ Version: 0.1.1
4
+ Summary: Orcha agent SDK + CLI — register an agent in three lines.
5
+ Project-URL: Homepage, https://github.com/solvent-labs-org/metaorcha
6
+ Project-URL: Repository, https://github.com/solvent-labs-org/metaorcha
7
+ Project-URL: Issues, https://github.com/solvent-labs-org/metaorcha/issues
8
+ Author: Orcha
9
+ License: MIT
10
+ Keywords: a2a,ai-agents,emerge,mcp,orcha,orchestration
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: System :: Distributed Computing
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: pyyaml>=6.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # orcha-sdk
25
+
26
+ The developer SDK + `emerge` CLI for [Orcha](https://github.com/solvent-labs-org/metaorcha) —
27
+ an orchestration runtime that plans, routes, and executes one goal across agents
28
+ speaking MCP, A2A, and COMPUTER_USE (ACP is accepted as an A2A-routed alias).
29
+
30
+ Register an agent in three lines:
31
+
32
+ ```python
33
+ import emerge
34
+
35
+ @emerge.agent(name="My Agent", description="What I do")
36
+ def handle(task: str) -> str:
37
+ return f"handled: {task}"
38
+ ```
39
+
40
+ Then serve it:
41
+
42
+ ```bash
43
+ orcha-sdk run # serve locally + register against the local registry
44
+ ```
45
+
46
+ ## Install
47
+
48
+ No clone, no venv setup — just run it:
49
+
50
+ ```bash
51
+ uvx orcha-sdk init my-agent
52
+ ```
53
+
54
+ Or install it into a project:
55
+
56
+ ```bash
57
+ pip install orcha-sdk
58
+ ```
59
+
60
+ (Import name is `emerge`; distribution name is `orcha-sdk`.)
61
+
62
+ ## CLI
63
+
64
+ | Command | What it does |
65
+ |---|---|
66
+ | `orcha-sdk init "My Agent"` | scaffold a new agent from a template |
67
+ | `orcha-sdk run [module]` | serve decorated agents locally + register them against `http://localhost:8000` |
68
+ | `orcha-sdk publish [module] --registry <url>` | register against a remote registry |
69
+
70
+ `orcha-sdk run --no-register` serves without registering. Set `ORCHA_REGISTRY_URL`
71
+ and `ORCHA_PAT` to point at and authenticate against a non-local registry.
72
+
73
+ ## How it works
74
+
75
+ The decorator records your handler and declared skills. `orcha-sdk run` serves an
76
+ A2A-compatible HTTP endpoint (`/health`, `/.well-known/agent.json`, JSON-RPC
77
+ `message/send` / `tasks/get`) using only the standard library, and uploads a
78
+ generated `emerge.yaml` to the registry. The runtime's planner can then discover
79
+ and orchestrate your agent alongside agents that speak other protocols.
80
+
81
+ Apache 2.0 runtime, MIT SDK. See the [main repo](https://github.com/solvent-labs-org/metaorcha).
@@ -0,0 +1,58 @@
1
+ # orcha-sdk
2
+
3
+ The developer SDK + `emerge` CLI for [Orcha](https://github.com/solvent-labs-org/metaorcha) —
4
+ an orchestration runtime that plans, routes, and executes one goal across agents
5
+ speaking MCP, A2A, and COMPUTER_USE (ACP is accepted as an A2A-routed alias).
6
+
7
+ Register an agent in three lines:
8
+
9
+ ```python
10
+ import emerge
11
+
12
+ @emerge.agent(name="My Agent", description="What I do")
13
+ def handle(task: str) -> str:
14
+ return f"handled: {task}"
15
+ ```
16
+
17
+ Then serve it:
18
+
19
+ ```bash
20
+ orcha-sdk run # serve locally + register against the local registry
21
+ ```
22
+
23
+ ## Install
24
+
25
+ No clone, no venv setup — just run it:
26
+
27
+ ```bash
28
+ uvx orcha-sdk init my-agent
29
+ ```
30
+
31
+ Or install it into a project:
32
+
33
+ ```bash
34
+ pip install orcha-sdk
35
+ ```
36
+
37
+ (Import name is `emerge`; distribution name is `orcha-sdk`.)
38
+
39
+ ## CLI
40
+
41
+ | Command | What it does |
42
+ |---|---|
43
+ | `orcha-sdk init "My Agent"` | scaffold a new agent from a template |
44
+ | `orcha-sdk run [module]` | serve decorated agents locally + register them against `http://localhost:8000` |
45
+ | `orcha-sdk publish [module] --registry <url>` | register against a remote registry |
46
+
47
+ `orcha-sdk run --no-register` serves without registering. Set `ORCHA_REGISTRY_URL`
48
+ and `ORCHA_PAT` to point at and authenticate against a non-local registry.
49
+
50
+ ## How it works
51
+
52
+ The decorator records your handler and declared skills. `orcha-sdk run` serves an
53
+ A2A-compatible HTTP endpoint (`/health`, `/.well-known/agent.json`, JSON-RPC
54
+ `message/send` / `tasks/get`) using only the standard library, and uploads a
55
+ generated `emerge.yaml` to the registry. The runtime's planner can then discover
56
+ and orchestrate your agent alongside agents that speak other protocols.
57
+
58
+ Apache 2.0 runtime, MIT SDK. See the [main repo](https://github.com/solvent-labs-org/metaorcha).
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "orcha-sdk"
3
+ version = "0.1.1"
4
+ description = "Orcha agent SDK + CLI — register an agent in three lines."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Orcha" }]
9
+ keywords = ["ai-agents", "mcp", "a2a", "orchestration", "orcha", "emerge"]
10
+ dependencies = [
11
+ "pyyaml>=6.0",
12
+ ]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ "Topic :: System :: Distributed Computing",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/solvent-labs-org/metaorcha"
27
+ Repository = "https://github.com/solvent-labs-org/metaorcha"
28
+ Issues = "https://github.com/solvent-labs-org/metaorcha/issues"
29
+
30
+ [project.scripts]
31
+ # `uvx <name>` resolves the *distribution* name, so the primary console script
32
+ # must match it for `uvx orcha-sdk init` to work. `emerge` stays as an alias.
33
+ orcha-sdk = "emerge.cli:main"
34
+ emerge = "emerge.cli:main"
35
+
36
+ [build-system]
37
+ requires = ["hatchling"]
38
+ build-backend = "hatchling.build"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/emerge"]
@@ -0,0 +1,60 @@
1
+ """Orcha agent SDK — register an agent in three lines.
2
+
3
+ import emerge
4
+
5
+ @emerge.agent(name="My Agent", description="What I do")
6
+ def handle(task: str) -> str:
7
+ return f"handled: {task}"
8
+
9
+ if __name__ == "__main__":
10
+ emerge.run()
11
+
12
+ Or use the CLI: ``emerge run`` / ``emerge publish``.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import sys
18
+
19
+ from .manifest import build_manifest, manifest_yaml
20
+ from .sdk import AgentSpec, Skill, agent, registered_agents
21
+
22
+ __version__ = "0.1.1"
23
+
24
+ __all__ = [
25
+ "agent",
26
+ "Skill",
27
+ "AgentSpec",
28
+ "registered_agents",
29
+ "build_manifest",
30
+ "manifest_yaml",
31
+ "run",
32
+ "__version__",
33
+ ]
34
+
35
+
36
+ def run() -> int:
37
+ """Serve every decorated agent in this process (convenience for ``__main__``).
38
+
39
+ Agents are already registered via import, so this just serves them — it does
40
+ not register against a registry. Use the ``emerge run`` CLI for registration.
41
+ """
42
+ from .server import serve_agent
43
+
44
+ agents = registered_agents()
45
+ if not agents:
46
+ print(
47
+ "emerge.run(): no @emerge.agent registered in this module.", file=sys.stderr
48
+ )
49
+ return 1
50
+ for spec in agents[:-1]:
51
+ serve_agent(spec, block=False)
52
+ print(f"✓ Serving {spec.name} on http://localhost:{spec.port} ({spec.did})")
53
+ last = agents[-1]
54
+ print(f"✓ Serving {last.name} on http://localhost:{last.port} ({last.did})")
55
+ print("Press Ctrl+C to stop.")
56
+ try:
57
+ serve_agent(last, block=True)
58
+ except KeyboardInterrupt:
59
+ print("\nStopped.")
60
+ return 0
@@ -0,0 +1,311 @@
1
+ """Orcha agent CLI — the launch-scope developer surface.
2
+
3
+ Installed as ``orcha-sdk`` (and ``emerge``, kept as an alias). Four commands
4
+ only (resist scope creep — test/deploy/login are post-launch):
5
+
6
+ - ``orcha-sdk init [name]`` scaffold a new agent from the bundled template
7
+ - ``orcha-sdk run [module]`` serve decorated agents locally + register them
8
+ - ``orcha-sdk publish [module]`` register decorated agents against a remote registry
9
+ - ``orcha-sdk validate`` validator demo (``--once`` synthetic attestation)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import importlib.util
16
+ import json
17
+ import logging
18
+ import os
19
+ import sys
20
+ import threading
21
+ from datetime import UTC, datetime
22
+ from pathlib import Path
23
+
24
+ from . import __version__
25
+ from .client import DEFAULT_REGISTRY_URL, RegistryError, register
26
+ from .manifest import manifest_yaml
27
+ from .sdk import AgentSpec, clear_registry, registered_agents
28
+ from .server import serve_agent
29
+
30
+ logger = logging.getLogger("emerge")
31
+
32
+ _TEMPLATE_DIR = Path(__file__).parent / "templates" / "your-first-agent"
33
+
34
+
35
+ def _dan_experimental_enabled() -> bool:
36
+ """True when ORCHA_DAN_EXPERIMENTAL=1 (experimental network opt-in)."""
37
+ return os.getenv("ORCHA_DAN_EXPERIMENTAL", "").lower() in ("1", "true", "yes")
38
+
39
+
40
+ def _require_dan_experimental(feature: str) -> bool:
41
+ if _dan_experimental_enabled():
42
+ return True
43
+ print(
44
+ f"emerge: {feature} requires experimental network mode.\n"
45
+ " Set ORCHA_DAN_EXPERIMENTAL=1 or network.experimental: true in emerge.yaml.",
46
+ file=sys.stderr,
47
+ )
48
+ return False
49
+
50
+
51
+ def _load_module(module_path: str) -> None:
52
+ """Import a Python file by path so its @emerge.agent decorators register."""
53
+ path = Path(module_path).resolve()
54
+ if not path.exists():
55
+ sys.exit(f"emerge: no such file: {module_path}")
56
+ sys.path.insert(0, str(path.parent))
57
+ spec = importlib.util.spec_from_file_location(path.stem, path)
58
+ if spec is None or spec.loader is None:
59
+ sys.exit(f"emerge: cannot import {module_path}")
60
+ module = importlib.util.module_from_spec(spec)
61
+ sys.modules[path.stem] = module
62
+ spec.loader.exec_module(module)
63
+
64
+
65
+ def _discover(module_path: str) -> list[AgentSpec]:
66
+ clear_registry()
67
+ _load_module(module_path)
68
+ agents = registered_agents()
69
+ if not agents:
70
+ sys.exit(
71
+ f"emerge: no @emerge.agent found in {module_path}. "
72
+ "Did you decorate a handler?"
73
+ )
74
+ return agents
75
+
76
+
77
+ def _default_module() -> str:
78
+ for candidate in ("agent.py", "main.py"):
79
+ if Path(candidate).exists():
80
+ return candidate
81
+ sys.exit(
82
+ "emerge: no agent.py/main.py here. Pass a module path or run `emerge init`."
83
+ )
84
+
85
+
86
+ def cmd_init(args: argparse.Namespace) -> int:
87
+ name = args.name
88
+ slug = name.lower().replace(" ", "-")
89
+ dest = Path(args.dir or slug)
90
+ if dest.exists() and any(dest.iterdir()):
91
+ sys.exit(f"emerge: {dest} already exists and is not empty.")
92
+ dest.mkdir(parents=True, exist_ok=True)
93
+ for src in _TEMPLATE_DIR.rglob("*"):
94
+ rel = src.relative_to(_TEMPLATE_DIR)
95
+ target = dest / rel
96
+ if src.is_dir():
97
+ target.mkdir(parents=True, exist_ok=True)
98
+ continue
99
+ text = src.read_text(encoding="utf-8")
100
+ text = text.replace("{{AGENT_NAME}}", name).replace("{{AGENT_SLUG}}", slug)
101
+ target.write_text(text, encoding="utf-8")
102
+ print(f"✓ Scaffolded '{name}' in {dest}/")
103
+ print(" Next:")
104
+ print(f" cd {dest}")
105
+ print(" orcha-sdk run # serve locally; registers if a registry is up")
106
+ return 0
107
+
108
+
109
+ def cmd_run(args: argparse.Namespace) -> int:
110
+ module = args.module or _default_module()
111
+ agents = _discover(module)
112
+ registry_url = args.registry or os.getenv(
113
+ "ORCHA_REGISTRY_URL", DEFAULT_REGISTRY_URL
114
+ )
115
+ token = os.getenv("ORCHA_PAT")
116
+
117
+ # Every agent serves in a background thread first: the registry harvests
118
+ # capabilities from the live endpoint, so it has to be reachable before
119
+ # register() is called. The main thread parks afterwards — serving a port
120
+ # that is already bound raises OSError(EADDRINUSE).
121
+ for spec in agents:
122
+ serve_agent(spec, block=False)
123
+ print(f"✓ Serving {spec.name} on http://localhost:{spec.port} ({spec.did})")
124
+ if args.register:
125
+ try:
126
+ resp = register(
127
+ manifest_yaml(spec), registry_url=registry_url, token=token
128
+ )
129
+ print(
130
+ f" ✓ Registered with {registry_url}"
131
+ + (
132
+ f" (agent_id={resp.get('agent_id')})"
133
+ if resp.get("agent_id")
134
+ else ""
135
+ )
136
+ )
137
+ except RegistryError as exc:
138
+ print(f" ⚠ Registration skipped: {exc}", file=sys.stderr)
139
+ print(
140
+ " The agent is serving locally regardless. To register it, "
141
+ "start the runtime (./scripts/run-all.sh) or pass --no-register "
142
+ "to silence this.",
143
+ file=sys.stderr,
144
+ )
145
+
146
+ print("\nPress Ctrl+C to stop.")
147
+ try:
148
+ threading.Event().wait()
149
+ except KeyboardInterrupt:
150
+ print("\nStopped.")
151
+ return 0
152
+
153
+
154
+ def cmd_publish(args: argparse.Namespace) -> int:
155
+ if getattr(args, "network", None) and not _require_dan_experimental(
156
+ "emerge publish --network"
157
+ ):
158
+ return 1
159
+ module = args.module or _default_module()
160
+ agents = _discover(module)
161
+ registry_url = args.registry or os.getenv("ORCHA_REGISTRY_URL")
162
+ if not registry_url:
163
+ sys.exit("emerge publish: pass --registry <url> or set ORCHA_REGISTRY_URL.")
164
+ token = args.token or os.getenv("ORCHA_PAT")
165
+ host = args.host
166
+ failures = 0
167
+ for spec in agents:
168
+ try:
169
+ resp = register(
170
+ manifest_yaml(spec, host=host), registry_url=registry_url, token=token
171
+ )
172
+ print(
173
+ f"✓ Published {spec.name} → {registry_url}"
174
+ + (
175
+ f" (agent_id={resp.get('agent_id')})"
176
+ if resp.get("agent_id")
177
+ else ""
178
+ )
179
+ )
180
+ except RegistryError as exc:
181
+ failures += 1
182
+ print(f"✗ {spec.name}: {exc}", file=sys.stderr)
183
+ return 1 if failures else 0
184
+
185
+
186
+ def _synthetic_attestation(*, validator_did: str) -> dict:
187
+ """Build a demo attestation for `emerge validate --once`."""
188
+ success = True
189
+ content = "ok"
190
+ score = 0.85 if success and content and not content.startswith("Error:") else 0.2
191
+ return {
192
+ "schema_version": "1.0",
193
+ "call_id": "call-validate-demo",
194
+ "agent_id": "did:orcha:agent:demo",
195
+ "validator_did": validator_did,
196
+ "success": success,
197
+ "latency_ms": 42,
198
+ "judge_score": score,
199
+ "notes": "spike-heuristic (--once demo)",
200
+ "observed_at": datetime.now(UTC).isoformat(),
201
+ }
202
+
203
+
204
+ def cmd_validate(args: argparse.Namespace) -> int:
205
+ validator_did = args.validator_did or os.getenv(
206
+ "VALIDATOR_DID", "did:orcha:validator:local"
207
+ )
208
+ if args.once:
209
+ attestation = _synthetic_attestation(validator_did=validator_did)
210
+ print(json.dumps(attestation, indent=2))
211
+ print(
212
+ f"\n✓ Demo attestation for validator {validator_did} "
213
+ "(subscribe to execution.step_complete for live traffic)"
214
+ )
215
+ return 0
216
+ if not _require_dan_experimental("emerge validate (live mode)"):
217
+ return 1
218
+ if args.kafka:
219
+ print(
220
+ "emerge validate: Kafka consumer is not wired yet.\n"
221
+ " Use --once for a local demo, or run a validator process against "
222
+ "execution.step_complete when KAFKA_ENABLED=true.",
223
+ file=sys.stderr,
224
+ )
225
+ return 1
226
+ print(
227
+ "emerge validate: pass --once for a demo attestation, or --kafka <brokers> "
228
+ "(consumer TBD).",
229
+ file=sys.stderr,
230
+ )
231
+ return 1
232
+
233
+
234
+ def build_parser() -> argparse.ArgumentParser:
235
+ p = argparse.ArgumentParser(description="Orcha agent developer CLI")
236
+ p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
237
+ sub = p.add_subparsers(dest="command", required=True)
238
+
239
+ pi = sub.add_parser("init", help="scaffold a new agent from a template")
240
+ pi.add_argument("name", help="agent name, e.g. 'My Agent'")
241
+ pi.add_argument("--dir", help="target directory (default: slug of name)")
242
+ pi.set_defaults(func=cmd_init)
243
+
244
+ pr = sub.add_parser(
245
+ "run", help="serve agents locally + register against local registry"
246
+ )
247
+ pr.add_argument(
248
+ "module", nargs="?", help="path to agent module (default: agent.py/main.py)"
249
+ )
250
+ pr.add_argument(
251
+ "--registry", help=f"registry URL (default: {DEFAULT_REGISTRY_URL})"
252
+ )
253
+ pr.add_argument(
254
+ "--no-register",
255
+ dest="register",
256
+ action="store_false",
257
+ help="serve only; do not register",
258
+ )
259
+ pr.set_defaults(func=cmd_run, register=True)
260
+
261
+ pp = sub.add_parser("publish", help="register agents against a remote registry")
262
+ pp.add_argument(
263
+ "module", nargs="?", help="path to agent module (default: agent.py/main.py)"
264
+ )
265
+ pp.add_argument("--registry", help="remote registry URL (or ORCHA_REGISTRY_URL)")
266
+ pp.add_argument("--token", help="PAT token (or ORCHA_PAT)")
267
+ pp.add_argument(
268
+ "--host",
269
+ default="localhost",
270
+ help="host advertised in the manifest endpoint (default: localhost)",
271
+ )
272
+ pp.add_argument(
273
+ "--network",
274
+ help="network bootstrap peer for publish (experimental)",
275
+ )
276
+ pp.set_defaults(func=cmd_publish)
277
+
278
+ pv = sub.add_parser(
279
+ "validate",
280
+ help="run a validator observer node (experimental — --once demo)",
281
+ )
282
+ pv.add_argument(
283
+ "--validator-did",
284
+ help="validator DID (default: did:orcha:validator:local or VALIDATOR_DID)",
285
+ )
286
+ pv.add_argument(
287
+ "--bootstrap",
288
+ help="network bootstrap peer (reserved)",
289
+ )
290
+ pv.add_argument(
291
+ "--kafka",
292
+ help="Kafka bootstrap servers for execution.step_complete (consumer TBD)",
293
+ )
294
+ pv.add_argument(
295
+ "--once",
296
+ action="store_true",
297
+ help="emit one synthetic attestation and exit (local demo)",
298
+ )
299
+ pv.set_defaults(func=cmd_validate)
300
+
301
+ return p
302
+
303
+
304
+ def main(argv: list[str] | None = None) -> int:
305
+ logging.basicConfig(level=os.getenv("EMERGE_LOG_LEVEL", "INFO"))
306
+ args = build_parser().parse_args(argv)
307
+ return args.func(args)
308
+
309
+
310
+ if __name__ == "__main__":
311
+ raise SystemExit(main())
@@ -0,0 +1,71 @@
1
+ """Minimal registry client — register an agent's emerge.yaml.
2
+
3
+ Uses only the standard library (urllib) so the SDK stays dependency-light.
4
+ Registration mirrors ``POST /api/v1/agents/register`` (multipart upload of the
5
+ ``emerge.yaml`` file, optional ``Authorization: Bearer <PAT>``).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ import urllib.error
13
+ import urllib.request
14
+ import uuid
15
+
16
+ logger = logging.getLogger("emerge.client")
17
+
18
+ DEFAULT_REGISTRY_URL = "http://localhost:8000"
19
+ _REGISTER_PATH = "/api/v1/agents/register"
20
+
21
+
22
+ class RegistryError(RuntimeError):
23
+ """Raised when registration fails."""
24
+
25
+
26
+ def _multipart_body(yaml_text: str, *, field: str = "emerge_yaml") -> tuple[bytes, str]:
27
+ boundary = f"----emerge{uuid.uuid4().hex}"
28
+ parts = [
29
+ f"--{boundary}",
30
+ f'Content-Disposition: form-data; name="{field}"; filename="emerge.yaml"',
31
+ "Content-Type: application/x-yaml",
32
+ "",
33
+ yaml_text,
34
+ f"--{boundary}--",
35
+ "",
36
+ ]
37
+ body = "\r\n".join(parts).encode("utf-8")
38
+ return body, boundary
39
+
40
+
41
+ def register(
42
+ yaml_text: str,
43
+ *,
44
+ registry_url: str = DEFAULT_REGISTRY_URL,
45
+ token: str | None = None,
46
+ timeout: float = 30.0,
47
+ ) -> dict:
48
+ """Register an agent manifest against a registry. Returns the JSON response.
49
+
50
+ Raises :class:`RegistryError` on non-2xx responses or transport errors.
51
+ """
52
+ url = registry_url.rstrip("/") + _REGISTER_PATH
53
+ body, boundary = _multipart_body(yaml_text)
54
+ req = urllib.request.Request(url, data=body, method="POST") # noqa: S310 - http(s) only
55
+ req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")
56
+ if token:
57
+ req.add_header("Authorization", f"Bearer {token}")
58
+ try:
59
+ with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310
60
+ payload = resp.read().decode("utf-8")
61
+ return json.loads(payload) if payload else {}
62
+ except urllib.error.HTTPError as exc:
63
+ detail = exc.read().decode("utf-8", "replace")
64
+ raise RegistryError(
65
+ f"Registry returned {exc.code} for {url}: {detail}"
66
+ ) from exc
67
+ except urllib.error.URLError as exc:
68
+ raise RegistryError(
69
+ f"Could not reach registry at {registry_url} — is the local stack up? "
70
+ f"({exc.reason})"
71
+ ) from exc
@@ -0,0 +1,56 @@
1
+ """Generate an ``emerge.yaml`` manifest from an :class:`AgentSpec`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .sdk import AgentSpec
8
+
9
+ SCHEMA_VERSION = "1.0"
10
+
11
+
12
+ def build_manifest(spec: AgentSpec, *, host: str = "localhost") -> dict[str, Any]:
13
+ """Build the manifest dict the registry validates against emerge.yaml schema."""
14
+ endpoint = f"http://{host}:{spec.port}"
15
+ manifest: dict[str, Any] = {
16
+ "schema_version": SCHEMA_VERSION,
17
+ "identity": {
18
+ "id": spec.did,
19
+ "name": spec.name,
20
+ "version": spec.version,
21
+ "description": spec.description,
22
+ "tags": spec.tags,
23
+ },
24
+ "protocol": {
25
+ "type": "a2a",
26
+ "version": "1.0",
27
+ "transport": {"type": "http", "endpoint": endpoint},
28
+ },
29
+ "health_endpoint": f"{endpoint}/health",
30
+ "security": {
31
+ "transport_layer": {"type": "none"},
32
+ "auth_strategies": [],
33
+ },
34
+ "skills": [
35
+ {
36
+ "id": s.id,
37
+ "name": s.name,
38
+ "description": s.description,
39
+ "tags": s.tags,
40
+ "examples": s.examples,
41
+ }
42
+ for s in spec.skills
43
+ ],
44
+ }
45
+ if spec.base_fee is not None:
46
+ manifest["payment"] = {"enabled": True, "base_fee": spec.base_fee}
47
+ return manifest
48
+
49
+
50
+ def manifest_yaml(spec: AgentSpec, *, host: str = "localhost") -> str:
51
+ """Serialize the manifest to YAML text."""
52
+ import yaml
53
+
54
+ return yaml.safe_dump(
55
+ build_manifest(spec, host=host), sort_keys=False, default_flow_style=False
56
+ )
@@ -0,0 +1,129 @@
1
+ """The ``@emerge.agent`` decorator and the in-process agent registry.
2
+
3
+ Register an agent in three lines::
4
+
5
+ import emerge
6
+
7
+ @emerge.agent(name="My Agent", description="What I do")
8
+ def handle(task: str) -> str:
9
+ return f"handled: {task}"
10
+
11
+ Then ``emerge run`` (CLI) — or ``emerge.run()`` in ``__main__`` — serves it over
12
+ an A2A-compatible HTTP endpoint and registers it against a local registry.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import inspect
18
+ import re
19
+ from collections.abc import Callable
20
+ from dataclasses import dataclass, field
21
+ from typing import Any
22
+
23
+ Handler = Callable[[str], Any]
24
+
25
+
26
+ def _slugify(name: str) -> str:
27
+ slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
28
+ return slug or "agent"
29
+
30
+
31
+ @dataclass
32
+ class Skill:
33
+ """A declared capability, surfaced on the agent card and in the manifest."""
34
+
35
+ id: str
36
+ name: str = ""
37
+ description: str = ""
38
+ tags: list[str] = field(default_factory=list)
39
+ examples: list[str] = field(default_factory=list)
40
+
41
+ def __post_init__(self) -> None:
42
+ if not self.name:
43
+ self.name = self.id.replace("-", " ").replace("_", " ").title()
44
+
45
+
46
+ @dataclass
47
+ class AgentSpec:
48
+ """Everything the runtime needs to serve, describe, and register an agent."""
49
+
50
+ name: str
51
+ description: str
52
+ handler: Handler
53
+ did: str
54
+ version: str = "0.1.0"
55
+ port: int = 8900
56
+ tags: list[str] = field(default_factory=list)
57
+ skills: list[Skill] = field(default_factory=list)
58
+ base_fee: str | None = None
59
+
60
+ async def invoke(self, task: str) -> str:
61
+ """Call the handler, awaiting it if it is a coroutine. Returns text."""
62
+ result = self.handler(task)
63
+ if inspect.isawaitable(result):
64
+ result = await result
65
+ return result if isinstance(result, str) else str(result)
66
+
67
+
68
+ # Process-wide registry of decorated agents, in declaration order.
69
+ _AGENTS: list[AgentSpec] = []
70
+
71
+
72
+ def agent(
73
+ _fn: Handler | None = None,
74
+ *,
75
+ name: str,
76
+ description: str,
77
+ did: str | None = None,
78
+ version: str = "0.1.0",
79
+ port: int = 8900,
80
+ tags: list[str] | None = None,
81
+ skills: list[Skill | dict[str, Any]] | None = None,
82
+ base_fee: str | None = None,
83
+ ) -> Callable[[Handler], Handler] | Handler:
84
+ """Register ``fn`` as an Orcha agent. Returns ``fn`` unchanged.
85
+
86
+ ``did`` defaults to ``did:orcha:agent:<slug-of-name>``. If no ``skills`` are
87
+ given, a single skill is derived from the agent name so the agent card is
88
+ never empty (the registry harvests skills from the card at registration).
89
+ """
90
+
91
+ def decorate(fn: Handler) -> Handler:
92
+ slug = _slugify(name)
93
+ resolved_did = did or f"did:orcha:agent:{slug}"
94
+ resolved_skills: list[Skill] = []
95
+ for s in skills or []:
96
+ resolved_skills.append(s if isinstance(s, Skill) else Skill(**s))
97
+ if not resolved_skills:
98
+ resolved_skills = [
99
+ Skill(id=slug, name=name, description=description, tags=tags or [])
100
+ ]
101
+ _AGENTS.append(
102
+ AgentSpec(
103
+ name=name,
104
+ description=description,
105
+ handler=fn,
106
+ did=resolved_did,
107
+ version=version,
108
+ port=port,
109
+ tags=tags or [],
110
+ skills=resolved_skills,
111
+ base_fee=base_fee,
112
+ )
113
+ )
114
+ return fn
115
+
116
+ # Support both @agent(...) and bare @agent usage.
117
+ if _fn is not None:
118
+ return decorate(_fn)
119
+ return decorate
120
+
121
+
122
+ def registered_agents() -> list[AgentSpec]:
123
+ """Return all agents registered via the decorator, in declaration order."""
124
+ return list(_AGENTS)
125
+
126
+
127
+ def clear_registry() -> None:
128
+ """Test helper — drop all registered agents."""
129
+ _AGENTS.clear()
@@ -0,0 +1,190 @@
1
+ """A dependency-light A2A-compatible HTTP server for SDK agents.
2
+
3
+ Implements the same wire contract the Orcha SuperAgent speaks to A2A agents,
4
+ using only the standard library so ``emerge run`` has no heavy dependencies:
5
+
6
+ - ``GET /health`` → liveness probe
7
+ - ``GET /.well-known/agent.json`` → A2A agent card (registry harvests skills)
8
+ - ``POST /`` → JSON-RPC 2.0: ``message/send`` + ``tasks/get``
9
+
10
+ Tasks complete synchronously, so ``message/send`` returns a completed task and
11
+ ``tasks/get`` reads it back from a small in-memory store.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import json
18
+ import logging
19
+ import os
20
+ import threading
21
+ import uuid
22
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
23
+ from typing import Any
24
+
25
+ from .sdk import AgentSpec
26
+
27
+ logger = logging.getLogger("emerge.server")
28
+
29
+
30
+ def agent_card(spec: AgentSpec, host: str = "localhost") -> dict[str, Any]:
31
+ """Build the A2A agent card served at /.well-known/agent.json."""
32
+ return {
33
+ "schemaVersion": "1.0",
34
+ "name": spec.name,
35
+ "description": spec.description,
36
+ "url": f"http://{host}:{spec.port}",
37
+ "version": spec.version,
38
+ "capabilities": {"streaming": False, "pushNotifications": False},
39
+ "skills": [
40
+ {
41
+ "id": s.id,
42
+ "name": s.name,
43
+ "description": s.description,
44
+ "tags": s.tags,
45
+ "examples": s.examples,
46
+ }
47
+ for s in spec.skills
48
+ ],
49
+ }
50
+
51
+
52
+ def _extract_text(message: dict[str, Any]) -> str:
53
+ parts = message.get("parts", []) or []
54
+ texts = [
55
+ p.get("text", "")
56
+ for p in parts
57
+ if p.get("kind") == "text" or p.get("type") == "text"
58
+ ]
59
+ return " ".join(texts).strip()
60
+
61
+
62
+ def _completed_task(task_id: str, answer: str) -> dict[str, Any]:
63
+ return {
64
+ "id": task_id,
65
+ "status": {"state": "completed"},
66
+ "artifacts": [{"parts": [{"kind": "text", "text": answer}]}],
67
+ }
68
+
69
+
70
+ def _failed_task(task_id: str, error: str) -> dict[str, Any]:
71
+ return {
72
+ "id": task_id,
73
+ "status": {
74
+ "state": "failed",
75
+ "message": {
76
+ "role": "agent",
77
+ "parts": [{"kind": "text", "text": f"Error: {error}"}],
78
+ },
79
+ },
80
+ }
81
+
82
+
83
+ def _make_handler(spec: AgentSpec, host: str):
84
+ card = agent_card(spec, host=host)
85
+ task_store: dict[str, dict[str, Any]] = {}
86
+
87
+ class Handler(BaseHTTPRequestHandler):
88
+ protocol_version = "HTTP/1.1"
89
+
90
+ def log_message(self, fmt: str, *args: Any) -> None: # quieter logs
91
+ logger.debug("%s - %s", self.address_string(), fmt % args)
92
+
93
+ def _send_json(self, payload: dict[str, Any], status: int = 200) -> None:
94
+ body = json.dumps(payload).encode("utf-8")
95
+ self.send_response(status)
96
+ self.send_header("Content-Type", "application/json")
97
+ self.send_header("Content-Length", str(len(body)))
98
+ self.send_header("Access-Control-Allow-Origin", "*")
99
+ self.end_headers()
100
+ self.wfile.write(body)
101
+
102
+ def do_GET(self) -> None: # noqa: N802
103
+ if self.path == "/health":
104
+ self._send_json(
105
+ {"status": "healthy", "agent": spec.name, "version": spec.version}
106
+ )
107
+ elif self.path == "/.well-known/agent.json":
108
+ self._send_json(card)
109
+ else:
110
+ self._send_json({"error": "not found"}, status=404)
111
+
112
+ def do_POST(self) -> None: # noqa: N802
113
+ length = int(self.headers.get("Content-Length", 0) or 0)
114
+ try:
115
+ body = json.loads(self.rfile.read(length) or b"{}")
116
+ except json.JSONDecodeError:
117
+ self._send_json(
118
+ {
119
+ "jsonrpc": "2.0",
120
+ "id": "",
121
+ "error": {"code": -32700, "message": "Parse error"},
122
+ },
123
+ status=400,
124
+ )
125
+ return
126
+
127
+ rpc_id = body.get("id", "")
128
+ method = body.get("method", "")
129
+ params = body.get("params", {}) or {}
130
+
131
+ if method == "message/send":
132
+ message = params.get("message", {}) or {}
133
+ task_id = (
134
+ params.get("taskId") or message.get("taskId") or str(uuid.uuid4())
135
+ )
136
+ query = _extract_text(message)
137
+ try:
138
+ answer = asyncio.run(spec.invoke(query))
139
+ task = _completed_task(task_id, answer)
140
+ except Exception as exc: # noqa: BLE001 - surface as failed task
141
+ logger.exception("Handler raised for task %s", task_id)
142
+ task = _failed_task(task_id, str(exc))
143
+ task_store[task_id] = task
144
+ self._send_json({"jsonrpc": "2.0", "id": rpc_id, "result": task})
145
+ elif method == "tasks/get":
146
+ task_id = params.get("id", "")
147
+ task = task_store.get(task_id)
148
+ if task is None:
149
+ self._send_json(
150
+ {
151
+ "jsonrpc": "2.0",
152
+ "id": rpc_id,
153
+ "error": {
154
+ "code": -32602,
155
+ "message": f"Task {task_id!r} not found",
156
+ },
157
+ }
158
+ )
159
+ else:
160
+ self._send_json({"jsonrpc": "2.0", "id": rpc_id, "result": task})
161
+ else:
162
+ self._send_json(
163
+ {
164
+ "jsonrpc": "2.0",
165
+ "id": rpc_id,
166
+ "error": {
167
+ "code": -32601,
168
+ "message": f"Method not found: {method!r}",
169
+ },
170
+ }
171
+ )
172
+
173
+ return Handler
174
+
175
+
176
+ def serve_agent(
177
+ spec: AgentSpec, host: str = "0.0.0.0", *, block: bool = True
178
+ ) -> ThreadingHTTPServer:
179
+ """Start an HTTP server for a single agent. Returns the server.
180
+
181
+ When ``block`` is False the server runs in a daemon thread (useful for tests
182
+ and multi-agent serving); otherwise this call blocks via ``serve_forever``.
183
+ """
184
+ card_host = os.getenv("EMERGE_ADVERTISE_HOST", "localhost")
185
+ httpd = ThreadingHTTPServer((host, spec.port), _make_handler(spec, card_host))
186
+ if block:
187
+ httpd.serve_forever()
188
+ else:
189
+ threading.Thread(target=httpd.serve_forever, daemon=True).start()
190
+ return httpd
@@ -0,0 +1,31 @@
1
+ # {{AGENT_NAME}}
2
+
3
+ Your first Orcha agent, scaffolded with `orcha-sdk init`.
4
+
5
+ ## Run it
6
+
7
+ ```bash
8
+ pip install orcha-sdk # if you haven't already
9
+ orcha-sdk run # serve + register against http://localhost:8000
10
+ ```
11
+
12
+ Or serve without registering:
13
+
14
+ ```bash
15
+ orcha-sdk run --no-register
16
+ ```
17
+
18
+ ## What's here
19
+
20
+ - `agent.py` — the agent. A decorated `handle(task)` function is the whole thing.
21
+ - `requirements.txt` — just `orcha-sdk`.
22
+
23
+ ## Next steps
24
+
25
+ 1. Edit `handle()` in `agent.py` with your real logic.
26
+ 2. Update the `description` and `skills` — the planner uses them to route to you.
27
+ 3. `orcha-sdk publish --registry <url>` to register against a remote registry.
28
+
29
+ DID: `did:orcha:agent:{{AGENT_SLUG}}` · Manifest: generated from the decorator.
30
+ See the [bridges guide](https://metaorcha.ai/docs)
31
+ to connect a whole other protocol.
@@ -0,0 +1,55 @@
1
+ """{{AGENT_NAME}} — your first Orcha agent.
2
+
3
+ An agent is just a function that takes a natural-language `task` string and
4
+ returns a result string. The `@emerge.agent` decorator does the rest:
5
+
6
+ - gives the agent a DID (did:orcha:agent:{{AGENT_SLUG}})
7
+ - generates the emerge.yaml manifest the registry validates
8
+ - serves it over an A2A-compatible HTTP endpoint when you run it
9
+
10
+ Try it:
11
+
12
+ orcha-sdk run # serve + register against the local registry
13
+ orcha-sdk run --no-register # serve only
14
+
15
+ Then ask the orchestrator to do something this agent can handle, or call it
16
+ directly:
17
+
18
+ curl -s localhost:8900/.well-known/agent.json
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import emerge
24
+
25
+
26
+ @emerge.agent(
27
+ name="{{AGENT_NAME}}",
28
+ description="Describe in one sentence what this agent does — the planner reads this to decide when to route to you.",
29
+ version="0.1.0",
30
+ port=8900,
31
+ tags=["example"],
32
+ # Declare the things your agent can do. Skills are surfaced on the agent
33
+ # card and harvested by the registry. Good descriptions + examples make the
34
+ # planner route to you correctly.
35
+ skills=[
36
+ {
37
+ "id": "echo",
38
+ "name": "Echo",
39
+ "description": "Echo the task back. Replace this with your real capability.",
40
+ "examples": ["say hello", "repeat after me: orcha"],
41
+ }
42
+ ],
43
+ # Uncomment to charge per invocation (mock mode by default — no wallet needed):
44
+ # base_fee="0.05",
45
+ )
46
+ def handle(task: str) -> str:
47
+ """Handle one task. Replace the body with your real logic.
48
+
49
+ The handler may be sync or async (`async def handle(...)`).
50
+ """
51
+ return f"{{AGENT_NAME}} received: {task}"
52
+
53
+
54
+ if __name__ == "__main__":
55
+ emerge.run()
@@ -0,0 +1,125 @@
1
+ """End-to-end-ish tests for the emerge SDK: decorator → manifest → A2A server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.request
7
+
8
+ import pytest
9
+
10
+ import emerge
11
+ from emerge.manifest import build_manifest
12
+ from emerge.sdk import clear_registry, registered_agents
13
+ from emerge.server import agent_card, serve_agent
14
+
15
+
16
+ @pytest.fixture(autouse=True)
17
+ def _clean_registry():
18
+ clear_registry()
19
+ yield
20
+ clear_registry()
21
+
22
+
23
+ def test_decorator_registers_agent_with_default_did():
24
+ @emerge.agent(name="My Agent", description="does things")
25
+ def handle(task: str) -> str:
26
+ return task
27
+
28
+ agents = registered_agents()
29
+ assert len(agents) == 1
30
+ spec = agents[0]
31
+ assert spec.did == "did:orcha:agent:my-agent"
32
+ assert spec.name == "My Agent"
33
+ # No explicit skills → one derived skill so the agent card is never empty.
34
+ assert spec.skills and spec.skills[0].id == "my-agent"
35
+
36
+
37
+ def test_decorator_returns_callable_unchanged():
38
+ @emerge.agent(name="X", description="d")
39
+ def handle(task: str) -> str:
40
+ return "ok:" + task
41
+
42
+ assert handle("hi") == "ok:hi"
43
+
44
+
45
+ def test_manifest_shape_matches_registry_schema():
46
+ @emerge.agent(name="Priced", description="d", base_fee="0.10", port=8901)
47
+ def handle(task: str) -> str:
48
+ return task
49
+
50
+ m = build_manifest(registered_agents()[0])
51
+ assert m["schema_version"] == "1.0"
52
+ assert m["identity"]["id"] == "did:orcha:agent:priced"
53
+ assert m["protocol"]["type"] == "a2a"
54
+ assert m["protocol"]["transport"]["endpoint"] == "http://localhost:8901"
55
+ assert m["health_endpoint"] == "http://localhost:8901/health"
56
+ assert m["payment"] == {"enabled": True, "base_fee": "0.10"}
57
+
58
+
59
+ def test_agent_card_is_valid():
60
+ @emerge.agent(name="Carded", description="d", port=8902)
61
+ def handle(task: str) -> str:
62
+ return task
63
+
64
+ card = agent_card(registered_agents()[0])
65
+ assert card["name"] == "Carded"
66
+ assert card["skills"]
67
+ assert card["url"] == "http://localhost:8902"
68
+
69
+
70
+ def _post_jsonrpc(port: int, method: str, params: dict) -> dict:
71
+ body = json.dumps({"jsonrpc": "2.0", "id": "1", "method": method, "params": params}).encode()
72
+ req = urllib.request.Request(
73
+ f"http://127.0.0.1:{port}/", data=body,
74
+ headers={"Content-Type": "application/json"}, method="POST",
75
+ )
76
+ with urllib.request.urlopen(req, timeout=5) as resp:
77
+ return json.loads(resp.read())
78
+
79
+
80
+ def _get(port: int, path: str) -> dict:
81
+ with urllib.request.urlopen(f"http://127.0.0.1:{port}{path}", timeout=5) as resp:
82
+ return json.loads(resp.read())
83
+
84
+
85
+ def test_server_serves_health_card_and_executes_task():
86
+ port = 8943
87
+
88
+ @emerge.agent(name="Live Agent", description="echoes", port=port)
89
+ def handle(task: str) -> str:
90
+ return f"echo: {task}"
91
+
92
+ httpd = serve_agent(registered_agents()[0], host="127.0.0.1", block=False)
93
+ try:
94
+ assert _get(port, "/health")["status"] == "healthy"
95
+ assert _get(port, "/.well-known/agent.json")["name"] == "Live Agent"
96
+
97
+ result = _post_jsonrpc(port, "message/send", {
98
+ "message": {"parts": [{"kind": "text", "text": "hello"}]},
99
+ })["result"]
100
+ assert result["status"]["state"] == "completed"
101
+ answer = result["artifacts"][0]["parts"][0]["text"]
102
+ assert answer == "echo: hello"
103
+
104
+ # tasks/get reads the same task back.
105
+ fetched = _post_jsonrpc(port, "tasks/get", {"id": result["id"]})["result"]
106
+ assert fetched["id"] == result["id"]
107
+ finally:
108
+ httpd.shutdown()
109
+
110
+
111
+ def test_async_handler_supported():
112
+ port = 8944
113
+
114
+ @emerge.agent(name="Async Agent", description="async", port=port)
115
+ async def handle(task: str) -> str:
116
+ return f"async: {task}"
117
+
118
+ httpd = serve_agent(registered_agents()[0], host="127.0.0.1", block=False)
119
+ try:
120
+ result = _post_jsonrpc(port, "message/send", {
121
+ "message": {"parts": [{"type": "text", "text": "x"}]},
122
+ })["result"]
123
+ assert result["artifacts"][0]["parts"][0]["text"] == "async: x"
124
+ finally:
125
+ httpd.shutdown()