cortad 0.3.1-rc.9 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +8 -5
  2. package/lib/app-port.mjs +9 -0
  3. package/lib/auto-reach.mjs +88 -0
  4. package/lib/backing.mjs +230 -0
  5. package/lib/calls.mjs +23 -0
  6. package/lib/cli.mjs +115 -30
  7. package/lib/contact-text.mjs +167 -0
  8. package/lib/data.mjs +16 -22
  9. package/lib/dissection-text.mjs +232 -0
  10. package/lib/document.mjs +75 -0
  11. package/lib/env-secrets.mjs +48 -0
  12. package/lib/failed-text.mjs +36 -0
  13. package/lib/fresh.mjs +33 -12
  14. package/lib/health.mjs +61 -0
  15. package/lib/held.mjs +67 -0
  16. package/lib/home.mjs +55 -17
  17. package/lib/hosted.mjs +201 -0
  18. package/lib/identities.mjs +1 -1
  19. package/lib/keeping.mjs +162 -0
  20. package/lib/ledger.mjs +20 -0
  21. package/lib/lock.mjs +6 -1
  22. package/lib/mcp.mjs +52 -18
  23. package/lib/mint.mjs +179 -38
  24. package/lib/mongo.mjs +74 -0
  25. package/lib/numbers-text.mjs +68 -0
  26. package/lib/pg.mjs +118 -0
  27. package/lib/proc.mjs +100 -0
  28. package/lib/proof.mjs +1014 -0
  29. package/lib/proxy.mjs +184 -0
  30. package/lib/pyhook/cortad_sites.py +218 -0
  31. package/lib/pyhook/cortad_tied.py +291 -0
  32. package/lib/pyhook/sitecustomize.py +1175 -166
  33. package/lib/qdrant.mjs +47 -0
  34. package/lib/reach.mjs +223 -0
  35. package/lib/read-text.mjs +78 -24
  36. package/lib/redis.mjs +130 -0
  37. package/lib/register.mjs +36 -27
  38. package/lib/replay.mjs +464 -34
  39. package/lib/runner.mjs +85 -0
  40. package/lib/service.mjs +25 -0
  41. package/lib/spec.mjs +7 -4
  42. package/lib/start.mjs +33 -2
  43. package/lib/stick.mjs +65 -25
  44. package/lib/stores.mjs +347 -0
  45. package/lib/switches.mjs +36 -0
  46. package/lib/tether.mjs +9 -0
  47. package/lib/text.mjs +314 -179
  48. package/lib/tied.cjs +217 -0
  49. package/lib/trace.cjs +386 -514
  50. package/lib/verbs.mjs +155 -38
  51. package/lib/wire.cjs +882 -0
  52. package/lib/words.mjs +2 -0
  53. package/lib/writes.mjs +2 -2
  54. package/local.mjs +661 -297
  55. package/package.json +31 -1
  56. package/skill/SKILL.md +49 -42
  57. package/skill/references/results.md +279 -102
@@ -0,0 +1,291 @@
1
+ # Which request a model call belongs to when it is made outside every request: a framework that
2
+ # queues the handler and runs it later on its own worker (a UI framework's queue), an app that hands
3
+ # the work to a thread pool, a job whose result is fetched on a second request.
4
+ #
5
+ # The call is pinned to the request that asked: the one still open whose words are in the prompt, the
6
+ # one an open request is tied to by an id it sent or got back (the answer is being fetched), or, when
7
+ # the call starts before that fetch, the newest request whose words are in the prompt, if every such
8
+ # request shares its id (one client's turn). Anything less pins nothing: never a guess.
9
+ #
10
+ # The request that asked answered only when it ended with none of its calls still running; otherwise
11
+ # the next request tied to it by an id carries the answer, and completes the exchange when it ends.
12
+ # A request whose reply went out before any model call was made for it (a webhook that queues the
13
+ # message and answers at once) and that no request comes to fetch is answered later, by its model:
14
+ # the exchange is written once its calls have gone quiet.
15
+ import collections
16
+ import contextvars
17
+ import json
18
+ import re
19
+ import time
20
+ from urllib.parse import parse_qsl
21
+
22
+ # A value made to name one thing: a session hash, an event id, a job number.
23
+ ID = re.compile(r"^(?=.*\d)[\w.:-]{6,200}$")
24
+ RECENT_KEPT, RECENT_MS = 32, 300000
25
+ # The requests a client sends just before the one that asked, carrying the same id, are part of its
26
+ # turn (a chat UI shows the message with one event, then answers it with the next).
27
+ BEFORE_MS = 60000
28
+ PROMPTS_KEPT, PROMPT_HEAD = 64, 500
29
+ # The ask the last call pinned in this handler's context was made for. A queue runs each handler in a
30
+ # context of its own, so its later calls, which carry none of the person's words, are told apart by
31
+ # it when several asks are open at once. Only a tiebreak: it never outranks what the rules decide.
32
+ _HELD = contextvars.ContextVar("cortad_asker", default=None)
33
+
34
+
35
+ def _scalars(v, out, depth=0):
36
+ if depth > 12 or len(out) > 2000:
37
+ return out
38
+ if isinstance(v, str) or (isinstance(v, int) and not isinstance(v, bool)):
39
+ out.append(str(v))
40
+ elif isinstance(v, list):
41
+ for x in v:
42
+ _scalars(x, out, depth + 1)
43
+ elif isinstance(v, dict):
44
+ for x in v.values():
45
+ _scalars(x, out, depth + 1)
46
+ return out
47
+
48
+
49
+ def _json(raw):
50
+ try:
51
+ return json.loads(raw)
52
+ except (TypeError, ValueError):
53
+ return None
54
+
55
+
56
+ def _sentence(v):
57
+ return len(v) >= 8 and (any(c.isspace() for c in v) or not v.isascii())
58
+
59
+
60
+ # Whether a reply says something of its own, read as the person's words are: an "ok" or an id says nothing.
61
+ def _speaks(text):
62
+ v = _json(text)
63
+ return any(_sentence(x) for x in (_scalars(v, []) if v is not None else [str(text or "")]))
64
+
65
+
66
+ def _address_ids(req):
67
+ path, _, query = req["path"].partition("?")
68
+ return {v for v in path.split("/") + [v for _, v in parse_qsl(query)] if ID.match(v)}
69
+
70
+
71
+ class Tied:
72
+ # `body_of(req)` and `reply_of(req)` read what a request carried and what the app answered.
73
+ def __init__(self, norm, body_of, reply_of):
74
+ self.norm, self.body_of, self.reply_of = norm, body_of, reply_of
75
+ self.open = {}
76
+ self.recent = collections.deque(maxlen=RECENT_KEPT)
77
+ # The heads of the prompts this process sent: one arriving at a server in the same process (an
78
+ # app that serves its own model) is a model call, never a person asking.
79
+ self.prompts = collections.deque(maxlen=PROMPTS_KEPT)
80
+ # The ask each id's last answered turn was made by: no request up to it is a step of a later
81
+ # turn, even one that never became an exchange.
82
+ self.answered = collections.OrderedDict()
83
+
84
+ def _ids(self, req):
85
+ if "ids" not in req:
86
+ got = _scalars(_json(self.body_of(req)), []) + _scalars(_json(self.reply_of(req)), [])
87
+ req["ids"] = _address_ids(req) | {v for v in got if ID.match(v)}
88
+ return req["ids"]
89
+
90
+ # The person's words: a value with a space or a letter outside ASCII, long enough to be a sentence.
91
+ def _words(self, req):
92
+ return [v for v in _scalars(_json(self.body_of(req)), []) if _sentence(v)]
93
+
94
+ # tied.cjs sentencesIn: a body that is not JSON (a form post, plain text) is read as the text it is.
95
+ def sentences(self, req):
96
+ raw = self.body_of(req)
97
+ v = _json(raw)
98
+ text = raw.decode("utf-8", "replace") if isinstance(raw, (bytes, bytearray)) else str(raw or "")
99
+ return [s for s in (_scalars(v, []) if v is not None else [text]) if _sentence(s)]
100
+
101
+ def _says(self, req, prompt):
102
+ return any(self.norm(v) in prompt for v in self._words(req))
103
+
104
+ # Among one client's requests whose words the prompt carries, the one being answered: the one whose
105
+ # calls have begun and that has not settled (a later step of its work, while the next message sits
106
+ # in the prompt as history the app saved as it arrived), else the one whose own words sit last in
107
+ # the prompt. Requests that all carry the same words are the steps of one turn, the newest the one
108
+ # answered (a chat UI shows the message with one event and answers it with the next). None when
109
+ # that still leaves more than one: a missing answer, never a wrong one.
110
+ def _answering(self, sayers, prompt):
111
+ if len(sayers) == 1:
112
+ return sayers[0]
113
+ begun = [t for t in sayers if t.get("pinned")]
114
+ if len(begun) == 1:
115
+ return begun[0]
116
+ pool = begun or sayers
117
+ words = {id(t): self._words(t) for t in pool}
118
+ own = {id(t): [v for v in words[id(t)] if self.norm(v) in prompt and not any(o is not t and v in words[id(o)] for o in pool)] for t in pool}
119
+ if not any(own.values()):
120
+ return pool[-1]
121
+ first, second = sorted(((max([prompt.rfind(self.norm(v)) for v in own[id(t)]], default=-1), t) for t in pool), key=lambda p: p[0], reverse=True)[:2]
122
+ return first[1] if first[0] >= 0 and first[0] != second[0] else None
123
+
124
+ # The newest request that asked before `req` and shares an id with its address.
125
+ def _asker(self, req, pinned_only=False):
126
+ mine = _address_ids(req)
127
+ now = int(time.time() * 1000)
128
+ for t in reversed(list(self.recent)) if mine else []:
129
+ # A request answered later is fetched afterwards only by a read of it, never by a new message.
130
+ if t["at"] <= req["at"] and now - t["at"] <= RECENT_MS and (t.get("pinned") or not pinned_only) and (not t.get("settled") or req["method"] == "GET"):
131
+ shared = mine & self._ids(t)
132
+ if shared:
133
+ return t, shared
134
+ return None
135
+
136
+ def opened(self, req):
137
+ self.open[req["id"]] = req
138
+ found = self._asker(req, pinned_only=True)
139
+ if found:
140
+ req["asker"], req["tie"] = found
141
+ found[0]["fetched"] = True
142
+
143
+ # The requests of the exchange this request completes, the one that asked last, or None. A
144
+ # request that asked and has not answered is kept to be answered on a later one. Once answered it
145
+ # leaves, written or not: a later turn's fetch tied by the same id is never pinned to it.
146
+ def closed(self, req):
147
+ self.open.pop(req["id"], None)
148
+ asker = req.get("asker")
149
+ if asker and not asker.get("written") and not asker.get("calls_open"):
150
+ since = max([self.answered[i] for i in req["tie"] if self.answered.get(i, asker["at"]) < asker["at"]], default=0)
151
+ before = [p for p in list(self.recent) if p is not asker and not p.get("settled") and since < p["at"] < asker["at"] and asker["at"] - p["at"] <= BEFORE_MS and self._ids(p) & req["tie"]]
152
+ for s in before + [asker]:
153
+ try:
154
+ self.recent.remove(s)
155
+ except ValueError:
156
+ pass
157
+ asker["answered"] = True
158
+ self._answered(req["tie"], asker["at"])
159
+ return before + [asker]
160
+ if req["method"] != "GET" and self.body_of(req)[:PROMPT_HEAD] not in self.prompts and (not req.get("noted") or req.get("calls_open") or req.get("late")):
161
+ self.recent.append(req)
162
+ return None
163
+
164
+ def _answered(self, tie, at):
165
+ for i in tie:
166
+ self.answered.pop(i, None)
167
+ self.answered[i] = at
168
+ while len(self.answered) > RECENT_KEPT * 8:
169
+ self.answered.popitem(last=False)
170
+
171
+ def sent(self, prompt):
172
+ self.prompts.append(prompt[:PROMPT_HEAD])
173
+
174
+ # Asks reached through one session are one client's turns, the newest the one being answered: a
175
+ # client holds a second stream open on its session (a heartbeat) that points at an older ask.
176
+ @staticmethod
177
+ def _one_client(found):
178
+ ties = [f[2] for f in found.values()]
179
+ return all(ties) and bool(set.intersection(*map(set, ties)))
180
+
181
+ # The request that asked for the call made with `sent`, or None. `held`: the request whose context
182
+ # the call was made in after that request's reply went out, which only breaks a tie.
183
+ def pinned(self, sent, held=None):
184
+ prompt = self.norm(sent)
185
+ found = {}
186
+ # Each: the ask, the open fetch to tie to it, and the ids of the session it was reached through.
187
+ # A read of a request already answered later can complete that request's exchange, never be
188
+ # the one a new call answers. A read stands for a request still working, except for a call that
189
+ # carries another unsettled request's words and none of its own: that call is the other one's
190
+ # (a page polling the sender while the next message is worked on). A receipt's fetch carries no
191
+ # message.
192
+ def reads(asker):
193
+ if asker.get("settled"):
194
+ return False
195
+ others = list(self.recent) + list(self.open.values())
196
+ return self._says(asker, prompt) or not any(t is not asker and t["method"] != "GET" and not t.get("settled") and self._says(t, prompt) for t in others)
197
+ for r in list(self.open.values()):
198
+ if r.get("asker"):
199
+ if reads(r["asker"]):
200
+ found[id(r["asker"])] = (r["asker"], None, r.get("tie") or set())
201
+ # A request whose own context made a model call is served there; a call outside it is not its.
202
+ elif r["method"] != "GET" and (not r.get("noted") or r.get("pinned")) and self._says(r, prompt):
203
+ found[id(r)] = (r, None, set())
204
+ else:
205
+ asker = self._asker(r)
206
+ if asker and reads(asker[0]):
207
+ found[id(asker[0])] = (asker[0], (r, asker[1]), asker[1])
208
+ sayers = []
209
+ if not found:
210
+ sayers = [t for t in list(self.recent) if not t.get("settled") and self._says(t, prompt)]
211
+ turn = self._answering(sayers, prompt) if sayers and all(self._ids(t) & self._ids(sayers[-1]) for t in sayers[:-1]) else None
212
+ if turn:
213
+ found[id(turn)] = (turn, None, set())
214
+ candidates = dict(found) or {id(t): (t, None, set()) for t in sayers}
215
+ if len(found) > 1 and not self._one_client(found):
216
+ found = {k: f for k, f in found.items() if self._says(f[0], prompt)}
217
+ if len(found) > 1 and self._one_client(found):
218
+ newest = max(found.values(), key=lambda f: f[0]["at"])
219
+ found = {id(newest[0]): newest}
220
+ if len(found) != 1:
221
+ held = next((h for h in (_HELD.get(), held) if h and not h.get("answered") and (not candidates or id(h) in candidates)), None)
222
+ if not held:
223
+ return None
224
+ found = {id(held): candidates.get(id(held), (held, None, set()))}
225
+ asker, fetch, _ = next(iter(found.values()))
226
+ if fetch:
227
+ fetch[0]["asker"], fetch[0]["tie"] = asker, fetch[1]
228
+ asker["fetched"] = True
229
+ if not asker.get("noted") and asker.get("finished"):
230
+ asker["late"] = True
231
+ asker["pinned"] = True
232
+ asker["calls_open"] = asker.get("calls_open", 0) + 1
233
+ _HELD.set(asker)
234
+ return asker
235
+
236
+ # A call made for the request has answered or failed. Each call is counted once, in `waiting` (its
237
+ # own, while it has not been answered later) or in `calls_open`: the two together are its calls
238
+ # still running, and one ending takes one off.
239
+ @staticmethod
240
+ def done(req):
241
+ if not req:
242
+ return
243
+ if req.get("waiting"):
244
+ req["waiting"] -= 1
245
+ else:
246
+ req["calls_open"] = max(0, req.get("calls_open", 0) - 1)
247
+
248
+ # A call made in a request's own context: its own, unless the request's reply went out before any
249
+ # model call answered it. Then a loop the request started (a queue drained from its handler) can
250
+ # keep its context while it works through later messages, so the call is its own only when the
251
+ # prompt carries its words, and is otherwise pinned as a call made outside every request is, the
252
+ # context breaking a tie. A request that answered with its own model call keeps every later one
253
+ # (a title written after the reply). A request whose reply went out before its first call is
254
+ # answered later.
255
+ def inside(self, req, sent):
256
+ if req.get("finished") and (not req.get("noted") or req.get("late")) and not self._says(req, self.norm(sent)):
257
+ return self.pinned(sent, req)
258
+ if not req.get("noted") and req.get("finished"):
259
+ req["late"] = True
260
+ if req.get("late"):
261
+ req["pinned"] = True
262
+ req["calls_open"] = req.get("calls_open", 0) + 1
263
+ else:
264
+ req["waiting"] = req.get("waiting", 0) + 1
265
+ return req
266
+
267
+ # The request's reply has gone out, with `status`, and `text()` reads it. One whose reply only took
268
+ # the message (a 2xx that says nothing of its own) while its model calls are still running and
269
+ # none of them has answered (a task the handler started without waiting on it) is answered later,
270
+ # those calls with it. A reply that says something, an apology after a time limit or an error, is
271
+ # the answer the person got. `heard`: a model call made for it got its response, set as the
272
+ # response arrives, so a stream the app passes straight on is never taken for one (lib/tied.cjs).
273
+ @staticmethod
274
+ def replied(req, status, text):
275
+ req["finished"] = True
276
+ if req.get("heard") or req.get("late") or not (req.get("waiting") or req.get("calls_open")):
277
+ return
278
+ if not (200 <= int(status or 0) < 300) or _speaks(text()):
279
+ return
280
+ req["late"] = req["pinned"] = True
281
+ req["calls_open"] = req.get("calls_open", 0) + req.get("waiting", 0)
282
+ req["waiting"] = 0
283
+
284
+ # Whether a request answered later is done, once: it has ended, none of its calls is open, and no
285
+ # request tied to it came to fetch the answer. It stays where a fetch that comes later can still
286
+ # find it and complete the exchange on a second request, but no call is pinned to it by its words.
287
+ def settled(self, req):
288
+ if not req or not req.get("late") or not req.get("ended") or req.get("calls_open") or req.get("waiting") or req.get("written") or req.get("settled") or req.get("fetched"):
289
+ return False
290
+ req["settled"] = req["answered"] = True
291
+ return True