id-agent 1.1.3__py3-none-any.whl

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.
id_agent/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from ._alias import AliasMap
4
+ from ._detect import detect_duplicates
5
+ from ._generate import generate, generate_deterministic
6
+ from ._parse import parse, validate
7
+ from ._types import DuplicateResult, ParsedId, ValidationResult
8
+ from ._wordlist import WORDLIST
9
+
10
+ __all__ = [
11
+ "generate",
12
+ "generate_deterministic",
13
+ "parse",
14
+ "validate",
15
+ "AliasMap",
16
+ "detect_duplicates",
17
+ "WORDLIST",
18
+ "ParsedId",
19
+ "ValidationResult",
20
+ "DuplicateResult",
21
+ ]
id_agent/_alias.py ADDED
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from ._generate import _validate_words, generate
6
+
7
+
8
+ class AliasMap:
9
+ def __init__(self, words: int, allow_collision: bool = False) -> None:
10
+ self._words = _validate_words(words)
11
+ if not isinstance(allow_collision, bool):
12
+ raise TypeError("allow_collision must be a bool")
13
+ self._allow_collision = allow_collision
14
+ self._forward: dict[str, str] = {}
15
+ self._reverse: dict[str, str] = {}
16
+
17
+ def set(self, original: str) -> str:
18
+ if not isinstance(original, str):
19
+ raise TypeError("original must be a string")
20
+
21
+ existing = self._forward.get(original)
22
+ if existing is not None:
23
+ return existing
24
+
25
+ while True:
26
+ alias = generate(words=self._words)
27
+ if self._allow_collision or alias not in self._reverse:
28
+ break
29
+
30
+ self._forward[original] = alias
31
+ self._reverse[alias] = original
32
+ return alias
33
+
34
+ def get(self, alias: str) -> str | None:
35
+ if not isinstance(alias, str):
36
+ raise TypeError("alias must be a string")
37
+ return self._reverse.get(alias)
38
+
39
+ def clear(self) -> None:
40
+ self._forward.clear()
41
+ self._reverse.clear()
42
+
43
+ def __len__(self) -> int:
44
+ return len(self._forward)
45
+
46
+ def entries(self) -> tuple[tuple[str, str], ...]:
47
+ return tuple(self._forward.items())
48
+
49
+ def replace(self, text: str, pattern: str | re.Pattern[str] | None = None) -> str:
50
+ if not isinstance(text, str):
51
+ raise TypeError("text must be a string")
52
+
53
+ if pattern is not None:
54
+ compiled = _compile_pattern(pattern)
55
+ matches = dict.fromkeys(match.group(0) for match in compiled.finditer(text))
56
+ for match in matches:
57
+ self.set(match)
58
+
59
+ regex = _build_replacement_regex(self._forward.keys())
60
+ if regex is None:
61
+ return text
62
+ return regex.sub(lambda match: self._forward.get(match.group(0), match.group(0)), text)
63
+
64
+ def restore(self, text: str) -> str:
65
+ if not isinstance(text, str):
66
+ raise TypeError("text must be a string")
67
+
68
+ regex = _build_replacement_regex(self._reverse.keys())
69
+ if regex is None:
70
+ return text
71
+ return regex.sub(lambda match: self._reverse.get(match.group(0), match.group(0)), text)
72
+
73
+
74
+ def _compile_pattern(pattern: str | re.Pattern[str]) -> re.Pattern[str]:
75
+ if isinstance(pattern, str):
76
+ return re.compile(pattern)
77
+ if isinstance(pattern, re.Pattern):
78
+ return pattern
79
+ raise TypeError("pattern must be a string regex, compiled regex, or None")
80
+
81
+
82
+ def _build_replacement_regex(keys: object) -> re.Pattern[str] | None:
83
+ sorted_keys = sorted((str(key) for key in keys), key=len, reverse=True)
84
+ if not sorted_keys:
85
+ return None
86
+ return re.compile("|".join(re.escape(key) for key in sorted_keys))
id_agent/_detect.py ADDED
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections.abc import Iterable
5
+
6
+ from ._types import DuplicateResult
7
+
8
+
9
+ def detect_duplicates(
10
+ text: str | Iterable[str],
11
+ pattern: str | re.Pattern[str],
12
+ ) -> list[DuplicateResult]:
13
+ texts = _normalize_texts(text)
14
+ compiled = _compile_pattern(pattern)
15
+ counts: dict[str, int] = {}
16
+
17
+ for item in texts:
18
+ for match in compiled.finditer(item):
19
+ matched_id = match.group(0)
20
+ counts[matched_id] = counts.get(matched_id, 0) + 1
21
+
22
+ return [
23
+ DuplicateResult(id=matched_id, count=count)
24
+ for matched_id, count in counts.items()
25
+ if count > 1
26
+ ]
27
+
28
+
29
+ def _normalize_texts(text: str | Iterable[str]) -> tuple[str, ...]:
30
+ if isinstance(text, str):
31
+ return (text,)
32
+ if not isinstance(text, Iterable):
33
+ raise TypeError("text must be a string or iterable of strings")
34
+
35
+ texts = tuple(text)
36
+ if not all(isinstance(item, str) for item in texts):
37
+ raise TypeError("text iterable must contain only strings")
38
+ return texts
39
+
40
+
41
+ def _compile_pattern(pattern: str | re.Pattern[str]) -> re.Pattern[str]:
42
+ if isinstance(pattern, str):
43
+ return re.compile(pattern)
44
+ if isinstance(pattern, re.Pattern):
45
+ if not isinstance(pattern.pattern, str):
46
+ raise TypeError("pattern must match strings")
47
+ return pattern
48
+ raise TypeError("pattern must be a string regex or compiled regex")
id_agent/_generate.py ADDED
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import hmac
5
+ import re
6
+ import secrets
7
+
8
+ from ._wordlist import WORDLIST
9
+
10
+ _MIN_WORDS = 1
11
+ _MAX_WORDS = 16
12
+ _DEFAULT_WORDS = 8
13
+ _DEFAULT_NAMESPACE = "id-agent"
14
+ _PREFIX_RE = re.compile(r"^[a-z0-9]+$")
15
+
16
+
17
+ def _validate_words(words: int) -> int:
18
+ if not isinstance(words, int) or isinstance(words, bool):
19
+ raise TypeError("words must be an integer")
20
+ if words < _MIN_WORDS:
21
+ raise ValueError("words must be at least 1")
22
+ if words > _MAX_WORDS:
23
+ raise ValueError("words must be at most 16")
24
+ return words
25
+
26
+
27
+ def _validate_prefix(prefix: str | None) -> str | None:
28
+ if prefix is None:
29
+ return None
30
+ if not isinstance(prefix, str):
31
+ raise TypeError("prefix must be a string or None")
32
+ if not _PREFIX_RE.fullmatch(prefix):
33
+ raise ValueError("prefix must be lowercase alphanumeric")
34
+ return prefix
35
+
36
+
37
+ def _format_id(prefix: str | None, body: str) -> str:
38
+ return f"{prefix}_{body}" if prefix else body
39
+
40
+
41
+ def generate(*, words: int = _DEFAULT_WORDS, prefix: str | None = None) -> str:
42
+ word_count = _validate_words(words)
43
+ checked_prefix = _validate_prefix(prefix)
44
+ selected = [secrets.choice(WORDLIST) for _ in range(word_count)]
45
+ return _format_id(checked_prefix, "-".join(selected))
46
+
47
+
48
+ def generate_deterministic(
49
+ input: str,
50
+ *,
51
+ namespace: str | None = None,
52
+ words: int = _DEFAULT_WORDS,
53
+ prefix: str | None = None,
54
+ ) -> str:
55
+ if not isinstance(input, str):
56
+ raise TypeError("input must be a non-empty string")
57
+ if input == "":
58
+ raise ValueError("input must be a non-empty string")
59
+ if namespace is None:
60
+ checked_namespace = _DEFAULT_NAMESPACE
61
+ elif not isinstance(namespace, str):
62
+ raise TypeError("namespace must be a string or None")
63
+ elif namespace == "":
64
+ raise ValueError("zero-length namespace is not supported")
65
+ else:
66
+ checked_namespace = namespace
67
+
68
+ word_count = _validate_words(words)
69
+ checked_prefix = _validate_prefix(prefix)
70
+ digest = hmac.new(
71
+ checked_namespace.encode("utf-8"),
72
+ input.encode("utf-8"),
73
+ hashlib.sha256,
74
+ ).digest()
75
+ selected = [
76
+ WORDLIST[int.from_bytes(digest[i * 2 : i * 2 + 2], "big") % len(WORDLIST)]
77
+ for i in range(word_count)
78
+ ]
79
+ return _format_id(checked_prefix, "-".join(selected))
id_agent/_parse.py ADDED
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import re
5
+
6
+ from ._types import ParsedId, ValidationResult
7
+ from ._wordlist import WORDLIST
8
+
9
+ _READABLE_BODY_RE = re.compile(r"^[a-z]{2,}(?:[-_][a-z]{2,})*$")
10
+ _WORD_SEPARATOR_RE = re.compile(r"[-_]")
11
+ _BITS_PER_WORD = math.log2(len(WORDLIST))
12
+ _WORD_SET = frozenset(WORDLIST)
13
+
14
+
15
+ def parse(id: object) -> ParsedId | None:
16
+ if not isinstance(id, str) or id == "":
17
+ return None
18
+
19
+ prefix: str | None = None
20
+ body = id
21
+
22
+ underscore_index = id.find("_")
23
+ if underscore_index > 0:
24
+ prefix = id[:underscore_index]
25
+ body = id[underscore_index + 1 :]
26
+
27
+ if body == "":
28
+ return None
29
+
30
+ if not _READABLE_BODY_RE.fullmatch(body):
31
+ return None
32
+
33
+ words = tuple(_WORD_SEPARATOR_RE.split(body))
34
+ return ParsedId(
35
+ prefix=prefix,
36
+ words=words,
37
+ word_count=len(words),
38
+ bits=len(words) * _BITS_PER_WORD,
39
+ raw=id,
40
+ format="readable",
41
+ )
42
+
43
+
44
+ def validate(id: object) -> ValidationResult:
45
+ if not isinstance(id, str) or id == "":
46
+ return ValidationResult(valid=False, reason="empty string")
47
+ if re.search(r"[A-Z]", id):
48
+ return ValidationResult(valid=False, reason="contains uppercase characters")
49
+ if re.search(r"[^a-z0-9_-]", id):
50
+ return ValidationResult(valid=False, reason="contains invalid characters")
51
+
52
+ parsed = parse(id)
53
+ if parsed is None:
54
+ return ValidationResult(valid=False, reason="unrecognized format")
55
+
56
+ unknown_words = tuple(word for word in parsed.words if word not in _WORD_SET)
57
+ if unknown_words:
58
+ return ValidationResult(
59
+ valid=False,
60
+ reason=f"unknown words: {', '.join(unknown_words)}",
61
+ unknown_words=unknown_words,
62
+ )
63
+
64
+ return ValidationResult(
65
+ valid=True,
66
+ prefix=parsed.prefix,
67
+ word_count=parsed.word_count,
68
+ words=parsed.words,
69
+ )
id_agent/_types.py ADDED
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Literal
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class ParsedId:
9
+ prefix: str | None
10
+ words: tuple[str, ...]
11
+ word_count: int
12
+ bits: float
13
+ raw: str
14
+ format: Literal["readable"]
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ValidationResult:
19
+ valid: bool
20
+ reason: str | None = None
21
+ prefix: str | None = None
22
+ word_count: int | None = None
23
+ words: tuple[str, ...] = ()
24
+ unknown_words: tuple[str, ...] = ()
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class DuplicateResult:
29
+ id: str
30
+ count: int
id_agent/_wordlist.py ADDED
@@ -0,0 +1,264 @@
1
+ from __future__ import annotations
2
+
3
+ WORDLIST: tuple[str, ...] = tuple(
4
+ (
5
+ "aal aani aba abama abas abase abb abba abby abe abel abele abet abidi abies able abler "
6
+ "ably abort about abouts above abox abu abut aby acca accent accept access accord ace "
7
+ "acer aces ach achar ache achen acher aching acid acier acker acle acor acre act acting "
8
+ "action active actor acts actual acute ada adai adam adapt adar adat aday adays add "
9
+ "adda added adder addle adds addy ade adet adin adjust admin ado adult advert ady adz "
10
+ "aes afar affa affirm aft after aga again agal agama agar agate age aged agen agena "
11
+ "agency agenda agent ager ages agger aghan aging agio agnes ago agog agon agonal agra "
12
+ "agre agree agua ague agy aha ahead ahir aho ahu aias aid ail ailing ails aim aint air "
13
+ "aira aire airs airt airy ait ajar ajari aka akal akala akan ake akey akia akin akka "
14
+ "ako akov aku ala alada alaki alala alan aland alar alarm alary alas alb album ale alea "
15
+ "alee alem alen alert ales alette alex alf alias alice alida alien align alin aline "
16
+ "alist alit alite alive alk all allah allan alle allen aller allow ally alma almost alo "
17
+ "alogy alone along alp alpha also alt altar alter altern alto alur always aly alya ama "
18
+ "amah amain amang amani amar amara amarin amazon amba ambar amber amble ambo ame amen "
19
+ "ament ami amia amic amid amide amil amin amine amini amir amma amman ammer ammo ammu "
20
+ "among amor amos amount amp ample amt amy ana anam anan anana anas anat anay anchor and "
21
+ "anda ande andi andre andy anele anes angel angelo anger angers angka angle angled "
22
+ "angler angles ango ani animal anime anion anis anker ann anna anne annet annie annual "
23
+ "anoa anon ansa anser ansu answer ant anta antal antar antara ante antes anthem anti "
24
+ "antic anton ants antu any apa apache apar apart apatan ape aper aping apis apollo "
25
+ "aport appear append apple apply apps apse apt apus ara arab arad arar arc arca arch "
26
+ "arched arches archie archy ardu are area ared arena argas argent argo argon aria arian "
27
+ "aries ariot arius ark arks arm armed arming armor arms army arn arna arne arni aro "
28
+ "around arrant arras array arris arrow arry art artar artha artist arts arty aru arum "
29
+ "ary aryl asa asana ascii ascus ase asem ash asha ashen asher ashes ashy asian aside "
30
+ "ask askar asks asp aspect asper assert asset assets assi assign assis assist ast asta "
31
+ "aster astern astian ata atap ate aten ates athing ati atik atis atlas atma atom atomic "
32
+ "atoms atria atta attach attack attar atter attern attery audio audit auge augh aught "
33
+ "auh auk aum aus author auto autos ava avail aval avar ave avens aver avera averse "
34
+ "avery aves avian avid avis avo avoid awa await awake awan award aware awat away awd "
35
+ "awin awl awn awner awning axe axed axes axis axon ayah aye ayin ayu azine azo azole "
36
+ "azon azure baa baar bab babel baby bac bach back backs backup bad badge bae bag bags "
37
+ "bah bait baka bal ball balls bam ban banana band bands bane bang bank banks banner bar "
38
+ "bara bard bare barer barn barra bars bart barth baru bas base based bases bash basic "
39
+ "basis basket bass bat batch bate bath batis battle baud bay bea beam bean beans bear "
40
+ "beat beats beaut beck bed beds bee beek been beer before beg begin behind being bel "
41
+ "belief bell bella bells below belt ben bench bend bene beni ber bere berg berger beri "
42
+ "bern berry bert bes best bet beta beth bets better bhar bias bib bid bien bier big "
43
+ "bike bill bin binary bind binder bine bing bins bios biotic bird birds birth bis "
44
+ "bishop bit bite bito biz black blad blade blah blank blas blast bled blend blick blind "
45
+ "blink blo blob bloc block blood blower blue blur boa board boat bob bod boden body bog "
46
+ "bola bold bole boll bolo bolt bom bomb bon bond bone bones boni bonus boo book bool "
47
+ "boom boost boot boots bor bord border borg born borne boro borrow bos boss bot both "
48
+ "bottom bounce bound bourg bout bouw bow bower boxes boxing boy bra brace brain brains "
49
+ "bran branch brand bread break bred breed brew brick bridge brief brig bright bring "
50
+ "brit broken broker brook brot brown browse brush bubble buck bucket bud buddy budget "
51
+ "buds buff buffer bug build built bulk bull bullet bum bun bund bundle bung bur bureau "
52
+ "burg burn burst bury bus buster busy but button buy buyer buzz cab cache cad cade cake "
53
+ "cakes cal call caller cam came camel camera camp campo can cancel cand canner cannot "
54
+ "canon cant canvas cap cape car cara carbon card care career caret cargo carry cart "
55
+ "case cash casino cass cast caster cat catch cate cation caught cause cede cee ceil cel "
56
+ "cell cement cent center cep ceptor ceral ceria cern cess cha chaft chai chain chair "
57
+ "chal chalk champ chance chang change chant chap char charge chart chat chaus che cheap "
58
+ "check chee chef chen cheng chet chi chia chid chief chien chil child chin china chine "
59
+ "ching chip chips chlor cho choice chol choose chosen christ chrome chunk church cid "
60
+ "cine cion cipher circle cit cite city civil claim clam clang clare class clave clean "
61
+ "clear cled click client cline cling clinic clip clips clipse cloak clock clone close "
62
+ "closed cloth cloud club coach coal coat cobra cod code coder coe coffee cohol coil "
63
+ "coin col cola cold cole coli coll colon color colors column coma comb combat come "
64
+ "comes comic coming comma commit common con cond cone cones conn conte conto contra "
65
+ "cony cook cool coop cop cope copy cor cord corder core corn corner corp cos cost cot "
66
+ "could count county coupon cours course court cover cow cox craft cran cratch crate "
67
+ "crawl cream crease create credit cree creen crest crete crew crib crime cris critic "
68
+ "cro crop cross crow crumb cry crypt cub cube cue cult cup cur curl curr curse cursor "
69
+ "curve custom cut cute cyan cycle dab dad dade dae daemon dag daily dain dak dal dale "
70
+ "dam damage dan dance dang danger dao dap dar dark dart das dash data date dater dating "
71
+ "datum day days dead deal dealer death deb decess deck decode dee deep define deg "
72
+ "degree del delay dele delete delta deme den dene dense dent deny depart depend deploy "
73
+ "depth dere derive dern design desk dess detach detail detect dev device dew dha dial "
74
+ "dib dice did diet dig digest digit dim din ding dip dire direct dirty dis disc dish "
75
+ "disk dit dita ditch div divide dob doc dock docker doctor dod does dog dogs doing dol "
76
+ "dom domain don done dont doom door dop dor dos dose dot double dow down draft drag "
77
+ "dragon dram draw drawer dream dress drink drive driver drop dry dual dub duck duct "
78
+ "ductor duit dum dummy dump dup during dust dux each ean ear earing early earn earth "
79
+ "ease east easy eat ebb eben echo eddy eden edge edict edit edith editor edo educt eel "
80
+ "eer effect eft egg egger ego either eke eker eking eland elate elb eld elder elding "
81
+ "elect elf eli elia elian elite elk ell ella elle ellen elm elod elon elope els else "
82
+ "elt elves embar embed ember eme emit emm emma empt empty emu enable encode end ended "
83
+ "ender endere ending endue enemy energy engine enos ens ense ensky ensure ental enter "
84
+ "entity entry enzyme epoch equal equip era erade eral erase erd ere erg eria erian erie "
85
+ "ermine erne eros err erring error ers erse escape escort ess essay essed essie estado "
86
+ "estate esteem ester estre eta etch ether ethyl etna etta ettle eva eve even event ever "
87
+ "every evil ewer exact exam excel except exist exit expand expect expert expire expiry "
88
+ "export extend extent extern extra eye eyen eyer eyey eyn fabric face facet fact factor "
89
+ "fad fade fail fair faith fake faker fall fallen false fam family fan fang fant far "
90
+ "farm fass fast fat fatal father fatter fault favor fear feat fed fee feed feel feer "
91
+ "fei fell fels felt female fen fern fest fet fetch few fib fiber fico fid field fifo "
92
+ "fig fight figure file fill filled film filter fin final find finder fine finger finish "
93
+ "finite finity fir fire firm first fish fit five fix fixed flag flake flare flash flat "
94
+ "fleet flex flight flip flix flo float floor flora flow flower flu fluid flush flux fly "
95
+ "foam focus fod fog foil fold folder folie folio folk follow fon fond fono font foo "
96
+ "food foot footer for fora force forced forcer ford forest forge forget forgot fork "
97
+ "form formal format forme formed former formin fort forth forum fot found fox fra fram "
98
+ "frame fred free freeze fresh fried friend frig frist fro frog from front fruit fuel "
99
+ "full fully fum fun fund funk fusc fusion future gab gabe gable gad gage gain gal gam "
100
+ "game gaming gamma gan gang gant gaon gap gar gara garden gare garten gary gas gast gat "
101
+ "gata gate gau gave gaz gear ged gee gel geld gelt gem gen gender gene genic genre gens "
102
+ "gent geo ger gerald gers ges gest get geta getter gey gez ghan ghost gid gie gif gift "
103
+ "gig gil gin ging gio girl gist git give given giver giving glass glib global glyph gnu "
104
+ "goal god going gol gold golly gon gone gong goo good goods gor gos got gra grab grad "
105
+ "grade graded grader grain gram grand grant graph grass grat gratis grave gray great "
106
+ "gree green greg grey grid griff grim gris gross ground group grow grown growth grunt "
107
+ "guan guard gue guess guest guide guild guise gul gulp gum gun guna gur guru gus gust "
108
+ "gut gym gyro habit hack had haft hag hah hail hain hair hak hal hala hale half hall "
109
+ "halo halt halter halve ham hammer han hand handle hanger hao hap happy hard harm hart "
110
+ "hash hasta hat hau hausen have haven haw hawk hawm hay head headed header heal health "
111
+ "heap hear heart heat heater heavy heck heder heed heer hehe hei height hein hele hello "
112
+ "helm helmet help helper hem heme hen hend hep her hern hero hers het hew hex hey hia "
113
+ "hic hid hidden hide high higher hill him hin hind hing hint hip hire hiro his hist hit "
114
+ "hler hob hobo hod hog hoi hold holder hole holm holt holy home hon hone hong hood hoof "
115
+ "hook hoot hop hope hopper hora horn horse hose host hot hotel hound hour house hoven "
116
+ "hover how hub hud hug hui huk hum huma human hun hung hunt hunter hurst hurt hut hwa "
117
+ "hyp hyper ian iao iba iban ibid ibo ice iced ich icho icing icon icy ida ide idea "
118
+ "ideal idean ides idle idler ido idol ife iffy ignite ignore ihi ijo ikat ike ikey ila "
119
+ "ilia ilian ilium ilk ilka ill illy ilot ima image imam imber imer imi imine immi immit "
120
+ "immune imp impact impi import imu inca inch income ind indaba indan inde indent index "
121
+ "india indic indra indy ineri inermi inez infect infer inform infra ing inga inger "
122
+ "ingle inia inion initis inject ink inken inker inkle inks inky inn inner inning ino "
123
+ "inoma input insert inside insula intend intent inter intern into invent invert invest "
124
+ "invite invoke ion ione ioni ionic ipid ira iran irani ire iris irish irk irma iron ism "
125
+ "iso issue issuer ist istle ita itala itali italic itch item iten iter ither ito its "
126
+ "itys itza iva ivan ivin ivy iwa iyo izar izard izzard jab jack jade jam jama jaman jan "
127
+ "jar jara jat jati java jaw jay jed jem jest jet jete jim jin jing job joch joe john "
128
+ "join joint joker jon jong joni jos jours joy juan jud judge jug juice juju jump jun "
129
+ "junk junto jur jury just jut kai kaj kan kana kang kari karma kat kate kay kea ked kee "
130
+ "keen keep keeper keet kele ken kend kenn kent kep ker kern kernel ket keta key kick "
131
+ "kid kids kil killer kim kin kina kind king kino kip kish kiss kit kite kitty knife "
132
+ "know known koa kob koch kol kon kona kop kor kos kou kra kraft kreis kua kung lab "
133
+ "label labor lac lace lack lad lade laden lady lag lagen lager lai laid lain lak lake "
134
+ "lalo lam lama lambda lament lamp lan lana land lander lane lant lap lar large lari las "
135
+ "laser lash lass last lat late lated laten later latest latex latin lation laugh launch "
136
+ "lava law lay layer layout lazy lea lead leader leaf league lean lear learn lease least "
137
+ "leave lech leck lector led leda lede leden ledge ledged ledger lee leed leep leer lees "
138
+ "leet left leg legacy legal legate legend leger leges lei leigh lek lemma len lend lene "
139
+ "length lens lent leo ler les less lesson lest let lete lett letter lev levant level "
140
+ "lever lew lex lexia ley liable lias lice lich lichen licit lick lid lide lie lied lief "
141
+ "lier lif life lift light ligne like lim lime limit lin line linear lined linen liner "
142
+ "ling linge linger linha linie lining link linked links linky lint lion lip lis lish "
143
+ "list listed listen lit lite liter little liv live living liz loa load loaded loader "
144
+ "loan lob lobal local locale loch lock locked locker lod lof log loge logged logger "
145
+ "logic login logo logos loin loir long loo lood look lookup loom loon loop loot lop "
146
+ "lope lord lose loss lost lot lots lotte lou love lover low lower lox loyd luc luck lug "
147
+ "lum lunes lung lur lush lut lux lying lys lysis mac mack macro mad made mag mage magic "
148
+ "mah mail mailer main maine maint major make maker making mal malar mall mallow mam man "
149
+ "mana manage mand mande manent manes mani mania mann mano mans manse mant manual manuel "
150
+ "many mao map mapper mar march mare margin marine mark marked marker market markup mars "
151
+ "mart marvin mary mas masa mask masked mass massa mast master mat match mate mater math "
152
+ "matrix matter max may maya maybe maze meal mean mede media median medio medium meet "
153
+ "meg mein mel meld mele melon mem member memo memory men meng ment mental mentor menu "
154
+ "mer mere merge merk mero mes mesa mese mesh mess messe messer mester met meta metal "
155
+ "mete meteor meter method metic metric mica mico micro mid middle mien mig might mil "
156
+ "mile milk mill mime min mina mind mine miner ming mino minor mint minus minute mir "
157
+ "mirror mise miss mist mite mitt mix mixed mob mobile mock mod modal mode model modern "
158
+ "modify module mog moil moire moment mon money mong mongo mono mont month monton moo "
159
+ "moon moor mooth mor more morgen mort most mostly mot mother motion motor mount mouse "
160
+ "mouth move movie moving mrs much mud mult mum mun mund mung munity mus museum music "
161
+ "must mute mux naa naam nab nable nach nae nag naio nais nak nam nama name nan nance "
162
+ "nancy nant nap nar nard nast nat nation native nature nay nea neal near neath neb neck "
163
+ "ned nee need needle needs neet nef nei nell nelly neo ness nest nesty net nete never "
164
+ "new news next nias nice nici nick nid night nil nim nine ning nisse nit nod node nog "
165
+ "noise noma nome non nonce none noop nor norm normal north nose nostic not note notice "
166
+ "notify nou noun nous nova novo now nth nub nul null number numero nun nut nye oak oban "
167
+ "obe obi object occur och ock ocracy ocular oda odal odd ode odel odin odium odor ods "
168
+ "odz oes off offer office offset oft often ogle ohia oil oint oka okee oki okia okra "
169
+ "old olden older ole oleg olid olio olive olla olm ology olor olson oman omar omber "
170
+ "omega omen omer omina omit ona once one oner onium only ons ontal onto onus onym onza "
171
+ "oodles oons oord opal opaque ope open opera opsy opt optic option opus ora oracle "
172
+ "orage orang orange orb orbit orc orca order ordu ore orf organ orgia orias orient "
173
+ "origin orna orris ort ory osc ose ossal osse ostic otate other otic otis oto otto otus "
174
+ "ouch ouf ough ought ounce ounds ours oust out outed outen outer outing output ova oval "
175
+ "oven over ovine ovis owe ower owing owl owler own owner oxide oxy oxygen oyer paar pac "
176
+ "pace paced pack packer packet pad page pager pagina pail pain paint pair paired pais "
177
+ "pal pam pan pand panel pang panic pant pants pap paper paque par para param parate "
178
+ "parcel pard paren parent parer pari paring park parse parser part party pass passen "
179
+ "past paste pat patch path pathy pau pause paw pawn pay payer pea peace peak peaker "
180
+ "peat ped peed peer peg pele pell pen pend pent people pep per peri period perk perm "
181
+ "permit person pert perty pes peso pet phase phasis phi phil pho phon phone phonic "
182
+ "phony phot photo phrase pia pic pick picked picker pickle pie pier pig pik pile pill "
183
+ "pillar pilot pin pine ping pink pip pipe pir piro pit pitch pivot pix pizza place "
184
+ "placer plain plan planet plant plash plass plat plate play player please pled plex "
185
+ "pliant plied plier pliers plies plot ploy pluck plug plural plus ply pod poi point "
186
+ "poke pol polar pole policy polis poly pom pon pond ponder pone ponent pong pont pony "
187
+ "pool poor pop poral port porta portal ported porter porto pose poser posing posit poss "
188
+ "post postal posted poster pot pow power pox pray prefer prefix prep preset press prest "
189
+ "pretty price priced prim prima prime primer print prior pro prob probe prod prof "
190
+ "profit prog prompt proof prop proper props prote proto prove proved prow proxy pseudo "
191
+ "psi pst psych puan pub public pul pull pulse pump pun punt pur pure purple puru push "
192
+ "put python qua quad quake quan quant quare quart quat queen query quest quet queue "
193
+ "quick quid quiet quila quin quina quip quire quis quit quite quiz quo quot quota quote "
194
+ "raad rab rabbit race rach rack rad rada radio radius raff raft rag rage rah raid rail "
195
+ "rain rais raise raised raiser raj rale ram rama rame ran rana rance rand random rang "
196
+ "range rank rant rap rapid rapper rare ras rase rash rat rate rated rather rating ratio "
197
+ "ration raul raw rax ray rea reach react read reader ready reak real really realm ream "
198
+ "rear reason reb recent recipe reck record rect red redd rede redo reduce redux ree "
199
+ "reef reek reem reen rees reet ref refer refix refund reg region reh rein reis reit "
200
+ "reject rel reland relay reload remain reman remark remote remove renal rename rend "
201
+ "render renew rent rep repair repeat repen reply report resent reset resh resize resp "
202
+ "rest result resume ret retain retan retch retry return reuse rev reve revet review "
203
+ "reward rex rhein rho ria rial rib ric rice rich richt rick rid ridden ride ridge rie "
204
+ "rier riff rift rig right rik rim rine ring ringer rink rio riot rip rise risk rist rit "
205
+ "rita rite rive river rix roach road rob robe robot roc rock rocket rod rodd roe rog "
206
+ "roger roi roit rok roke roker role rolled roller rolley roman rome ron rone rong roof "
207
+ "rook room roon root rope ropy rosa rose ross rot rota rotate rote rottle roud rough "
208
+ "round roup route router rove row rowing rox roy rub ruby ruck rud ruff rug rule rum "
209
+ "run runner rupa rus rush rust rut ruta sab sad safe sage said sak sal salary sale salt "
210
+ "sam same sample san sand sans sap sar sass sat save saved saving say scala scalar "
211
+ "scale scaled scan scape scar scene schema scheme school schule scient scious scope "
212
+ "score screen scribe script scroll sea seal sealed search season seat sec second secret "
213
+ "sect sector secure seed seeing seek seen seer seg segue seid seit sele select self "
214
+ "seller semble semi sen send sender sense sensor sept ser sera seri serial series sero "
215
+ "sert serve server servo sess set sett setter setup seven sey sha shade shader shadow "
216
+ "shake shall shan shape share sharp shaw shay she shed sheet shell shen sheng shi "
217
+ "shield shift shim shin shine ship shire shirt shiv sho shock shoe shop shore short "
218
+ "shot should show shown shu sia sib sic sid side sie sig sigma sign signal sil silent "
219
+ "silver sim simple sin since sing single sink sip sir sis sit six size sizes skal skat "
220
+ "sker ski skill skim skin skip sky sla slam slash sled sleep slice slick slide slider "
221
+ "slot slow slug small smart smarty smith smooth snake snap snow soap sob soc social "
222
+ "sock socket soe soever soft soil sol solar sold sole solid solo solute solve solver "
223
+ "some son song sons soon sorry sort sorted sou sound source south soy spa space spam "
224
+ "span spann spar spark spart spawn spec sped speech speed spell spent sphere spiel "
225
+ "spill spin spire spired splice split spoken spor sport sports spot spread spring "
226
+ "sprite spur spy square ssi stable stack staff stag stage stake stall stamp stan stance "
227
+ "stand star start stash state static status staw stay stead steady steam steel steen "
228
+ "stein stell stem sten step stern stick sticks sticky stid still stim stime sting stock "
229
+ "stocks stod stof stoff stone stood stop store storm story stra stract strain strand "
230
+ "strap stre stream street streng stress strict stride strike string strip stripe strix "
231
+ "stroke strom strong stu stub stud studio study stuff stum sty style sub submit subset "
232
+ "such suffix suite summer sup super sur sure surf survey sus swap sweet swift swiper "
233
+ "switch symbol sync syntax system taa tab tabla table tables tablet tach tag tai tail "
234
+ "tain taj take taken taking tal talk tam tan tank tap tar tara tare target task taste "
235
+ "tat tau tax tay tea teach team tear tec tech ted tee teen teg teil tele tell tema temp "
236
+ "temper tempo tempt ten tenant tensor tent tera teras teri term termin tern terna "
237
+ "ternal terne test teste tested tester text tez tha thai than thane thank thanks that "
238
+ "the their them theme then these theta they thin thing think third this tho thon thorn "
239
+ "those though thouse thread three throw thumb thus thy tic tick ticker ticket tid tie "
240
+ "tier tig tight til tile tim time timer times tin ting tiny tip title titre toa toast "
241
+ "tod today toe tog toggle toi token tol tom ton tone tong too tool toon top topia topic "
242
+ "tops tor torch tot total tou touch tour tout tower town tox toy tra trace track tract "
243
+ "trade trag trail train trait tram tran trap trash travel tray tree trend trent tri "
244
+ "trial trica trim trin trip tron truck true trust truth try trying tube tum tun tur "
245
+ "turn turned tweet twig two type uang ubi uca udal udder udi udo uds ugh uji uke ula "
246
+ "ule ull ulla uller ulling ultima ultimo ulu umber umble umbo ume ump umph umu una "
247
+ "unable unal unami unbind unca uncate unci uncia unde unden under undo undy unie union "
248
+ "unique unit unity unken unless unlink unlock unsafe unset untar until unto unused "
249
+ "unwrap unze update upload upo upon upper ura ural uran urate urban urd urde urdu ure "
250
+ "urent urf urge urgent urger uri uria urlar urn urna urnal urning uro urs ursa ursal "
251
+ "ursus uru urus usable usage usar use used user ush usher ussels ust uster usual usun "
252
+ "uta utas utch ute utile utter utu utum uva vai vain vair val vale vali valid vall "
253
+ "valor value valuer van vang vara vare vari varna vary vas vast vat vault veal vector "
254
+ "veda vedic vee veen veer vei veil vela vell velo velte vend vendor vent venter venue "
255
+ "venus vera verb verd verify verity vern versal verse versed verst vert vertex very "
256
+ "vest vester vet via vic vice video vie vier view viewer vill villa ville vim vina "
257
+ "vince vine vino viol vip vira viral viron virus vis visa vise vision visit visor vista "
258
+ "visual vita vive vod voet vog voice void vol vole volent volt volume vote vum waar wab "
259
+ "wad wadi wag wagon wah wain wait waka wake wale wali walk walker wall wallet wan wand "
260
+ "wander wandle wane wang want wany wap war ward warden ware warf warm warn warp wart "
261
+ "was wash washed washer wat watch water wave wax way wayne ways weak wealth weapon wear "
262
+ "web wed wee weekly ween weep weet wei weka well welt wem wen wend went"
263
+ ).split()
264
+ )
id_agent/py.typed ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,527 @@
1
+ Metadata-Version: 2.4
2
+ Name: id-agent
3
+ Version: 1.1.3
4
+ Summary: Human-readable, token-efficient IDs for AI agent systems
5
+ Project-URL: Homepage, https://github.com/vostride/id-agent#readme
6
+ Project-URL: Repository, https://github.com/vostride/id-agent
7
+ Project-URL: Issues, https://github.com/vostride/id-agent/issues
8
+ Author: Vostride
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent,ai,collision-resistant,human-readable,id,identifier,llm,token-efficient,uuid-alternative
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Provides-Extra: dev
24
+ Requires-Dist: build>=1.3; extra == 'dev'
25
+ Requires-Dist: pytest>=9.0; extra == 'dev'
26
+ Requires-Dist: ruff>=0.15; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ <div align="center">
30
+ <img src="https://raw.githubusercontent.com/vostride/id-agent/main/docs/id-agent.png" alt="id-agent - Token efficient IDs for AI agents" />
31
+
32
+ [![Featured on Hacker News](https://img.shields.io/badge/Hacker%20News-featured-ff6600?style=flat&logo=ycombinator&logoColor=white&colorA=13110f)](https://news.ycombinator.com/item?id=48191852)
33
+ [![npm](https://img.shields.io/npm/dm/id-agent?style=flat&colorA=13110f&colorB=196872)](https://npm.chart.dev/id-agent?primary=neutral&gray=neutral&theme=dark)
34
+ [![npm version](https://img.shields.io/npm/v/id-agent.svg?style=flat&colorA=13110f&colorB=196872)](https://www.npmjs.com/package/id-agent)
35
+ [![GitHub stars](https://img.shields.io/github/stars/vostride/id-agent?style=flat&colorA=13110f&colorB=196872)](https://github.com/vostride/id-agent/stargazers)
36
+
37
+ <p>
38
+ <a href="#the-math">The Math</a>
39
+ ·
40
+ <a href="#javascript--typescript">JavaScript</a>
41
+ ·
42
+ <a href="#python">Python</a>
43
+ ·
44
+ <a href="https://www.npmjs.com/package/id-agent">npm</a>
45
+ ·
46
+ <a href="https://vostride.com">Vostride AI</a>
47
+ </p>
48
+ </div>
49
+
50
+ # id-agent
51
+
52
+ > Token efficient IDs for AI agents.
53
+
54
+ AI agents pass IDs through prompts, tool calls, logs, traces, and memory. UUIDs are great for databases, but they are expensive in context windows and hard for humans or LLMs to copy reliably. id-agent gives you word-based IDs that are compact in modern tokenizers, readable in logs, and configurable by collision budget.
55
+
56
+ Where UUIDs cost about 23 tokens and get hallucinated by LLMs, id-agent produces memorable word-based IDs at about 14 tokens with strong collision resistance. It is built for the context window, not just the database.
57
+
58
+ - **Human-readable** -- word-based IDs that humans and LLMs can remember.
59
+ - **Token-efficient** -- every word in the wordlist is exactly 1 BPE token on o200k_base.
60
+ - **Collision-safe** -- configurable entropy from about 12 to 192 bits.
61
+ - **Validated inputs** -- descriptive validation on all public APIs.
62
+
63
+ ## The Math
64
+
65
+ ### Token Cost Comparison
66
+
67
+ | Format | Example | Tokens (o200k_base) | Collision Resistance |
68
+ |--------|---------|---------------------|----------------------|
69
+ | UUID v4 | `89b842d9-6df9-4cf4-8db0-9dc3aed3cfd7` | about 23 | 122 bits |
70
+ | id-agent (default, 8 words) | `urd-antes-sorry-pac-dire-total-expire-going` | about 14 | about 96 bits |
71
+ | id-agent (5 words) | `frame-beer-bell-tog-hoot` | about 8 | about 60 bits |
72
+
73
+ <table align="center">
74
+ <tr>
75
+ <td align="center"><img src="docs/id-agent-tokenizer.png" alt="id-agent: 12 tokens for 8 words" width="100%" /><br/><strong>id-agent</strong> - 8 words, 43 chars, <strong>12 tokens</strong></td>
76
+ <td align="center"><img src="docs/uuid-tokenizer.png" alt="UUID v4: 26 tokens" width="100%" /><br/><strong>UUID v4</strong> - 36 chars, <strong>26 tokens</strong></td>
77
+ </tr>
78
+ </table>
79
+
80
+ ### Entropy
81
+
82
+ Each word is drawn uniformly from a curated **4096-word list** (2^12). Every position in the ID is an independent random selection:
83
+
84
+ ```text
85
+ Entropy per word = log2(4096) = 12 bits
86
+ Total entropy = words * 12 bits
87
+ ID space = 4096^words = 2^(words * 12)
88
+ ```
89
+
90
+ This holds regardless of individual word length. A 3-character word and a 6-character word both contribute exactly 12 bits because the attacker must guess from the same 4096-word pool.
91
+
92
+ ### Collision Probability
93
+
94
+ The probability of at least one collision among `n` randomly generated IDs:
95
+
96
+ ```text
97
+ P(collision) ~= n^2 / (2 * 2^b)
98
+
99
+ where b = total bits of entropy
100
+ ```
101
+
102
+ This is the birthday paradox approximation, valid when P is small (P < 0.01).
103
+
104
+ | Words | Bits | ID Space | P @ 1K | P @ 10K | P @ 100K | P @ 1M | P @ 1B | 50% collision at |
105
+ |-------|------|----------|--------|---------|----------|--------|--------|------------------|
106
+ | 3 | 36 | 6.9 * 10^10 | 7.3 * 10^-6 | 7.3 * 10^-4 | 7.3 * 10^-2 | about 1 | about 1 | about 309K items |
107
+ | 4 | 48 | 2.8 * 10^14 | 1.8 * 10^-9 | 1.8 * 10^-7 | 1.8 * 10^-5 | 1.8 * 10^-3 | about 1 | about 20M items |
108
+ | 5 | 60 | 1.2 * 10^18 | 4.3 * 10^-13 | 4.3 * 10^-11 | 4.3 * 10^-9 | 4.3 * 10^-7 | 0.43 | about 1.3B items |
109
+ | **8** | **96** | **7.9 * 10^28** | **6.3 * 10^-24** | **6.3 * 10^-22** | **6.3 * 10^-20** | **6.3 * 10^-18** | **6.3 * 10^-12** | **about 331T items** |
110
+ | 10 | 120 | 1.3 * 10^36 | 3.8 * 10^-31 | 3.8 * 10^-29 | 3.8 * 10^-27 | 3.8 * 10^-25 | 3.8 * 10^-19 | about 1.4 * 10^18 items |
111
+ | *UUID v4* | *122* | *5.3 * 10^36* | *9.4 * 10^-32* | *9.4 * 10^-30* | *9.4 * 10^-28* | *9.4 * 10^-26* | *9.4 * 10^-20* | *about 2.7 * 10^18 items* |
112
+
113
+ The default 8-word ID is safe for more than 300 trillion items before reaching a 50% collision chance. Most applications will never generate more than a few million IDs.
114
+
115
+ ### Worked Example
116
+
117
+ For the default 8-word ID at 1 million items:
118
+
119
+ ```text
120
+ b = 8 * 12 = 96 bits
121
+ n = 1,000,000
122
+
123
+ P ~= (10^6)^2 / (2 * 2^96)
124
+ = 10^12 / (2 * 7.92 * 10^28)
125
+ = 10^12 / (1.58 * 10^29)
126
+ = 6.3 * 10^-18
127
+ ```
128
+
129
+ That is roughly **1 in 158 quadrillion**.
130
+
131
+ ### Token Cost
132
+
133
+ All measurements use **o200k_base** (GPT-4o, GPT-4.1, o1, o3) through tiktoken. Token counts vary slightly per ID because of BPE merge behavior with hyphens. Values below are averages over 100 samples:
134
+
135
+ | Format | Avg Tokens | Entropy | Tokens Saved vs UUID | Savings |
136
+ |--------|------------|---------|----------------------|---------|
137
+ | UUID v4 | **about 23** | 122 bits | -- | -- |
138
+ | id-agent (3 words) | **about 5** | 36 bits | about 18 | **78%** |
139
+ | id-agent (5 words) | **about 8** | 60 bits | about 15 | **65%** |
140
+ | **id-agent (8 words, default)** | **about 14** | **96 bits** | **about 9** | **39%** |
141
+ | id-agent (10 words) | **about 17** | 120 bits | about 6 | **26%** |
142
+
143
+ ### Choosing Word Count
144
+
145
+ The right word count depends on your scale. The default of 8 is deliberately conservative and globally safe.
146
+
147
+ | Scale | Recommended | Entropy | Why |
148
+ |-------|-------------|---------|-----|
149
+ | Dev/testing | `words: 3` | 36 bits | Fast, memorable, about 5 tokens. Collides at about 300K items. |
150
+ | Team tools | `words: 4` | 48 bits | Safe to about 20M items. Good for internal APIs. |
151
+ | Production SaaS | `words: 5` | 60 bits | Safe to about 1B items. 65% token savings vs UUID. |
152
+ | High-volume / distributed | `words: 8` (default) | 96 bits | Safe to about 300T items. The safe default. |
153
+ | UUID-equivalent | `words: 10` | 120 bits | Matches UUID v4 collision math. |
154
+
155
+ ### Why Words Beat Hex
156
+
157
+ BPE tokenizers used by modern LLMs were trained on natural language. Short English words are single tokens by design. UUIDs are hex strings that split unpredictably:
158
+
159
+ ```text
160
+ "storm-delta-stone" => 4 tokens (3 words + separators)
161
+ "dc193952-186a-4645" => 11 tokens (same 18 characters)
162
+ ```
163
+
164
+ id-agent's wordlist is curated so every word is exactly **1 BPE token** on o200k_base. The hyphens add about 1 token per 6 words due to BPE merge behavior. This is why word-based IDs are fundamentally more token-efficient than random hex or alphanumeric strings.
165
+
166
+ ## Implementations
167
+
168
+ id-agent is implemented as first-class packages for:
169
+
170
+ - [JavaScript / TypeScript](#javascript--typescript)
171
+ - [Python](#python)
172
+
173
+ Both implementations use the same 4096-word list, separator rules, entropy model, prefix validation, and public behavior.
174
+
175
+ ## How It Works
176
+
177
+ id-agent uses a curated wordlist of 4096 English words, each verified as a single BPE token on the o200k_base tokenizer used by GPT-4o and GPT-4.1. Words are 3-6 characters and filtered for offensive terms and homophones.
178
+
179
+ Random IDs use cryptographically secure randomness. Deterministic IDs use HMAC-SHA256, mapping hash bytes to wordlist indices.
180
+
181
+ ## JavaScript / TypeScript
182
+
183
+ ### Install
184
+
185
+ ```bash
186
+ npm install id-agent
187
+ ```
188
+
189
+ ```bash
190
+ pnpm add id-agent
191
+ ```
192
+
193
+ ### Quick Start
194
+
195
+ ```typescript
196
+ import { idAgent } from 'id-agent'
197
+
198
+ const id = idAgent()
199
+ // => "urd-antes-sorry-pac-dire-total-expire-going"
200
+
201
+ const taskId = idAgent({ prefix: 'task' })
202
+ // => "task_slide-exact-cede-bury-linge-ease-bean-impact"
203
+
204
+ const short = idAgent({ words: 3 })
205
+ // => "front-reject-tho"
206
+ ```
207
+
208
+ ### Random IDs
209
+
210
+ Generate a random, human-readable ID.
211
+
212
+ ```typescript
213
+ import { idAgent } from 'id-agent'
214
+
215
+ idAgent() // 8 words, about 96 bits
216
+ idAgent({ words: 5 }) // 5 words, about 60 bits
217
+ idAgent({ prefix: 'user' }) // "user_cloud-train-scope-frame-match-level-paint-field"
218
+ ```
219
+
220
+ **Options:**
221
+
222
+ | Option | Type | Default | Description |
223
+ |--------|------|---------|-------------|
224
+ | `prefix` | `string` | `undefined` | Type prefix, lowercase alphanumeric only |
225
+ | `words` | `number` | `8` | Number of words, 1-16. Controls entropy: words * 12 bits |
226
+
227
+ ### Deterministic IDs
228
+
229
+ Generate a deterministic ID from a string input using HMAC-SHA256. The same input always produces the same ID.
230
+
231
+ ```typescript
232
+ import { idAgent } from 'id-agent'
233
+
234
+ const id = await idAgent.from('user@example.com')
235
+
236
+ const namespaced = await idAgent.from('user@example.com', {
237
+ namespace: 'my-app',
238
+ prefix: 'user',
239
+ words: 5,
240
+ })
241
+ ```
242
+
243
+ **Options:**
244
+
245
+ | Option | Type | Default | Description |
246
+ |--------|------|---------|-------------|
247
+ | `prefix` | `string` | `undefined` | Type prefix, lowercase alphanumeric only |
248
+ | `words` | `number` | `8` | Number of words, 1-16 |
249
+ | `namespace` | `string` | `'id-agent'` | HMAC key for domain separation |
250
+
251
+ ### Parse
252
+
253
+ Parse any id-agent ID into its components. Supports hyphen-separated and underscore-separated words. Returns `null` for unrecognized formats.
254
+
255
+ ```typescript
256
+ import { parse } from 'id-agent'
257
+
258
+ parse('task_storm-delta-stone')
259
+ // => { prefix: 'task', words: ['storm', 'delta', 'stone'], wordCount: 3, bits: 36, raw: 'task_storm-delta-stone', format: 'readable' }
260
+
261
+ parse('task_storm_delta_stone')
262
+ // => { prefix: 'task', words: ['storm', 'delta', 'stone'], wordCount: 3, bits: 36, raw: 'task_storm_delta_stone', format: 'readable' }
263
+ ```
264
+
265
+ ### Validate
266
+
267
+ Check if a string is a valid id-agent ID. Validation confirms that all words exist in the `WORDLIST`.
268
+
269
+ ```typescript
270
+ import { validate } from 'id-agent'
271
+
272
+ validate('storm-delta-stone')
273
+ // => { valid: true, prefix: undefined, wordCount: 3 }
274
+
275
+ validate('task_jump-notaword')
276
+ // => { valid: false, reason: 'unknown words: notaword' }
277
+
278
+ validate('INVALID')
279
+ // => { valid: false, reason: 'contains uppercase characters' }
280
+ ```
281
+
282
+ ### Alias Maps
283
+
284
+ Create a bidirectional alias map for token reduction in LLM contexts. Maps long IDs to short word-based aliases with replace and restore support.
285
+
286
+ ```typescript
287
+ import { createAliasMap } from 'id-agent'
288
+
289
+ const aliases = createAliasMap({ words: 3 })
290
+ aliases.set('8cdda07b-85d2-459c-8a2a-83c8f9245dbe')
291
+ // => "storm-delta-stone"
292
+
293
+ aliases.get('storm-delta-stone')
294
+ // => "8cdda07b-85d2-459c-8a2a-83c8f9245dbe"
295
+
296
+ const text = 'Process 8cdda07b-85d2-459c-8a2a-83c8f9245dbe then 6ba7b810-9dad-11d1-80b4-00c04fd430c8'
297
+ const shortened = aliases.replace(text, {
298
+ pattern: /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
299
+ })
300
+ // => "Process storm-delta-stone then cloud-train-scope"
301
+
302
+ const restored = aliases.restore(shortened)
303
+ // => original text
304
+
305
+ aliases.entries()
306
+ // => [["8cdda07b-85d2-459c-8a2a-83c8f9245dbe", "storm-delta-stone"], ...]
307
+ ```
308
+
309
+ **Options:**
310
+
311
+ | Option | Type | Required | Description |
312
+ |--------|------|----------|-------------|
313
+ | `words` | `number` | Yes | Number of words per alias, 1-16 |
314
+
315
+ ### Duplicate Detection
316
+
317
+ Scan text for duplicate IDs using a regex pattern. This is a pure function with no filesystem access.
318
+
319
+ ```typescript
320
+ import { detectDuplicates } from 'id-agent'
321
+
322
+ const dupes = detectDuplicates({
323
+ pattern: /[a-z]+(?:-[a-z]+)+/,
324
+ text: 'Found storm-delta-stone in file A and storm-delta-stone in file B',
325
+ })
326
+ // => [{ id: 'storm-delta-stone', count: 2 }]
327
+
328
+ const dupes2 = detectDuplicates({
329
+ pattern: /task_[a-z]+(?:-[a-z]+)+/,
330
+ text: ['const x = "task_red-fox-run"', 'const y = "task_red-fox-run"'],
331
+ })
332
+ ```
333
+
334
+ **Options:**
335
+
336
+ | Option | Type | Description |
337
+ |--------|------|-------------|
338
+ | `pattern` | `RegExp` | Regex to match IDs in text |
339
+ | `text` | `string \| string[]` | Text to scan for duplicates |
340
+
341
+ ### WORDLIST
342
+
343
+ Direct access to the curated 4096-word list. Every word is exactly 1 BPE token on o200k_base. The JavaScript export is a frozen array.
344
+
345
+ ```typescript
346
+ import { WORDLIST } from 'id-agent'
347
+
348
+ WORDLIST.length // => 4096
349
+ Object.isFrozen(WORDLIST) // => true
350
+ ```
351
+
352
+ ## Python
353
+
354
+ ### Install
355
+
356
+ ```bash
357
+ pip install id-agent
358
+ ```
359
+
360
+ ```bash
361
+ uv add id-agent
362
+ ```
363
+
364
+ ### Quick Start
365
+
366
+ ```python
367
+ from id_agent import generate
368
+
369
+ id = generate()
370
+ # => "urd-antes-sorry-pac-dire-total-expire-going"
371
+
372
+ task_id = generate(prefix="task")
373
+ # => "task_slide-exact-cede-bury-linge-ease-bean-impact"
374
+
375
+ short = generate(words=3)
376
+ # => "front-reject-tho"
377
+ ```
378
+
379
+ ### Random IDs
380
+
381
+ Generate a random, human-readable ID.
382
+
383
+ ```python
384
+ from id_agent import generate
385
+
386
+ generate() # 8 words, about 96 bits
387
+ generate(words=5) # 5 words, about 60 bits
388
+ generate(prefix="user") # "user_cloud-train-scope-frame-match-level-paint-field"
389
+ ```
390
+
391
+ **Options:**
392
+
393
+ | Option | Type | Default | Description |
394
+ |--------|------|---------|-------------|
395
+ | `prefix` | `str \| None` | `None` | Type prefix, lowercase alphanumeric only |
396
+ | `words` | `int` | `8` | Number of words, 1-16. Controls entropy: words * 12 bits |
397
+
398
+ ### Deterministic IDs
399
+
400
+ Generate a deterministic ID from a string input using HMAC-SHA256. The same input always produces the same ID.
401
+
402
+ ```python
403
+ from id_agent import generate_deterministic
404
+
405
+ id = generate_deterministic("user@example.com")
406
+
407
+ namespaced = generate_deterministic(
408
+ "user@example.com",
409
+ namespace="my-app",
410
+ prefix="user",
411
+ words=5,
412
+ )
413
+ ```
414
+
415
+ **Options:**
416
+
417
+ | Option | Type | Default | Description |
418
+ |--------|------|---------|-------------|
419
+ | `prefix` | `str \| None` | `None` | Type prefix, lowercase alphanumeric only |
420
+ | `words` | `int` | `8` | Number of words, 1-16 |
421
+ | `namespace` | `str` | `"id-agent"` | HMAC key for domain separation |
422
+
423
+ ### Parse
424
+
425
+ Parse any id-agent ID into its components. Supports hyphen-separated and underscore-separated words. Returns `None` for unrecognized formats.
426
+
427
+ ```python
428
+ from id_agent import parse
429
+
430
+ parse("task_storm-delta-stone")
431
+ # => ParsedId(prefix="task", words=("storm", "delta", "stone"), word_count=3, bits=36.0, raw="task_storm-delta-stone", format="readable")
432
+
433
+ parse("task_storm_delta_stone")
434
+ # => ParsedId(prefix="task", words=("storm", "delta", "stone"), word_count=3, bits=36.0, raw="task_storm_delta_stone", format="readable")
435
+ ```
436
+
437
+ ### Validate
438
+
439
+ Check if a string is a valid id-agent ID. Validation confirms that all words exist in the `WORDLIST`.
440
+
441
+ ```python
442
+ from id_agent import validate
443
+
444
+ validate("storm-delta-stone")
445
+ # => ValidationResult(valid=True, prefix=None, word_count=3, words=("storm", "delta", "stone"))
446
+
447
+ validate("task_jump-notaword")
448
+ # => ValidationResult(valid=False, reason="unknown words: notaword", unknown_words=("notaword",))
449
+
450
+ validate("INVALID")
451
+ # => ValidationResult(valid=False, reason="contains uppercase characters")
452
+ ```
453
+
454
+ ### Alias Maps
455
+
456
+ Create a bidirectional alias map for token reduction in LLM contexts. Maps long IDs to short word-based aliases with replace and restore support.
457
+
458
+ ```python
459
+ from id_agent import AliasMap
460
+
461
+ aliases = AliasMap(words=3)
462
+ aliases.set("8cdda07b-85d2-459c-8a2a-83c8f9245dbe")
463
+ # => "storm-delta-stone"
464
+
465
+ aliases.get("storm-delta-stone")
466
+ # => "8cdda07b-85d2-459c-8a2a-83c8f9245dbe"
467
+
468
+ text = "Process 8cdda07b-85d2-459c-8a2a-83c8f9245dbe then 6ba7b810-9dad-11d1-80b4-00c04fd430c8"
469
+ shortened = aliases.replace(
470
+ text,
471
+ pattern=r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
472
+ )
473
+ # => "Process storm-delta-stone then cloud-train-scope"
474
+
475
+ restored = aliases.restore(shortened)
476
+ # => original text
477
+
478
+ aliases.entries()
479
+ # => (("8cdda07b-85d2-459c-8a2a-83c8f9245dbe", "storm-delta-stone"), ...)
480
+ ```
481
+
482
+ **Options:**
483
+
484
+ | Option | Type | Required | Description |
485
+ |--------|------|----------|-------------|
486
+ | `words` | `int` | Yes | Number of words per alias, 1-16 |
487
+
488
+ ### Duplicate Detection
489
+
490
+ Scan text for duplicate IDs using a regex pattern. This is a pure function with no filesystem access.
491
+
492
+ ```python
493
+ from id_agent import detect_duplicates
494
+
495
+ dupes = detect_duplicates(
496
+ pattern=r"[a-z]+(?:-[a-z]+)+",
497
+ text="Found storm-delta-stone in file A and storm-delta-stone in file B",
498
+ )
499
+ # => [DuplicateResult(id="storm-delta-stone", count=2)]
500
+
501
+ dupes2 = detect_duplicates(
502
+ pattern=r"task_[a-z]+(?:-[a-z]+)+",
503
+ text=['const x = "task_red-fox-run"', 'const y = "task_red-fox-run"'],
504
+ )
505
+ ```
506
+
507
+ **Options:**
508
+
509
+ | Option | Type | Description |
510
+ |--------|------|-------------|
511
+ | `pattern` | `str \| re.Pattern[str]` | Regex to match IDs in text |
512
+ | `text` | `str \| Iterable[str]` | Text to scan for duplicates |
513
+
514
+ ### WORDLIST
515
+
516
+ Direct access to the curated 4096-word list. Every word is exactly 1 BPE token on o200k_base. The Python export is an immutable tuple.
517
+
518
+ ```python
519
+ from id_agent import WORDLIST
520
+
521
+ len(WORDLIST) # => 4096
522
+ isinstance(WORDLIST, tuple) # => True
523
+ ```
524
+
525
+ ## License
526
+
527
+ MIT
@@ -0,0 +1,12 @@
1
+ id_agent/__init__.py,sha256=EswACnwwV1bcjrBX48b-rnW6s6WssSVawjcYJAVDPcQ,502
2
+ id_agent/_alias.py,sha256=xKrNPGSQqNVjw-wxKeB66aqUFxAClEdmKY4evhvjqG8,2941
3
+ id_agent/_detect.py,sha256=mM7v0xUyDOkyfElUSBJWA_X-AlRp33YDXSJpfUnIZ54,1447
4
+ id_agent/_generate.py,sha256=mS2YOnHTYzZLJFt6VcPFsa26dwJ3teZ8dvjonrv766A,2375
5
+ id_agent/_parse.py,sha256=UHcsO8yOm3b1bPhcbmACrCSCwGOCt9128_mdN7tr3rQ,1915
6
+ id_agent/_types.py,sha256=2N00hZj-AyExvN5E19IZvhXZaCkZuVVbVJhgAWwRYfo,578
7
+ id_agent/_wordlist.py,sha256=ZQFrD2JWREh5dn2B2NYT79L9T3ZWjz3omjgGo-oj-cY,24806
8
+ id_agent/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
9
+ id_agent-1.1.3.dist-info/METADATA,sha256=UT-QECLtyAmpfP9trBGnZgmwyoMl-wkL8zveZbZq8F0,18100
10
+ id_agent-1.1.3.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
11
+ id_agent-1.1.3.dist-info/licenses/LICENSE,sha256=urCS6Lg0qwsNOG8gceeLh6Yx92oTgG5QxD4laIjocZ4,1078
12
+ id_agent-1.1.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 id-agent contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.