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.
- package/README.md +8 -5
- package/lib/app-port.mjs +9 -0
- package/lib/auto-reach.mjs +88 -0
- package/lib/backing.mjs +230 -0
- package/lib/calls.mjs +23 -0
- package/lib/cli.mjs +115 -30
- package/lib/contact-text.mjs +167 -0
- package/lib/data.mjs +16 -22
- package/lib/dissection-text.mjs +232 -0
- package/lib/document.mjs +75 -0
- package/lib/env-secrets.mjs +48 -0
- package/lib/failed-text.mjs +36 -0
- package/lib/fresh.mjs +33 -12
- package/lib/health.mjs +61 -0
- package/lib/held.mjs +67 -0
- package/lib/home.mjs +55 -17
- package/lib/hosted.mjs +201 -0
- package/lib/identities.mjs +1 -1
- package/lib/keeping.mjs +162 -0
- package/lib/ledger.mjs +20 -0
- package/lib/lock.mjs +6 -1
- package/lib/mcp.mjs +52 -18
- package/lib/mint.mjs +179 -38
- package/lib/mongo.mjs +74 -0
- package/lib/numbers-text.mjs +68 -0
- package/lib/pg.mjs +118 -0
- package/lib/proc.mjs +100 -0
- package/lib/proof.mjs +1014 -0
- package/lib/proxy.mjs +184 -0
- package/lib/pyhook/cortad_sites.py +218 -0
- package/lib/pyhook/cortad_tied.py +291 -0
- package/lib/pyhook/sitecustomize.py +1175 -166
- package/lib/qdrant.mjs +47 -0
- package/lib/reach.mjs +223 -0
- package/lib/read-text.mjs +78 -24
- package/lib/redis.mjs +130 -0
- package/lib/register.mjs +36 -27
- package/lib/replay.mjs +464 -34
- package/lib/runner.mjs +85 -0
- package/lib/service.mjs +25 -0
- package/lib/spec.mjs +7 -4
- package/lib/start.mjs +33 -2
- package/lib/stick.mjs +65 -25
- package/lib/stores.mjs +347 -0
- package/lib/switches.mjs +36 -0
- package/lib/tether.mjs +9 -0
- package/lib/text.mjs +314 -179
- package/lib/tied.cjs +217 -0
- package/lib/trace.cjs +386 -514
- package/lib/verbs.mjs +155 -38
- package/lib/wire.cjs +882 -0
- package/lib/words.mjs +2 -0
- package/lib/writes.mjs +2 -2
- package/local.mjs +661 -297
- package/package.json +31 -1
- package/skill/SKILL.md +49 -42
- 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
|