asm-protocol 0.5.2__tar.gz → 0.6.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.
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/PKG-INFO +38 -10
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/README.md +37 -9
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/asm_cli.py +14 -3
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/asm_protocol.egg-info/PKG-INFO +38 -10
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/asm_protocol.egg-info/SOURCES.txt +11 -1
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/asm_protocol.egg-info/top_level.txt +1 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/asm_select_api.py +25 -15
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/asm_selector_mcp.py +29 -14
- asm_protocol-0.6.0/library_select.py +11 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/pyproject.toml +13 -2
- asm_protocol-0.6.0/schema/selection-receipt-v0.1.schema.json +182 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/scorer/__init__.py +1 -1
- asm_protocol-0.6.0/scorer/data/__init__.py +1 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/scorer/scorer.py +10 -2
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/scorer/test_asm_lint.py +14 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/scorer/test_asm_selector_mcp.py +1 -0
- asm_protocol-0.6.0/scorer/test_cost_estimation.py +118 -0
- asm_protocol-0.6.0/scorer/test_langchain_adapter.py +114 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/scorer/test_library_select.py +106 -11
- asm_protocol-0.6.0/scorer/test_version_contract.py +35 -0
- asm_protocol-0.6.0/src/asm_protocol/__init__.py +15 -0
- asm_protocol-0.6.0/src/asm_protocol/cost.py +202 -0
- asm_protocol-0.6.0/src/asm_protocol/integrations/__init__.py +2 -0
- asm_protocol-0.6.0/src/asm_protocol/integrations/langchain.py +258 -0
- asm_protocol-0.6.0/src/asm_protocol/selection.py +487 -0
- asm_protocol-0.6.0/src/asm_protocol/version.py +7 -0
- asm_protocol-0.5.2/library_select.py +0 -218
- asm_protocol-0.5.2/scorer/test_langchain_adapter.py +0 -54
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/LICENSE +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/_asm_library_data.py +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/asm_lint.py +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/asm_protocol.egg-info/dependency_links.txt +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/asm_protocol.egg-info/entry_points.txt +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/asm_protocol.egg-info/requires.txt +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/mcp_server_json_asm.py +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/openrouter_adapter.py +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/schema/__init__.py +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/schema/asm-receipt-envelope-v0.1.schema.json +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/schema/asm-v0.2.schema.json +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/schema/asm-v0.3.schema.json +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/scorer/data/elo_snapshot.json +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/scorer/test_data_quality_audit.py +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/scorer/test_manifests_schema.py +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/scorer/test_mcp_server_json_asm.py +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/scorer/test_openrouter_adapter.py +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/scorer/test_scorer.py +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/scorer/test_x402_bridge.py +0 -0
- {asm_protocol-0.5.2 → asm_protocol-0.6.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asm-protocol
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Agent Service Manifest: value metadata and scoring for autonomous service selection
|
|
5
5
|
Author: Yi Guo
|
|
6
6
|
License-Expression: MIT
|
|
@@ -41,7 +41,7 @@ It accepts either a standalone ASM manifest or an MCP Registry `server.json`
|
|
|
41
41
|
with publisher-provided ASM metadata:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
python -m pip install "asm-protocol==0.
|
|
44
|
+
python -m pip install "asm-protocol==0.6.0"
|
|
45
45
|
asm-lint server.json --format markdown --output asm-lint-report.md
|
|
46
46
|
```
|
|
47
47
|
|
|
@@ -58,7 +58,7 @@ steps:
|
|
|
58
58
|
- uses: actions/setup-python@v5
|
|
59
59
|
with:
|
|
60
60
|
python-version: "3.12"
|
|
61
|
-
- uses: YE-YI7/asm-spec/.github/actions/asm-lint@v0.
|
|
61
|
+
- uses: YE-YI7/asm-spec/.github/actions/asm-lint@v0.6.0
|
|
62
62
|
with:
|
|
63
63
|
path: server.json
|
|
64
64
|
fail-on: invalid
|
|
@@ -67,6 +67,8 @@ steps:
|
|
|
67
67
|
The Action adds the full Markdown report to the job summary. It does not call
|
|
68
68
|
an ASM-hosted API or upload the inspected manifest. See the
|
|
69
69
|
[lint and CI guide](docs/adoption/asm-lint.md) for status semantics.
|
|
70
|
+
For a bounded producer-side change, use the
|
|
71
|
+
[10-minute adoption package](docs/adoption/ten-minute.md).
|
|
70
72
|
|
|
71
73
|
## Try it: pick a tool for a task
|
|
72
74
|
|
|
@@ -75,7 +77,13 @@ git clone https://github.com/YE-YI7/asm-spec.git && cd asm-spec
|
|
|
75
77
|
python library/select_demo.py
|
|
76
78
|
```
|
|
77
79
|
|
|
78
|
-
|
|
80
|
+
The deterministic core does **not** pretend to understand the task sentence. The
|
|
81
|
+
caller supplies structured facts such as `taxonomy`, `required_functions`,
|
|
82
|
+
platform, reach, and expected workload; `task` remains audit/display text. If
|
|
83
|
+
taxonomy and required functions are both absent, selection returns
|
|
84
|
+
`under_specified` instead of choosing an unrelated globally cheap tool.
|
|
85
|
+
|
|
86
|
+
For *"make a study plan and remind me daily"* with a cloud agent on Windows, the selector drops the tools it can't drive (Apple Reminders, Things 3 — local-device only) and the ones it can't call directly (Any.do — Zapier only), then ranks the rest from those explicit constraints. Ask for a built-in pomodoro and the pick changes to TickTick. Ask to *"edit an image and lay out a poster"* and it filters **Affinity Designer**, which exposes no automation API at all.
|
|
79
87
|
|
|
80
88
|
The library it selects over is in [`library/`](library/) — 30 real tools across task management, creative design, research, communication, developer tools, booking, and real-estate data today, each carrying:
|
|
81
89
|
|
|
@@ -105,31 +113,50 @@ ASM is MCP-compatible: publish a standalone `.well-known/asm`, or embed ASM in M
|
|
|
105
113
|
ASM ships an MCP server so any MCP client (Claude Desktop, Cursor, an agent host) can call the selector as a tool — no schema adoption required:
|
|
106
114
|
|
|
107
115
|
```bash
|
|
108
|
-
python3 -m pip install "asm-protocol[mcp]==0.
|
|
116
|
+
python3 -m pip install "asm-protocol[mcp]==0.6.0"
|
|
109
117
|
asm-selector # stdio MCP server (MCP SDK 2.x)
|
|
110
118
|
```
|
|
111
119
|
|
|
112
120
|
It exposes three tools: **`select_tool`** (pick a tool for a task and return its risk/approval policy), **`list_library_tools`**, and **`get_tool_manifest`**. Point your client's MCP config at `asm-selector`, or at `python3 /path/to/asm-spec/asm_selector_mcp.py`; the selector reads `library/` (override with `ASM_LIBRARY_DIR`). The Python server uses the stable MCP SDK 2.x line and supports the modern `2026-07-28` protocol era. The same selector is importable directly: `from library_select import select`.
|
|
113
121
|
|
|
122
|
+
Cost output has an explicit `known`, `partial`, or `unknown` status. Metered
|
|
123
|
+
prices need expected monthly usage; one-time licenses need an amortization
|
|
124
|
+
period; prose-only free tiers remain unknown because their allowance and reset
|
|
125
|
+
rules are not machine-readable. If every eligible candidate does not have a
|
|
126
|
+
known cost in the same currency, the selector returns `needs_cost_facts` rather
|
|
127
|
+
than guessing. A caller may explicitly request the `capability_breadth` fallback;
|
|
128
|
+
it is never applied implicitly.
|
|
129
|
+
|
|
114
130
|
The same engine is also available as:
|
|
115
131
|
|
|
116
132
|
```bash
|
|
117
133
|
# CLI (human or scripted)
|
|
118
134
|
asm select "find and book a refundable flight" --taxonomy tool.booking.travel \
|
|
119
|
-
--requires flight_search,flight_order_create --json
|
|
135
|
+
--requires flight_search,flight_order_create --fallback-policy capability_breadth --json
|
|
120
136
|
|
|
121
137
|
# Hosted HTTP API (stdlib-only; deploy anywhere that runs Python)
|
|
122
138
|
python asm_select_api.py # POST /select, GET /tools, GET /healthz on :8787
|
|
123
139
|
```
|
|
124
140
|
|
|
125
|
-
|
|
141
|
+
DeepSeek Harness developer-preview users can install the native
|
|
142
|
+
[`asm_select` tool adapter](integrations/deepseek-harness/README.md). It uses
|
|
143
|
+
the same HTTP contract and returns the current structured decision, but never
|
|
144
|
+
invokes or authorizes the selected service. The frozen v0.1 receipt is available
|
|
145
|
+
only through an explicit legacy compatibility profile. The adapter defaults to
|
|
146
|
+
a local selector so task text is not sent to a hosted endpoint implicitly.
|
|
147
|
+
|
|
148
|
+
LangChain / LangGraph builders get the same selector as a packaged drop-in tool:
|
|
126
149
|
|
|
127
150
|
```python
|
|
128
|
-
|
|
129
|
-
from asm_tools import ASMToolSelectorTool
|
|
151
|
+
from asm_protocol.integrations.langchain import ASMToolSelectorTool
|
|
130
152
|
agent_tools = [ASMToolSelectorTool()] # name: asm_tool_selector
|
|
131
153
|
```
|
|
132
154
|
|
|
155
|
+
Install it with `python -m pip install "asm-protocol[langchain]==0.6.0"`. LangChain
|
|
156
|
+
hosts receive the full structured decision in `ToolMessage.artifact`; the
|
|
157
|
+
model-facing content remains a short summary. The adapter never executes or
|
|
158
|
+
authorizes the selected service.
|
|
159
|
+
|
|
133
160
|
A public reference instance runs at **https://asm-spec.onrender.com**. It also dogfoods ASM's own publishing convention: `GET /.well-known/asm` serves the library catalog (one re-stampable `generated_at`, per-manifest links), and `GET /manifest/{service_id}` serves each full manifest — ASM is its own first publisher.
|
|
134
161
|
|
|
135
162
|
```bash
|
|
@@ -374,9 +401,10 @@ Schema: [`schema/asm-v0.3.schema.json`](schema/asm-v0.3.schema.json).
|
|
|
374
401
|
|
|
375
402
|
```text
|
|
376
403
|
schema/ ASM JSON Schema
|
|
404
|
+
src/asm_protocol/ Canonical Python SDK: selection, cost, version
|
|
377
405
|
library/ Tool-value library (agent tool selection) + select_demo.py
|
|
378
406
|
manifests/ 75 source-linked manifests
|
|
379
|
-
scorer/
|
|
407
|
+
scorer/ Legacy experimental per-unit TOPSIS scorer and tests
|
|
380
408
|
registry/ MCP registry server exposing ASM tools
|
|
381
409
|
examples/mcp-server-json/ MCP Registry server.json examples
|
|
382
410
|
docs/integrations/ MCP Registry and aggregator integration docs
|
|
@@ -25,7 +25,7 @@ It accepts either a standalone ASM manifest or an MCP Registry `server.json`
|
|
|
25
25
|
with publisher-provided ASM metadata:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
python -m pip install "asm-protocol==0.
|
|
28
|
+
python -m pip install "asm-protocol==0.6.0"
|
|
29
29
|
asm-lint server.json --format markdown --output asm-lint-report.md
|
|
30
30
|
```
|
|
31
31
|
|
|
@@ -42,7 +42,7 @@ steps:
|
|
|
42
42
|
- uses: actions/setup-python@v5
|
|
43
43
|
with:
|
|
44
44
|
python-version: "3.12"
|
|
45
|
-
- uses: YE-YI7/asm-spec/.github/actions/asm-lint@v0.
|
|
45
|
+
- uses: YE-YI7/asm-spec/.github/actions/asm-lint@v0.6.0
|
|
46
46
|
with:
|
|
47
47
|
path: server.json
|
|
48
48
|
fail-on: invalid
|
|
@@ -51,6 +51,8 @@ steps:
|
|
|
51
51
|
The Action adds the full Markdown report to the job summary. It does not call
|
|
52
52
|
an ASM-hosted API or upload the inspected manifest. See the
|
|
53
53
|
[lint and CI guide](docs/adoption/asm-lint.md) for status semantics.
|
|
54
|
+
For a bounded producer-side change, use the
|
|
55
|
+
[10-minute adoption package](docs/adoption/ten-minute.md).
|
|
54
56
|
|
|
55
57
|
## Try it: pick a tool for a task
|
|
56
58
|
|
|
@@ -59,7 +61,13 @@ git clone https://github.com/YE-YI7/asm-spec.git && cd asm-spec
|
|
|
59
61
|
python library/select_demo.py
|
|
60
62
|
```
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
The deterministic core does **not** pretend to understand the task sentence. The
|
|
65
|
+
caller supplies structured facts such as `taxonomy`, `required_functions`,
|
|
66
|
+
platform, reach, and expected workload; `task` remains audit/display text. If
|
|
67
|
+
taxonomy and required functions are both absent, selection returns
|
|
68
|
+
`under_specified` instead of choosing an unrelated globally cheap tool.
|
|
69
|
+
|
|
70
|
+
For *"make a study plan and remind me daily"* with a cloud agent on Windows, the selector drops the tools it can't drive (Apple Reminders, Things 3 — local-device only) and the ones it can't call directly (Any.do — Zapier only), then ranks the rest from those explicit constraints. Ask for a built-in pomodoro and the pick changes to TickTick. Ask to *"edit an image and lay out a poster"* and it filters **Affinity Designer**, which exposes no automation API at all.
|
|
63
71
|
|
|
64
72
|
The library it selects over is in [`library/`](library/) — 30 real tools across task management, creative design, research, communication, developer tools, booking, and real-estate data today, each carrying:
|
|
65
73
|
|
|
@@ -89,31 +97,50 @@ ASM is MCP-compatible: publish a standalone `.well-known/asm`, or embed ASM in M
|
|
|
89
97
|
ASM ships an MCP server so any MCP client (Claude Desktop, Cursor, an agent host) can call the selector as a tool — no schema adoption required:
|
|
90
98
|
|
|
91
99
|
```bash
|
|
92
|
-
python3 -m pip install "asm-protocol[mcp]==0.
|
|
100
|
+
python3 -m pip install "asm-protocol[mcp]==0.6.0"
|
|
93
101
|
asm-selector # stdio MCP server (MCP SDK 2.x)
|
|
94
102
|
```
|
|
95
103
|
|
|
96
104
|
It exposes three tools: **`select_tool`** (pick a tool for a task and return its risk/approval policy), **`list_library_tools`**, and **`get_tool_manifest`**. Point your client's MCP config at `asm-selector`, or at `python3 /path/to/asm-spec/asm_selector_mcp.py`; the selector reads `library/` (override with `ASM_LIBRARY_DIR`). The Python server uses the stable MCP SDK 2.x line and supports the modern `2026-07-28` protocol era. The same selector is importable directly: `from library_select import select`.
|
|
97
105
|
|
|
106
|
+
Cost output has an explicit `known`, `partial`, or `unknown` status. Metered
|
|
107
|
+
prices need expected monthly usage; one-time licenses need an amortization
|
|
108
|
+
period; prose-only free tiers remain unknown because their allowance and reset
|
|
109
|
+
rules are not machine-readable. If every eligible candidate does not have a
|
|
110
|
+
known cost in the same currency, the selector returns `needs_cost_facts` rather
|
|
111
|
+
than guessing. A caller may explicitly request the `capability_breadth` fallback;
|
|
112
|
+
it is never applied implicitly.
|
|
113
|
+
|
|
98
114
|
The same engine is also available as:
|
|
99
115
|
|
|
100
116
|
```bash
|
|
101
117
|
# CLI (human or scripted)
|
|
102
118
|
asm select "find and book a refundable flight" --taxonomy tool.booking.travel \
|
|
103
|
-
--requires flight_search,flight_order_create --json
|
|
119
|
+
--requires flight_search,flight_order_create --fallback-policy capability_breadth --json
|
|
104
120
|
|
|
105
121
|
# Hosted HTTP API (stdlib-only; deploy anywhere that runs Python)
|
|
106
122
|
python asm_select_api.py # POST /select, GET /tools, GET /healthz on :8787
|
|
107
123
|
```
|
|
108
124
|
|
|
109
|
-
|
|
125
|
+
DeepSeek Harness developer-preview users can install the native
|
|
126
|
+
[`asm_select` tool adapter](integrations/deepseek-harness/README.md). It uses
|
|
127
|
+
the same HTTP contract and returns the current structured decision, but never
|
|
128
|
+
invokes or authorizes the selected service. The frozen v0.1 receipt is available
|
|
129
|
+
only through an explicit legacy compatibility profile. The adapter defaults to
|
|
130
|
+
a local selector so task text is not sent to a hosted endpoint implicitly.
|
|
131
|
+
|
|
132
|
+
LangChain / LangGraph builders get the same selector as a packaged drop-in tool:
|
|
110
133
|
|
|
111
134
|
```python
|
|
112
|
-
|
|
113
|
-
from asm_tools import ASMToolSelectorTool
|
|
135
|
+
from asm_protocol.integrations.langchain import ASMToolSelectorTool
|
|
114
136
|
agent_tools = [ASMToolSelectorTool()] # name: asm_tool_selector
|
|
115
137
|
```
|
|
116
138
|
|
|
139
|
+
Install it with `python -m pip install "asm-protocol[langchain]==0.6.0"`. LangChain
|
|
140
|
+
hosts receive the full structured decision in `ToolMessage.artifact`; the
|
|
141
|
+
model-facing content remains a short summary. The adapter never executes or
|
|
142
|
+
authorizes the selected service.
|
|
143
|
+
|
|
117
144
|
A public reference instance runs at **https://asm-spec.onrender.com**. It also dogfoods ASM's own publishing convention: `GET /.well-known/asm` serves the library catalog (one re-stampable `generated_at`, per-manifest links), and `GET /manifest/{service_id}` serves each full manifest — ASM is its own first publisher.
|
|
118
145
|
|
|
119
146
|
```bash
|
|
@@ -358,9 +385,10 @@ Schema: [`schema/asm-v0.3.schema.json`](schema/asm-v0.3.schema.json).
|
|
|
358
385
|
|
|
359
386
|
```text
|
|
360
387
|
schema/ ASM JSON Schema
|
|
388
|
+
src/asm_protocol/ Canonical Python SDK: selection, cost, version
|
|
361
389
|
library/ Tool-value library (agent tool selection) + select_demo.py
|
|
362
390
|
manifests/ 75 source-linked manifests
|
|
363
|
-
scorer/
|
|
391
|
+
scorer/ Legacy experimental per-unit TOPSIS scorer and tests
|
|
364
392
|
registry/ MCP registry server exposing ASM tools
|
|
365
393
|
examples/mcp-server-json/ MCP Registry server.json examples
|
|
366
394
|
docs/integrations/ MCP Registry and aggregator integration docs
|
|
@@ -515,6 +515,7 @@ def cmd_select(args: argparse.Namespace) -> int:
|
|
|
515
515
|
required_functions=[f for f in (args.requires or "").split(",") if f],
|
|
516
516
|
require_approval_for=[s for s in (args.approval_for or "").split(",") if s],
|
|
517
517
|
require_agent_completable_setup=args.agent_setup_only,
|
|
518
|
+
fallback_policy=args.fallback_policy,
|
|
518
519
|
)
|
|
519
520
|
if args.json:
|
|
520
521
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
@@ -522,18 +523,26 @@ def cmd_select(args: argparse.Namespace) -> int:
|
|
|
522
523
|
|
|
523
524
|
sel = result["selected"]
|
|
524
525
|
if not sel:
|
|
525
|
-
print("No
|
|
526
|
+
print(f"No selection ({result['selection_status']}): {result['reason']}")
|
|
527
|
+
for candidate in result["alternatives"][: args.limit]:
|
|
528
|
+
print(f" candidate: {candidate['display_name']} ({candidate['cost_estimate']['status']} cost)")
|
|
526
529
|
for r in result["rejected"][: args.limit]:
|
|
527
530
|
print(f" - {r['service']}: {r['reason']}")
|
|
528
531
|
return 2
|
|
529
532
|
print(f"Selected: {sel['display_name']} ({sel['service_id']})")
|
|
530
|
-
|
|
533
|
+
estimate = sel["cost_estimate"]
|
|
534
|
+
cost = (f"${sel['monthly_cost_usd']}/mo" if sel["monthly_cost_usd"] is not None
|
|
535
|
+
else f"{estimate['status']} (workload or allowance facts required)")
|
|
536
|
+
print(f" cost={cost}, interface={sel['interface']}, reach={sel['reach']}")
|
|
531
537
|
if sel.get("agent_completable_setup") is not None:
|
|
532
538
|
print(f" setup: agent_completable={sel['agent_completable_setup']}, requires={sel.get('setup_requires', [])}")
|
|
533
539
|
print(f" risk={result['risk_class']}, approval_required={result['approval_required']}, side_effects={result['side_effects']}")
|
|
534
540
|
print(f" reason: {result['reason']}")
|
|
535
541
|
for alt in result["alternatives"][: args.limit]:
|
|
536
|
-
|
|
542
|
+
alt_cost = (f"${alt['monthly_cost_usd']}/mo"
|
|
543
|
+
if alt["monthly_cost_usd"] is not None
|
|
544
|
+
else alt["cost_estimate"]["status"])
|
|
545
|
+
print(f" alt: {alt['display_name']} ({alt_cost})")
|
|
537
546
|
if result["rejected"]:
|
|
538
547
|
print(" filtered out:")
|
|
539
548
|
for r in result["rejected"][: args.limit]:
|
|
@@ -556,6 +565,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
556
565
|
help="Comma-separated side-effects that force approval, e.g. financial_charge,sends_message")
|
|
557
566
|
sel.add_argument("--agent-setup-only", action="store_true",
|
|
558
567
|
help="Drop tools needing human-in-the-loop setup (paid signup, OAuth consent, approval)")
|
|
568
|
+
sel.add_argument("--fallback-policy", choices=["capability_breadth"],
|
|
569
|
+
help="Explicit fallback when eligible candidate costs are not comparable")
|
|
559
570
|
sel.add_argument("--json", action="store_true", help="Emit the structured decision as JSON")
|
|
560
571
|
sel.add_argument("--limit", type=int, default=5, help="Maximum alternatives/rejections to print")
|
|
561
572
|
sel.set_defaults(func=cmd_select)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asm-protocol
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Agent Service Manifest: value metadata and scoring for autonomous service selection
|
|
5
5
|
Author: Yi Guo
|
|
6
6
|
License-Expression: MIT
|
|
@@ -41,7 +41,7 @@ It accepts either a standalone ASM manifest or an MCP Registry `server.json`
|
|
|
41
41
|
with publisher-provided ASM metadata:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
python -m pip install "asm-protocol==0.
|
|
44
|
+
python -m pip install "asm-protocol==0.6.0"
|
|
45
45
|
asm-lint server.json --format markdown --output asm-lint-report.md
|
|
46
46
|
```
|
|
47
47
|
|
|
@@ -58,7 +58,7 @@ steps:
|
|
|
58
58
|
- uses: actions/setup-python@v5
|
|
59
59
|
with:
|
|
60
60
|
python-version: "3.12"
|
|
61
|
-
- uses: YE-YI7/asm-spec/.github/actions/asm-lint@v0.
|
|
61
|
+
- uses: YE-YI7/asm-spec/.github/actions/asm-lint@v0.6.0
|
|
62
62
|
with:
|
|
63
63
|
path: server.json
|
|
64
64
|
fail-on: invalid
|
|
@@ -67,6 +67,8 @@ steps:
|
|
|
67
67
|
The Action adds the full Markdown report to the job summary. It does not call
|
|
68
68
|
an ASM-hosted API or upload the inspected manifest. See the
|
|
69
69
|
[lint and CI guide](docs/adoption/asm-lint.md) for status semantics.
|
|
70
|
+
For a bounded producer-side change, use the
|
|
71
|
+
[10-minute adoption package](docs/adoption/ten-minute.md).
|
|
70
72
|
|
|
71
73
|
## Try it: pick a tool for a task
|
|
72
74
|
|
|
@@ -75,7 +77,13 @@ git clone https://github.com/YE-YI7/asm-spec.git && cd asm-spec
|
|
|
75
77
|
python library/select_demo.py
|
|
76
78
|
```
|
|
77
79
|
|
|
78
|
-
|
|
80
|
+
The deterministic core does **not** pretend to understand the task sentence. The
|
|
81
|
+
caller supplies structured facts such as `taxonomy`, `required_functions`,
|
|
82
|
+
platform, reach, and expected workload; `task` remains audit/display text. If
|
|
83
|
+
taxonomy and required functions are both absent, selection returns
|
|
84
|
+
`under_specified` instead of choosing an unrelated globally cheap tool.
|
|
85
|
+
|
|
86
|
+
For *"make a study plan and remind me daily"* with a cloud agent on Windows, the selector drops the tools it can't drive (Apple Reminders, Things 3 — local-device only) and the ones it can't call directly (Any.do — Zapier only), then ranks the rest from those explicit constraints. Ask for a built-in pomodoro and the pick changes to TickTick. Ask to *"edit an image and lay out a poster"* and it filters **Affinity Designer**, which exposes no automation API at all.
|
|
79
87
|
|
|
80
88
|
The library it selects over is in [`library/`](library/) — 30 real tools across task management, creative design, research, communication, developer tools, booking, and real-estate data today, each carrying:
|
|
81
89
|
|
|
@@ -105,31 +113,50 @@ ASM is MCP-compatible: publish a standalone `.well-known/asm`, or embed ASM in M
|
|
|
105
113
|
ASM ships an MCP server so any MCP client (Claude Desktop, Cursor, an agent host) can call the selector as a tool — no schema adoption required:
|
|
106
114
|
|
|
107
115
|
```bash
|
|
108
|
-
python3 -m pip install "asm-protocol[mcp]==0.
|
|
116
|
+
python3 -m pip install "asm-protocol[mcp]==0.6.0"
|
|
109
117
|
asm-selector # stdio MCP server (MCP SDK 2.x)
|
|
110
118
|
```
|
|
111
119
|
|
|
112
120
|
It exposes three tools: **`select_tool`** (pick a tool for a task and return its risk/approval policy), **`list_library_tools`**, and **`get_tool_manifest`**. Point your client's MCP config at `asm-selector`, or at `python3 /path/to/asm-spec/asm_selector_mcp.py`; the selector reads `library/` (override with `ASM_LIBRARY_DIR`). The Python server uses the stable MCP SDK 2.x line and supports the modern `2026-07-28` protocol era. The same selector is importable directly: `from library_select import select`.
|
|
113
121
|
|
|
122
|
+
Cost output has an explicit `known`, `partial`, or `unknown` status. Metered
|
|
123
|
+
prices need expected monthly usage; one-time licenses need an amortization
|
|
124
|
+
period; prose-only free tiers remain unknown because their allowance and reset
|
|
125
|
+
rules are not machine-readable. If every eligible candidate does not have a
|
|
126
|
+
known cost in the same currency, the selector returns `needs_cost_facts` rather
|
|
127
|
+
than guessing. A caller may explicitly request the `capability_breadth` fallback;
|
|
128
|
+
it is never applied implicitly.
|
|
129
|
+
|
|
114
130
|
The same engine is also available as:
|
|
115
131
|
|
|
116
132
|
```bash
|
|
117
133
|
# CLI (human or scripted)
|
|
118
134
|
asm select "find and book a refundable flight" --taxonomy tool.booking.travel \
|
|
119
|
-
--requires flight_search,flight_order_create --json
|
|
135
|
+
--requires flight_search,flight_order_create --fallback-policy capability_breadth --json
|
|
120
136
|
|
|
121
137
|
# Hosted HTTP API (stdlib-only; deploy anywhere that runs Python)
|
|
122
138
|
python asm_select_api.py # POST /select, GET /tools, GET /healthz on :8787
|
|
123
139
|
```
|
|
124
140
|
|
|
125
|
-
|
|
141
|
+
DeepSeek Harness developer-preview users can install the native
|
|
142
|
+
[`asm_select` tool adapter](integrations/deepseek-harness/README.md). It uses
|
|
143
|
+
the same HTTP contract and returns the current structured decision, but never
|
|
144
|
+
invokes or authorizes the selected service. The frozen v0.1 receipt is available
|
|
145
|
+
only through an explicit legacy compatibility profile. The adapter defaults to
|
|
146
|
+
a local selector so task text is not sent to a hosted endpoint implicitly.
|
|
147
|
+
|
|
148
|
+
LangChain / LangGraph builders get the same selector as a packaged drop-in tool:
|
|
126
149
|
|
|
127
150
|
```python
|
|
128
|
-
|
|
129
|
-
from asm_tools import ASMToolSelectorTool
|
|
151
|
+
from asm_protocol.integrations.langchain import ASMToolSelectorTool
|
|
130
152
|
agent_tools = [ASMToolSelectorTool()] # name: asm_tool_selector
|
|
131
153
|
```
|
|
132
154
|
|
|
155
|
+
Install it with `python -m pip install "asm-protocol[langchain]==0.6.0"`. LangChain
|
|
156
|
+
hosts receive the full structured decision in `ToolMessage.artifact`; the
|
|
157
|
+
model-facing content remains a short summary. The adapter never executes or
|
|
158
|
+
authorizes the selected service.
|
|
159
|
+
|
|
133
160
|
A public reference instance runs at **https://asm-spec.onrender.com**. It also dogfoods ASM's own publishing convention: `GET /.well-known/asm` serves the library catalog (one re-stampable `generated_at`, per-manifest links), and `GET /manifest/{service_id}` serves each full manifest — ASM is its own first publisher.
|
|
134
161
|
|
|
135
162
|
```bash
|
|
@@ -374,9 +401,10 @@ Schema: [`schema/asm-v0.3.schema.json`](schema/asm-v0.3.schema.json).
|
|
|
374
401
|
|
|
375
402
|
```text
|
|
376
403
|
schema/ ASM JSON Schema
|
|
404
|
+
src/asm_protocol/ Canonical Python SDK: selection, cost, version
|
|
377
405
|
library/ Tool-value library (agent tool selection) + select_demo.py
|
|
378
406
|
manifests/ 75 source-linked manifests
|
|
379
|
-
scorer/
|
|
407
|
+
scorer/ Legacy experimental per-unit TOPSIS scorer and tests
|
|
380
408
|
registry/ MCP registry server exposing ASM tools
|
|
381
409
|
examples/mcp-server-json/ MCP Registry server.json examples
|
|
382
410
|
docs/integrations/ MCP Registry and aggregator integration docs
|
|
@@ -19,10 +19,12 @@ schema/__init__.py
|
|
|
19
19
|
schema/asm-receipt-envelope-v0.1.schema.json
|
|
20
20
|
schema/asm-v0.2.schema.json
|
|
21
21
|
schema/asm-v0.3.schema.json
|
|
22
|
+
schema/selection-receipt-v0.1.schema.json
|
|
22
23
|
scorer/__init__.py
|
|
23
24
|
scorer/scorer.py
|
|
24
25
|
scorer/test_asm_lint.py
|
|
25
26
|
scorer/test_asm_selector_mcp.py
|
|
27
|
+
scorer/test_cost_estimation.py
|
|
26
28
|
scorer/test_data_quality_audit.py
|
|
27
29
|
scorer/test_langchain_adapter.py
|
|
28
30
|
scorer/test_library_select.py
|
|
@@ -30,5 +32,13 @@ scorer/test_manifests_schema.py
|
|
|
30
32
|
scorer/test_mcp_server_json_asm.py
|
|
31
33
|
scorer/test_openrouter_adapter.py
|
|
32
34
|
scorer/test_scorer.py
|
|
35
|
+
scorer/test_version_contract.py
|
|
33
36
|
scorer/test_x402_bridge.py
|
|
34
|
-
scorer/data/
|
|
37
|
+
scorer/data/__init__.py
|
|
38
|
+
scorer/data/elo_snapshot.json
|
|
39
|
+
src/asm_protocol/__init__.py
|
|
40
|
+
src/asm_protocol/cost.py
|
|
41
|
+
src/asm_protocol/selection.py
|
|
42
|
+
src/asm_protocol/version.py
|
|
43
|
+
src/asm_protocol/integrations/__init__.py
|
|
44
|
+
src/asm_protocol/integrations/langchain.py
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
Endpoints:
|
|
5
5
|
POST /select body: {task, taxonomy?, agent_reach?, user_platform?,
|
|
6
6
|
required_functions?, require_approval_for?,
|
|
7
|
-
require_agent_completable_setup
|
|
7
|
+
require_agent_completable_setup?, workload?: {
|
|
8
|
+
monthly_units?, amortization_months?}}
|
|
8
9
|
-> structured selection decision (same shape as library_select.select)
|
|
9
10
|
GET /tools ?taxonomy=... -> list of selectable tools
|
|
10
11
|
GET /.well-known/asm -> ASM catalog: one re-stampable generated_at + per-manifest
|
|
@@ -29,7 +30,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
29
30
|
from pathlib import Path
|
|
30
31
|
from urllib.parse import parse_qs, unquote, urlparse
|
|
31
32
|
|
|
32
|
-
from library_select import
|
|
33
|
+
from library_select import estimate_monthly_cost, load_library, select
|
|
33
34
|
|
|
34
35
|
_LIBRARY = load_library()
|
|
35
36
|
_BY_ID = {m.get("service_id"): m for m in _LIBRARY}
|
|
@@ -138,6 +139,7 @@ def _tools_listing(taxonomy: str | None) -> list[dict]:
|
|
|
138
139
|
if taxonomy and m.get("taxonomy") != taxonomy:
|
|
139
140
|
continue
|
|
140
141
|
inv = m.get("invocation") or {}
|
|
142
|
+
estimate = estimate_monthly_cost(m)
|
|
141
143
|
out.append({
|
|
142
144
|
"service_id": m.get("service_id"),
|
|
143
145
|
"display_name": m.get("display_name"),
|
|
@@ -146,7 +148,8 @@ def _tools_listing(taxonomy: str | None) -> list[dict]:
|
|
|
146
148
|
"reach": inv.get("reach"),
|
|
147
149
|
"agent_operable": inv.get("agent_operable"),
|
|
148
150
|
"agent_completable_setup": inv.get("agent_completable_setup"),
|
|
149
|
-
"monthly_cost_usd":
|
|
151
|
+
"monthly_cost_usd": estimate.monthly_total,
|
|
152
|
+
"cost_estimate": estimate.to_dict(),
|
|
150
153
|
})
|
|
151
154
|
return out
|
|
152
155
|
|
|
@@ -248,18 +251,25 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
248
251
|
if not task:
|
|
249
252
|
self._send(400, {"error": "'task' is required"})
|
|
250
253
|
return
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
254
|
+
try:
|
|
255
|
+
result = select(
|
|
256
|
+
task,
|
|
257
|
+
taxonomy=req.get("taxonomy"),
|
|
258
|
+
agent_reach=req.get("agent_reach", "cloud"),
|
|
259
|
+
user_platform=req.get("user_platform", "any"),
|
|
260
|
+
required_functions=req.get("required_functions") or [],
|
|
261
|
+
require_approval_for=req.get("require_approval_for") or [],
|
|
262
|
+
require_agent_completable_setup=bool(req.get("require_agent_completable_setup", False)),
|
|
263
|
+
workload=req.get("workload"),
|
|
264
|
+
fallback_policy=req.get("fallback_policy"),
|
|
265
|
+
selection_profile=req.get("selection_profile", "current"),
|
|
266
|
+
library=_LIBRARY,
|
|
267
|
+
receipt=bool(req.get("receipt", False)),
|
|
268
|
+
)
|
|
269
|
+
except (TypeError, ValueError) as error:
|
|
270
|
+
self._send(400, {"error": str(error)})
|
|
271
|
+
return
|
|
272
|
+
self._send(422 if result["selection_status"] == "under_specified" else 200, result)
|
|
263
273
|
|
|
264
274
|
def log_message(self, fmt, *args): # quiet default logging
|
|
265
275
|
pass
|
|
@@ -9,15 +9,16 @@ policy (risk / approval / side-effects) to gate the action.
|
|
|
9
9
|
Run (stdio): python asm_selector_mcp.py (or `asm-selector` once installed)
|
|
10
10
|
Requires: pip install "asm-protocol[mcp]"
|
|
11
11
|
"""
|
|
12
|
+
|
|
12
13
|
from __future__ import annotations
|
|
13
14
|
|
|
14
15
|
from mcp.server import MCPServer
|
|
15
16
|
|
|
16
|
-
from library_select import
|
|
17
|
+
from library_select import SELECTOR_VERSION, estimate_monthly_cost, load_library, select
|
|
17
18
|
|
|
18
19
|
mcp = MCPServer(
|
|
19
20
|
"asm-selector",
|
|
20
|
-
version=
|
|
21
|
+
version=SELECTOR_VERSION.removeprefix("asm-protocol/"),
|
|
21
22
|
description="Select agent-operable tools using ASM value and policy metadata.",
|
|
22
23
|
)
|
|
23
24
|
|
|
@@ -31,6 +32,9 @@ def select_tool(
|
|
|
31
32
|
required_functions: list[str] | None = None,
|
|
32
33
|
require_approval_for: list[str] | None = None,
|
|
33
34
|
require_agent_completable_setup: bool = False,
|
|
35
|
+
monthly_usage: dict[str, float] | None = None,
|
|
36
|
+
amortization_months: int | None = None,
|
|
37
|
+
fallback_policy: str | None = None,
|
|
34
38
|
) -> dict:
|
|
35
39
|
"""Pick the best tool from the ASM library for a task.
|
|
36
40
|
|
|
@@ -40,7 +44,7 @@ def select_tool(
|
|
|
40
44
|
policy so the caller can gate high-stakes actions before invoking.
|
|
41
45
|
|
|
42
46
|
Args:
|
|
43
|
-
task:
|
|
47
|
+
task: audit/display text; the deterministic core does not interpret it.
|
|
44
48
|
taxonomy: optional ASM taxonomy to scope candidates (e.g. tool.booking.travel).
|
|
45
49
|
agent_reach: 'cloud' (remote/headless) or 'local_device' (runs on user's machine).
|
|
46
50
|
user_platform: e.g. windows, macos, ios, android, web, any.
|
|
@@ -48,6 +52,9 @@ def select_tool(
|
|
|
48
52
|
require_approval_for: side-effects that should force approval (e.g. ["financial_charge","sends_message"]).
|
|
49
53
|
require_agent_completable_setup: if true, drop tools that need a human-in-the-loop
|
|
50
54
|
step (paid signup, OAuth consent, manual approval) the agent cannot complete unattended.
|
|
55
|
+
monthly_usage: expected monthly units keyed by billing dimension.
|
|
56
|
+
amortization_months: period for normalizing a declared one-time purchase.
|
|
57
|
+
fallback_policy: optional 'capability_breadth'; never applied implicitly.
|
|
51
58
|
"""
|
|
52
59
|
return select(
|
|
53
60
|
task,
|
|
@@ -57,27 +64,35 @@ def select_tool(
|
|
|
57
64
|
required_functions=required_functions or [],
|
|
58
65
|
require_approval_for=require_approval_for or [],
|
|
59
66
|
require_agent_completable_setup=require_agent_completable_setup,
|
|
67
|
+
workload={
|
|
68
|
+
"monthly_units": monthly_usage,
|
|
69
|
+
"amortization_months": amortization_months,
|
|
70
|
+
},
|
|
71
|
+
fallback_policy=fallback_policy,
|
|
60
72
|
)
|
|
61
73
|
|
|
62
74
|
|
|
63
75
|
@mcp.tool()
|
|
64
76
|
def list_library_tools(taxonomy: str | None = None) -> list[dict]:
|
|
65
|
-
"""List tools
|
|
66
|
-
interface/reach and monthly cost so a client can browse what's selectable."""
|
|
77
|
+
"""List tools with invocation facts and honest cost-estimate status."""
|
|
67
78
|
out = []
|
|
68
79
|
for m in load_library():
|
|
69
80
|
if taxonomy and m.get("taxonomy") != taxonomy:
|
|
70
81
|
continue
|
|
71
82
|
inv = m.get("invocation") or {}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
estimate = estimate_monthly_cost(m)
|
|
84
|
+
out.append(
|
|
85
|
+
{
|
|
86
|
+
"service_id": m.get("service_id"),
|
|
87
|
+
"display_name": m.get("display_name"),
|
|
88
|
+
"taxonomy": m.get("taxonomy"),
|
|
89
|
+
"interface": inv.get("interface"),
|
|
90
|
+
"reach": inv.get("reach"),
|
|
91
|
+
"agent_operable": inv.get("agent_operable"),
|
|
92
|
+
"monthly_cost_usd": estimate.monthly_total,
|
|
93
|
+
"cost_estimate": estimate.to_dict(),
|
|
94
|
+
}
|
|
95
|
+
)
|
|
81
96
|
return out
|
|
82
97
|
|
|
83
98
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Backward-compatible facade for the canonical :mod:`asm_protocol` SDK."""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_SRC = Path(__file__).resolve().parent / "src"
|
|
8
|
+
if str(_SRC) not in sys.path:
|
|
9
|
+
sys.path.insert(0, str(_SRC))
|
|
10
|
+
|
|
11
|
+
from asm_protocol.selection import *
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "asm-protocol"
|
|
7
|
-
|
|
7
|
+
dynamic = ["version"]
|
|
8
8
|
description = "Agent Service Manifest: value metadata and scoring for autonomous service selection"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -27,11 +27,21 @@ asm-selector = "asm_selector_mcp:main"
|
|
|
27
27
|
|
|
28
28
|
[tool.setuptools]
|
|
29
29
|
py-modules = ["asm_cli", "asm_lint", "mcp_server_json_asm", "openrouter_adapter", "library_select", "asm_selector_mcp", "asm_select_api", "_asm_library_data"]
|
|
30
|
-
packages = [
|
|
30
|
+
packages = [
|
|
31
|
+
"asm_protocol",
|
|
32
|
+
"asm_protocol.integrations",
|
|
33
|
+
"scorer",
|
|
34
|
+
"scorer.data",
|
|
35
|
+
"asm_schema",
|
|
36
|
+
]
|
|
31
37
|
|
|
32
38
|
[tool.setuptools.package-dir]
|
|
39
|
+
asm_protocol = "src/asm_protocol"
|
|
33
40
|
asm_schema = "schema"
|
|
34
41
|
|
|
42
|
+
[tool.setuptools.dynamic]
|
|
43
|
+
version = {attr = "asm_protocol.version.__version__"}
|
|
44
|
+
|
|
35
45
|
[tool.setuptools.package-data]
|
|
36
46
|
scorer = ["data/*.json"]
|
|
37
47
|
asm_schema = ["*.json"]
|
|
@@ -39,3 +49,4 @@ asm_schema = ["*.json"]
|
|
|
39
49
|
[tool.pytest.ini_options]
|
|
40
50
|
testpaths = ["scorer", "tools/asm-gen"]
|
|
41
51
|
python_files = ["test_*.py"]
|
|
52
|
+
pythonpath = [".", "src"]
|